Skip to content

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:

grep -rn "import boto3\|import pynamodb\|import requests\|from botocore" calculatecost4/core/

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:

  1. calculatecost4/core/calculator.py -- the CostCalculator.calculate() method shows the full pipeline: parse -> enrich products -> shipping -> coupons -> vouchers -> serialize.

  2. calculatecost4/interfaces/ -- read all 6 Protocol files (10 lines each). These define every external dependency.

  3. calculatecost4/core/models/order.py -- Order.from_dict() shows how input is parsed, Order.to_dict() shows the response shape.

  4. test/conftest.py -- all mock service implementations. Copy these patterns when writing tests.

  5. calculatecost4/handler.py -- shows how adapters are wired to core at Lambda startup.


Common Tasks

Add a new field to Order

  1. Add the field to calculatecost4/core/models/order.py (the Order dataclass)
  2. Add it to _KNOWN_FIELDS set so from_dict accepts it
  3. Add it to to_dict() output
  4. Add tests in test/core/models/test_order.py covering: default value, from_dict parsing, to_dict output
  5. Run tests: pytest test/core/models/test_order.py -v

Add a new service interface

  1. 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: ...
  1. Add it to calculatecost4/interfaces/__init__.py
  2. 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
  1. Add it to CostCalculator.__init__ parameters
  2. Wire it in handler.py _build_calculator()
  3. Write tests:
  4. test/interfaces/test_protocols.py -- add isinstance check
  5. test/adapters/test_adapters.py -- add mocked test
  6. test/core/test_calculator.py -- add mock to existing calculator tests

Add a new adapter for an existing interface

  1. Create the adapter file in calculatecost4/adapters/
  2. It must satisfy the Protocol (same method signatures)
  3. Use cross_account.get_cross_account_session() for DynamoDB or cross_account.get_lambda_client() for Lambda
  4. Use calculatecost4.adapters.cache for caching
  5. Read config from os.environ with sensible defaults
  6. Raise exceptions from calculatecost4.core.exceptions

Fix a bug

  1. Write a failing test that reproduces the bug
  2. Run it -- confirm RED
  3. Fix the code
  4. Run it -- confirm GREEN
  5. 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:

bd create --title "Found: missing validation on quantity" --type bug --priority 2

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.skip without 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)