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 - totalDiscount

Anything 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:

FieldSource
cartDiscountfixed and percentage coupons
bogoDiscountbogo
two4Discounttwo_for_x
specificSkuDiscountspecific_sku
voucherDiscountVilla gift vouchers, not coupons
totalDiscountSum of all of the above

Shipping is handled separately and is not part of totalDiscount:

FieldMeaning
deliveryFeeFee before any discount
shippingDiscountAmount waived, capped at deliveryFee
discountedDeliveryFeedeliveryFee - shippingDiscount, floored at 0
isFreeShippingTrue only for a full waiver

Checking a response is self-consistent

expected = subTotal + deliveryFee + expressShippingCost - totalDiscount - shippingDiscount
assert abs(grandTotal - expected) < 0.01

Forgetting 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