Agent Contribution Guide¶
Instructions for AI agents (Claude, Cursor, Codex, etc.) working on the villaCalculateCost4 codebase. This page is your onboarding document -- read it before writing any code.
Repository Layout¶
villaCalculateCost4/
calculatecost4/
core/ # PURE PYTHON -- no boto3, pynamodb, requests
models/ # Order, Product, Shipping, Voucher, Discount, Summary
calculator.py # CostCalculator (orchestrator -- start reading here)
totals.py # Grand total, subtotal, discount formulas
weight.py # Product weight calculation
shipping_calc.py # Shipping fee per schedule
coupon.py # CouponOrder payload builder
exceptions.py # All custom exceptions
interfaces/ # Protocol classes (6 service contracts)
adapters/ # AWS/HTTP implementations (DynamoDB, Lambda, HTTP)
cross_account.py # STS AssumeRole session (cross-account)
handler.py # Lambda entry point (wires adapters to core)
app.py # Docker CMD re-export
test/
core/ # Unit tests -- run without AWS
interfaces/ # Protocol conformance tests
adapters/ # Adapter unit tests (mocked)
integration/ # E2E tests against deployed endpoints (needs AWS)
conftest.py # Mock services (MockPricingService, etc.)
testData/ # 11 YAML fixtures
requirements/ # Source documentation (markdown)
template.yaml # SAM deployment template
Critical Rules¶
1. Core purity¶
calculatecost4/core/ must NEVER import boto3, pynamodb, requests, or any AWS SDK. Verify with:
This must return zero matches. If you need an external service, define a Protocol in interfaces/ and implement it in adapters/.
2. Test-first¶
Write the test file BEFORE writing the production code. Every commit that adds production code must include tests in the same commit. A commit that adds code without tests will be rejected.
3. No hardcoded infrastructure¶
Table names, Lambda ARNs, URLs -- all go in adapter constructors as parameters with defaults read from os.environ. Never hardcode them in the class body.
How to Read the Codebase¶
Start here, in this order:
-
calculatecost4/core/calculator.py-- theCostCalculator.calculate()method shows the full pipeline: parse -> enrich products -> shipping -> coupons -> vouchers -> serialize. -
calculatecost4/interfaces/-- read all 6 Protocol files (10 lines each). These define every external dependency. -
calculatecost4/core/models/order.py--Order.from_dict()shows how input is parsed,Order.to_dict()shows the response shape. -
test/conftest.py-- all mock service implementations. Copy these patterns when writing tests. -
calculatecost4/handler.py-- shows how adapters are wired to core at Lambda startup.
Common Tasks¶
Add a new field to Order¶
- Add the field to
calculatecost4/core/models/order.py(theOrderdataclass) - Add it to
_KNOWN_FIELDSset sofrom_dictaccepts it - Add it to
to_dict()output - Add tests in
test/core/models/test_order.pycovering: default value, from_dict parsing, to_dict output - Run tests:
pytest test/core/models/test_order.py -v
Add a new service interface¶
- Create
calculatecost4/interfaces/new_service.py:
from __future__ import annotations
from typing import Protocol, runtime_checkable
@runtime_checkable
class NewService(Protocol):
def do_thing(self, param: int) -> dict: ...
- Add it to
calculatecost4/interfaces/__init__.py - Create adapter
calculatecost4/adapters/new_adapter.py:
import os
from .cross_account import get_lambda_client # or get_cross_account_session
class NewAdapter:
def __init__(self, function_name: str | None = None):
self._function_name = function_name or os.environ.get("NEW_FUNC_ARN", "default-name")
def do_thing(self, param: int) -> dict:
client = get_lambda_client()
# ... implementation
- Add it to
CostCalculator.__init__parameters - Wire it in
handler.py_build_calculator() - Write tests:
test/interfaces/test_protocols.py-- add isinstance checktest/adapters/test_adapters.py-- add mocked testtest/core/test_calculator.py-- add mock to existing calculator tests
Add a new adapter for an existing interface¶
- Create the adapter file in
calculatecost4/adapters/ - It must satisfy the Protocol (same method signatures)
- Use
cross_account.get_cross_account_session()for DynamoDB orcross_account.get_lambda_client()for Lambda - Use
calculatecost4.adapters.cachefor caching - Read config from
os.environwith sensible defaults - Raise exceptions from
calculatecost4.core.exceptions
Fix a bug¶
- Write a failing test that reproduces the bug
- Run it -- confirm RED
- Fix the code
- Run it -- confirm GREEN
- Run full suite:
pytest test/core/ test/interfaces/ test/adapters/ test/test_handler.py -v
Mock Service Patterns¶
All mocks are in test/conftest.py. When writing tests, use these directly or create similar ones:
from test.conftest import (
MockPricingService,
MockProductLookup,
MockControlledProducts,
MockShippingPrice,
MockCouponService,
MockVoucherService,
)
# Build a calculator with all mocks
from calculatecost4.core.calculator import CostCalculator
calculator = CostCalculator(
pricing=MockPricingService(prices={(57822, 1049): 100.0}),
product_lookup=MockProductLookup(),
controlled_products=MockControlledProducts(),
shipping_price=MockShippingPrice(),
coupon_service=MockCouponService(),
voucher_service=MockVoucherService(),
)
result = calculator.calculate({
"productList": [{"cprcode": 57822, "productName": "Oil", "quantity": 1}],
"branchId": "1049",
})
assert result["grandTotal"] == 100.0
Interface Method Signatures¶
Quick reference for all 6 protocols -- these are the contracts your code depends on:
| Interface | Method | Signature | Returns | On Error |
|---|---|---|---|---|
| PricingService | get_price | (cprcode: int, brcode: int) |
float |
raises ProductPriceNotFoundException |
| PricingService | get_original_price | (cprcode: int, brcode: int) |
float |
returns 0 |
| ProductLookupService | get_product | (cprcode: int) |
dict |
raises ProductNotFoundError |
| ControlledProductService | get_controlled_product_list | () |
list[int] |
-- |
| ShippingPriceService | get_regular_price | (lat: float, lon: float, brcode: int) |
float |
raises BranchNotAvailableException |
| ShippingPriceService | get_nationwide_price | (postcode: str, weight: float, mode: str) |
float |
-- |
| CouponService | check_coupons | (order_dict: dict) |
DiscountResult |
-- |
| VoucherService | get_voucher | (voucher_id: str) |
Voucher |
raises VoucherNotFoundError |
Grand Total Formula¶
This is the single most important formula in the codebase:
grandTotal = max(0,
subTotal
+ discountedDeliveryFee
+ expressShippingCost
- cartDiscount
- bogoDiscount
- voucherDiscount
)
subTotal = max(0, sum(product.price * product.quantity for each product))
discountedDeliveryFee = max(0, deliveryFee - shippingDiscount)
deliveryFee = sum(schedule.deliveryFee for schedule in calculatedScheduleList)
Note: two4Discount and percentageDiscount are computed but NOT subtracted from grandTotal. This is intentional (see Known Behaviors).
File Naming Conventions¶
| Layer | Production file | Test file |
|---|---|---|
| Core model | calculatecost4/core/models/product.py |
test/core/models/test_product.py |
| Core logic | calculatecost4/core/totals.py |
test/core/test_totals.py |
| Interface | calculatecost4/interfaces/pricing.py |
test/interfaces/test_protocols.py |
| Adapter | calculatecost4/adapters/dynamodb_pricing.py |
test/adapters/test_adapters.py |
| Handler | calculatecost4/handler.py |
test/test_handler.py |
Pre-commit Checklist¶
Run all of these before committing:
# 1. Tests pass
pytest test/core/ test/interfaces/ test/adapters/ test/test_handler.py -v
# 2. Core purity
grep -rn "import boto3\|import pynamodb\|import requests\|from botocore" calculatecost4/core/ && echo "FAIL" && exit 1
# 3. No syntax errors
python -m py_compile calculatecost4/core/calculator.py # repeat for changed files
Gas Town / Beads Integration¶
This project uses Gas Town for agent coordination:
gt hook # Check what's assigned to you
gt prime # See your formula/checklist
bd show <issue-id> # View an issue
bd update <id> --notes "findings..." # Persist analysis
bd close <id> # Complete work
gt done # Submit work to merge queue
File new work as beads -- do not fix unrelated issues yourself:
What NOT to Do¶
- Do not add AWS imports to
calculatecost4/core/ - Do not hardcode table names, ARNs, or URLs in adapter classes
- Do not commit production code without tests
- Do not use
pytest.mark.skipwithout a tracking issue - Do not modify
test/testData/YAML fixtures (they are golden reference data) - Do not push directly to
main-- use branches and PRs - Do not delete or rename existing Protocol methods (breaking change)