Skip to content

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:

percentageDiscount = max(d.percentageDiscount for d in discounts) * subTotal

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:

shippingDiscount = min(calculatedShippingDiscount, 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

  1. Deploy LocalCouponService alongside LambdaCouponService
  2. Run both for every request; compare outputs
  3. Log discrepancies without affecting the response
  4. Once discrepancy rate is zero across production traffic, switch to local
  5. Deprecate Lambda invocation