calculatecost2

Prices a basket. Given a product list and a shipping address it returns the delivery fee, every discount, and the grand total. The website calls it on the cart and checkout pages on every change.

It does not decide whether a coupon is valid — that is villacoupon3. calculatecost2 asks VillaCoupon3 for discounts, then folds them into the order maths.

Where it lives

DevMaster
Lambdacalculate-cost-3-devcalculate-cost-3-master
API Gatewayvn0mqj906dm4q33ulvm9
Stackcalculate-cost-2-devcalculate-cost-2-master
Accountdefault / 394922924679same

Dev deploys from the dev branch via GitHub Actions — see deploy-calculatecost2.

Master is not wired to VillaCoupon3

calculate-cost-3-master still points at the legacy coupon checker. Any request sent to m4q33ulvm9 returns an empty couponCodeList and no discounts. If a coupon “doesn’t work”, check which API the caller actually hit before debugging anything else.

Request shape

{
  "ownerId": "...", "branchId": "1000",
  "couponCodeList": ["FREESHIP"],
  "orderSource": "WEB",
  "productList": [{ "cprcode": 220772, "quantity": 1 }],
  "shipping": {
    "shippingType": "DELIVERY",
    "scheduleList": [{ "mode": "REGULAR" }],
    "shippingPostcode": "10110",
    "shippingLat": 13.748, "shippingLon": 100.557
  }
}

Two fields decide the delivery fee and are easy to get wrong:

shippingType defaults to PICKUP. Pickup means no delivery fee, so omitting it silently returns deliveryFee: 0 and any free-shipping coupon then looks like it did nothing. Send DELIVERY.

scheduleList[].mode defaults to NATIONWIDE. Omitting the mode gets you the nationwide rate rather than the branch rate, which is a different number for the same address.

How shipping is priced

REGULAR and EXPRESS use RegularPriceCalculator, which asks the PolygonCost API which branches can serve the customer’s lat/lon and at what price. NATIONWIDE uses NationwideCalculator, which prices on postcode and weight via get-nationwide-delivery-cost2-master. Both cache into DynamoDB; pass bypassCache: true when testing.

How discounts are folded in

Discount.from_villacoupon3 maps each VillaCoupon3 discount into calculatecost2’s fields — see discount-fields for the field-by-field contract, which is where two production bugs have already come from.

Order.grandTotal subtracts totalDiscount rather than re-listing each component. It used to list them by hand and omitted two4Discount, so two-for-X coupons reported a discount that was never taken off the bill (2026-07-29-coupon-test-sheet-failures). Keep it expressed in terms of totalDiscount so the two cannot drift apart.

Known failure modes

Coupons silently stop applying. The handler falls back to the legacy resolve-coupon-master path when the real calculator raises. The fallback returns a well-formed response with no coupons, so it looks like the coupon was rejected rather than like an error. Check CloudWatch for the fallback log line before assuming the coupon is misconfigured. This is what happened in 2026-07-25-coupon-90000008-not-applying.

Cold-start timeouts. The calculator pulls pandas/numpy/pyarrow, which once exceeded Lambda’s 10s init phase. calculatecost3/app.py imports the real handler lazily inside the function to keep init small. Don’t move that import back to module scope.

Key files

PathWhat it holds
src/calculateCost2/schema/order.pygrandTotal, totalDiscount, shippingDiscount, toDict()
src/calculateCost2/coupon/discount.pyVillaCoupon3 → calculatecost2 mapping
src/calculateCost2/coupon/couponCalculator.pyCross-account invoke of the checker
src/calculateCost2/shipping/Regular and nationwide price calculators
calculatecost3/app.pyLambda entry point, lazy import