Discount field contract
How a discount travels from villacoupon3 through calculatecost2 to the website. Both production bugs in this area came from a consumer disagreeing with this contract, so treat it as authoritative and update it when the shape changes.
The three hops
VillaCoupon3 Discount calculatecost2 Discount response fields
discountType → (category only)
discountAmount → discount / specificSkuDiscount / ... → cartDiscount, specificSkuDiscount
bogoDiscount → bogoDiscount → bogoDiscount
two4Discount → two4Discount → two4Discount
shippingDiscount → shippingDiscount → shippingDiscount
freeShipping (bool) → freeshipping (bool) → isFreeShipping
Mapping lives in Discount.from_villacoupon3 (src/calculateCost2/coupon/discount.py).
Rules that have already been broken
freeShipping the boolean beats discountType the label. A free_shipping coupon capped by maxDiscountAmount sends freeShipping: false with a partial shippingDiscount. Inferring free shipping from the type waives the entire fee. Only fall back to the type when the boolean is absent entirely.
discountAmount is final. Caps are already applied. Never recompute from percentageRate × subTotal.
Everything in totalDiscount must come off grandTotal. These used to be assembled independently and two4Discount was missing from one of them. grandTotal now subtracts totalDiscount so they cannot diverge:
total = subTotal + discountedDeliveryFee + expressShippingCost - totalDiscountAnything that reduces grandTotal must appear in the website’s Discount line. The frontend sums fields individually rather than using totalDiscount, so a new discount field needs adding there too or the Total drops while the Discount line reads 0.
Response fields
Discounts that reduce the goods total:
| Field | Source |
|---|---|
cartDiscount | fixed and percentage coupons |
bogoDiscount | bogo |
two4Discount | two_for_x |
specificSkuDiscount | specific_sku |
voucherDiscount | Villa gift vouchers, not coupons |
totalDiscount | Sum of all of the above |
Shipping is handled separately and is not part of totalDiscount:
| Field | Meaning |
|---|---|
deliveryFee | Fee before any discount |
shippingDiscount | Amount waived, capped at deliveryFee |
discountedDeliveryFee | deliveryFee - shippingDiscount, floored at 0 |
isFreeShipping | True only for a full waiver |
Checking a response is self-consistent
expected = subTotal + deliveryFee + expressShippingCost - totalDiscount - shippingDiscount
assert abs(grandTotal - expected) < 0.01Forgetting expressShippingCost here will make correct express orders look wrong by exactly 50 baht.
Worked examples
Two-for-X worth 17 on a 2851 basket, nationwide fee 115:
two4Discount 17 · totalDiscount 17 · grandTotal 2851 + 115 − 17 = 2949
Capped free shipping (TSHIPDIS, max 50) on a 90 baht regular fee:
freeShipping false · shippingDiscount 50 · discountedDeliveryFee 40 · isFreeShipping false
Full free shipping (FREESHIP) on the same fee:
freeShipping true · shippingDiscount 90 · discountedDeliveryFee 0 · isFreeShipping true