Coupon Rewrite Spec¶
Specification for internalizing coupon logic from the external coupon3-checker-dev
Lambda into a local LocalCouponService within calculatecost4.
Current Architecture¶
LambdaCouponService¶
The current implementation invokes the coupon3-checker-dev Lambda function via
boto3. The adapter class LambdaCouponService implements the CouponService
protocol.
Payload Construction¶
CouponOrder.from_order() builds the payload sent to the coupon Lambda:
CouponOrder(
productList=[product._asdict() for product in order.productList],
branchId=order.branchId,
ownerId=order.ownerId,
shipping=order.shipping._asdict(),
couponCodeList=order.couponCodeList,
subTotal=0,
grandTotal=0,
totalExcludeControlledProducts=0,
expressShippingCost=express_shipping_cost,
deliveryFee=0,
totalWeight=total_weight,
)
The payload is wrapped in Event.getInput format before invocation.
Notable: subTotal, grandTotal, totalExcludeControlledProducts, and deliveryFee
are all hardcoded to 0 in the outgoing payload. The coupon Lambda does not use the
caller's computed totals -- it recalculates them internally.
Product Serialization¶
Products are serialized via _asdict(), which produces:
{
"cprcode": int,
"productName": str,
"quantity": int,
"scheduleId": int,
"isPreOrder": bool,
"originalPrice": float,
"price": float,
"rowTotal": float,
"discountedRowTotal": float,
"settlementPrice": float,
"weight": float,
"isControlledProduct": bool,
}
Shipping Serialization¶
Shipping is serialized via _asdict(), which includes all shipping fields and the
nested scheduleList with each schedule also serialized.
Response Contract¶
DiscountResult¶
The coupon Lambda returns a response that maps to DiscountResult:
| Field | Type | Description |
|---|---|---|
discounts |
list[Discount] |
Successfully applied discount entries |
failedCoupons |
list[str] |
Coupon codes that failed validation |
error |
dict |
Error details; empty dict on success |
Discount¶
Each entry in discounts represents one successfully applied coupon:
| Field | Type | Description |
|---|---|---|
coupon |
str |
The coupon code |
discount |
float |
Flat cart-level discount amount |
bogoDiscount |
float |
Buy-one-get-one discount amount |
two4Discount |
float |
2-for-price promotion discount |
percentageDiscount |
float |
Percentage-based discount (as a ratio, not amount) |
freeshipping |
bool |
Whether this coupon grants free shipping |
shippingDiscount |
float |
Partial shipping discount amount |
Coupon Types¶
The coupon Lambda supports six discount types. A single coupon produces exactly one type of discount (the fields for other types are zero/false):
BOGO (Buy-One-Get-One)¶
Populates bogoDiscount. The cheapest qualifying product in the order is free.
The discount amount equals the price of the free product.
TWO4 (2-for-Price)¶
Populates two4Discount. Two qualifying products are sold for a fixed combined
price. The discount is the difference between the sum of their individual prices
and the 2-for price.
Percentage Discount¶
Populates percentageDiscount as a ratio (e.g., 0.10 for 10%). The caller
multiplies this by subTotal to get the actual discount amount:
Only the highest percentage across all coupons is applied.
Cart Discount (Flat Amount)¶
Populates discount. A fixed THB amount subtracted from the order total.
Free Shipping¶
Sets freeshipping = True. The caller sets shippingDiscount = deliveryFee,
making the entire delivery fee free.
Shipping Discount (Partial)¶
Populates shippingDiscount with a specific THB amount. The caller caps this
at deliveryFee:
Target Architecture¶
LocalCouponService¶
A new class LocalCouponService that implements the same CouponService protocol
as LambdaCouponService. It replaces the Lambda invocation with in-process logic.
class CouponService(Protocol):
def check_coupons(self, order_dict: dict) -> DiscountResult: ...
class LocalCouponService:
def check_coupons(self, order_dict: dict) -> DiscountResult:
# In-process coupon validation and discount calculation
...
Interface Contract¶
The check_coupons method accepts the same order_dict produced by
CouponOrder.from_order() and returns the same DiscountResult structure.
No changes to the caller are required.
Required Knowledge¶
To implement LocalCouponService, the following must be extracted from the
coupon3-checker-dev Lambda:
| Knowledge area | Description |
|---|---|
| Coupon storage | Where coupons are stored (DynamoDB table, schema, indexes) |
| Validation rules | Expiry, usage limits, minimum order, branch restrictions |
| Product eligibility | How coupons match to products (by cprcode, category, brand) |
| Stacking rules | Whether multiple coupons can combine; priority/ordering |
| BOGO matching | Algorithm for selecting the free product |
| TWO4 matching | Algorithm for selecting the 2-for-price pair |
| Percentage cap | Why only the max percentage is applied |
| Free shipping eligibility | Conditions beyond the coupon flag |
Data Dependencies¶
The local service will need direct access to:
| Resource | Current access | Local access |
|---|---|---|
| Coupon definitions | Internal to Lambda | DynamoDB adapter (new) |
| Product categories | Internal to Lambda | May need product catalog adapter |
| Usage tracking | Internal to Lambda | DynamoDB adapter (new) |
Validation Strategy¶
- Deploy
LocalCouponServicealongsideLambdaCouponService - Run both for every request; compare outputs
- Log discrepancies without affecting the response
- Once discrepancy rate is zero across production traffic, switch to local
- Deprecate Lambda invocation