VillaCoupon3

Validates coupons and computes discount amounts. It is the source of truth for what a coupon is worth; it never touches the order total. calculatecost2 calls it and applies the result.

Replaces the older coupon1/coupon2 services. It supports rules they could not: specific-SKU targeting, two-for-X with per-set caps, cross-SKU BOGO pooling, order-source restrictions, and auto-applied promotions.

Where it lives

Value
Accountvilla-ecommerce / 017176210331
Checker (dev)villacoupon3-checker-dev
API Gatewayqe1hv09c68
Local clone~/stacks/villacoupon3

calculatecost2 invokes it cross-account via COUPON_FUNCTION_ARN:

arn:aws:lambda:ap-southeast-1:017176210331:function:villacoupon3-checker-dev

The villa-ecommerce side grants invoke permission by resource policy. If that statement is removed, calculatecost2 falls back to the legacy checker and coupons quietly stop applying.

Response contract

{
  "discounts": [{
    "couponCode": "TSHIPDIS",
    "discountType": "free_shipping",
    "discountAmount": 0.0,
    "shippingDiscount": 50,
    "freeShipping": false,
    "bogoDiscount": 0.0,
    "two4Discount": 0.0,
    "appliedToProducts": []
  }],
  "failedCoupons": [],
  "error": {}
}

discountAmount is already final — caps like maxDiscountAmount are applied before it is returned. Consumers must not re-derive it from percentageRate × subTotal.

discountType is not enough to know the behaviour

A free_shipping coupon with maxDiscountAmount set waives only part of the fee. VillaCoupon3 signals that correctly with freeShipping: false and a partial shippingDiscount, but a consumer that infers “free shipping” from the type alone will waive the whole fee. That exact bug shipped — see 2026-07-29-coupon-test-sheet-failures. The boolean is authoritative; the type is a category label.

Calculators

Each coupon type has a calculator in src/villacoupon3/coupon/calculator.py:

TypeFunctionNotes
fixed_calculate_fixedFlat baht off the cart
percentage_calculate_percentageHonours maxDiscountAmount
bogo_calculate_bogoPools units across SKUs
two_for_x_calculate_two_for_xPer-set cap; sorts units by price descending
free_shipping_calculate_free_shippingmaxDiscountAmount turns it into a partial waiver
shipping_calculate_shipping_discountExplicit partial shipping discount
specific_sku_calculate_specific_skuPercentage of the matching SKUs’ total

Note that free_shipping + maxDiscountAmount and shipping express the same intent by different routes. Both appear in production data.

Track record

As of 2026-07-29 every coupon failure investigated on dev traced to a consumer, not to VillaCoupon3. Its amounts and flags were correct in all cases in coupon-test-matrix. Verify the checker’s raw response before changing anything here:

curl -sS -X POST 'https://dev.villamarket.net/api/coupon2/couponChecker' \
  -H "Authorization: Bearer $APIKEY" -H 'Content-Type: application/json' \
  -d '{"couponCodeList":["TSHIPDIS"],"branchId":"1000","subTotal":3862,"deliveryFee":90,"orderSource":"WEB","productList":[]}'