Two-for-X ignored and capped free shipping over-waived
Five rows of the dev coupon test sheet failed. They turned out to be three separate defects — two in calculatecost2, one in the website. villacoupon3 returned correct amounts in every case.
| Sheet row | Coupon | Reported |
|---|---|---|
| 7, 8 | two-for-X 1260002784, 1260002613 | no discount |
| 10 | specific SKU HACOD150 | no discount |
| 11 | shipping discount TSHIPDIS | waived all 90 baht instead of 50 |
Two-for-X was counted but never charged
Order.grandTotal listed each discount component by hand and omitted two4Discount, while totalDiscount included it. The response therefore advertised a discount that was never taken off:
subTotal 2851 · totalDiscount 17 · deliveryFee 115 · grandTotal 2966 ← 2851 + 115, no 17
two4Discount was also missing from the top level of toDict() — it existed only inside summary — so the website read undefined.
Fixed by expressing grandTotal in terms of totalDiscount so the two cannot drift apart again, and by exposing two4Discount alongside the other discount fields.
total = subTotal + discountedDeliveryFee + expressShippingCost - totalDiscountA capped free-shipping coupon waived the whole fee
TSHIPDIS is a free_shipping coupon with maxDiscountAmount: 50. VillaCoupon3 handles that correctly — _calculate_free_shipping compares the capped amount against the fee and reports a partial waiver:
{"discountType": "free_shipping", "shippingDiscount": 50, "freeShipping": false}calculatecost2 then ORed the explicit flag with the type:
freeshipping=bool(
raw.get("freeshipping") or raw.get("freeShipping")
or discount_type in ("free_shipping", "freeshipping") # ← always wins
)discount_type alone forced True, and Order.shippingDiscount returns the full deliveryFee whenever freeShipping is set. The explicit boolean now wins; the type is consulted only when no boolean is present. See discount-fields.
Shop-by-brand was correct but invisible
Row 10 was already right at the API level — specificSkuDiscount: 150 and grandTotal reduced accordingly. The website’s Discount line summed only cartDiscount + bogoDiscount + voucherDiscount, so the Total dropped while the Discount line read 0. A tester reasonably calls that “no discount”. Fixed in villaEcommerceWeb#315, which adds two4Discount and specificSkuDiscount to that sum in both cart and checkout.
Verification
All 12 sheet rows re-run against dev after deploy 094bd0c, checking grandTotal == subTotal + deliveryFee + expressShippingCost − totalDiscount − shippingDiscount:
| Row | Coupon | Before | After |
|---|---|---|---|
| 1, 2, 5, 6 | bogo | pass | pass |
| 7 | 1260002784 | no discount | two4=17, grand 2949 |
| 8 | 1260002613 | no discount | two4=33, grand 2879 |
| 9 | MINDIS50 | pass | pass |
| 10 | HACOD150 | no discount | sku=150, grand 5017 |
| 11 | TSHIPDIS | 90 of 90 waived | 50 of 90 waived |
| 11b | TSHIPDIS express | 90 of 90 waived | 50 of 90 waived |
| 12, 13 | FREESHIP | pass | 90 of 90 waived |
Regression tests added in test/coupon/test_discount_mapping.py and test/order/test_grandtotal_discounts.py.
Worth remembering
Two of the three defects were the same mistake in different clothes: a total assembled in two places. grandTotal and totalDiscount each maintained their own list of components; the frontend maintained a third. Any new discount type has to be added to all three or it half-works. Deriving one from another removes the class of bug — which is why grandTotal now subtracts totalDiscount instead of re-listing.
The third defect is the recurring lesson about typed enums that carry a hidden qualifier. free_shipping describes a category, not a behaviour, once maxDiscountAmount can change what it does. When a type can be modified by another field, the derived boolean is the contract.