73. Design a Billing and discounts System for an ecommerce app
Design Billing and discounts System for an ecommerce app
Implement a fully executable, in-memory billing and discounts system for an ecommerce app. You must design and implement a bill creation flow, a discount application flow, and a point/level calculation system.
Core Requirements
- Bill: Create a bill for a customer using a list of cart items, compute a subtotal, and track bill state (open/paid).
- Discount: Apply one or more discount codes to an open bill and compute the payable amount deterministically.
- Point/Level calculation system: When a bill is paid successfully, award loyalty points and update the customer’s level.
- In-memory: No database; store customers, bills, applied discounts, and points in memory.
- Deterministic IDs: Bill IDs must be generated sequentially as
B1, B2, B3, ... in creation order.
Data Format
Cart Item Encoding
Each cart item is provided as a single string. The format is: "itemName|unitPrice|quantity"
itemName is a non-empty string without the | character.
unitPrice is an integer representing price per unit (in dollars).
quantity is an integer.
- Subtotal contribution =
unitPrice * quantity.
Supported Discount Codes
P10: 10% off subtotal.
P20: 20% off subtotal.
FLAT100: Flat 100 off, applicable only if subtotal >= 500.
REDEEM: Redeem customer points for additional discount (see rules below).
Discount Rules
- Percentage discount: At most one percentage code can be effective. If both
P10 and P20 are applied, only the highest percentage is used.
- Flat discount:
FLAT100 can be applied at most once and only if subtotal >= 500.
- Redemption discount:
REDEEM can be applied at most once.
- Computation order:
- Start with
subtotal.
- Apply the effective percentage discount (if any).
- Apply
FLAT100 (if applicable).
- Apply
REDEEM (if applied).
- Rounding: All calculations must use integer math. Percentage discount uses floor:
percentDiscount = (subtotal * percent) / 100.
- Non-negative payable: Payable amount must not go below
0.
- Idempotency: Applying the same discount code multiple times must not stack. It should have no additional effect after the first time.
Point/Level Calculation System
Point Earning
- On successful payment, points earned =
floor(payableAmount / 100).
- Points are awarded only if the bill transitions to paid.
Point Redemption
- When
REDEEM is applied, the bill can use customer’s currently available points for discount at the rate 1 point = 1 dollar.
- Redemption amount is capped to
20% of the current payable amount (after percentage and flat discounts):
redeemCap = floor(currentPayable * 20 / 100)
- Redemption amount =
min(customerPoints, redeemCap).
- Points are deducted only if payment succeeds. If payment fails then no points are deducted.
Customer Levels
BRONZE: 0 - 99 points
SILVER: 100 - 499 points
GOLD: 500 - 1999 points
PLATINUM: 2000+ points
- Level is derived from current total points after any redemption deduction and newly earned points.
Error Handling
- If an operation cannot be completed due to invalid input or invalid state, return an error output as defined by the method contract.
- A bill can be paid at most once. Discounts can be applied only while the bill is open.
Method Signatures
1) Create bill
String createBill(String customerId, List<String> cartItems)
customerId is a non-empty string.
cartItems contains 1 or more strings, each in format "itemName|unitPrice|quantity".
unitPrice >= 0 and quantity > 0 for every item.
- Returns the new bill ID (
"B1", "B2", ...).
- If input is invalid, return
"ERROR".
2) Apply discount
long applyDiscount(String billId, String discountCode)
billId must refer to an existing, open bill.
discountCode must be one of: P10, P20, FLAT100, REDEEM.
- Discount application is idempotent per code.
- Returns the current payable amount after applying the code (and re-evaluating all applied codes).
- If
billId is invalid or bill is already paid, return -1.
- If
discountCode is unknown, ignore it and return the current payable amount (no change).
3) Pay bill (also updates points/level)
String payBill(String billId, long amountPaid)
billId must refer to an existing, open bill.
amountPaid must be exactly equal to the current payable amount.
- On success, the bill becomes paid, points are redeemed (if
REDEEM was applied), then new points are earned, and level is updated.
- Return receipt string in format:
"PAID|final=<finalAmount>|pointsEarned=<x>|totalPoints=<y>|level=<LEVEL>"
- If
billId is invalid, bill is already paid, or amountPaid mismatches, return "ERROR".
Constraints
1 <= cartItems.size() <= 10^5
0 <= unitPrice <= 10^9
1 <= quantity <= 10^6
- Total subtotal fits in 64-bit signed integer (
long).
- At most 4 distinct discount codes can be applied to a bill (from the supported set).
Examples
Example 1: Basic bill + percentage discount + payment earns points
createBill(customerId = "C1", cartItems = List.of("book|200|1", "pen|10|5")) returns "B1"
Explanation: Subtotal = 200*1 + 10*5 = 250; bill ID starts from B1.
applyDiscount(billId = "B1", discountCode = "P10") returns 225
Explanation: 10% of 250 is 25; payable becomes 250 - 25 = 225.
applyDiscount(billId = "B1", discountCode = "FLAT100") returns 225
Explanation: FLAT100 requires subtotal >= 500; subtotal is 250 so it is not applicable.
payBill(billId = "B1", amountPaid = 225) returns "PAID|final=225|pointsEarned=2|totalPoints=2|level=BRONZE"
Explanation: Points earned = floor(225/100)=2; total points become 2; level stays BRONZE.
Example 2: Multiple discounts including REDEEM (points used + points earned)
createBill(customerId = "C1", cartItems = List.of("shoes|600|1", "tshirt|200|2")) returns "B2"
Explanation: Subtotal = 600 + 200*2 = 1000.
applyDiscount(billId = "B2", discountCode = "P20") returns 800
Explanation: 20% of 1000 is 200; payable becomes 1000 - 200 = 800.
applyDiscount(billId = "B2", discountCode = "FLAT100") returns 700
Explanation: Subtotal >= 500, so flat 100 applies; payable becomes 800 - 100 = 700.
applyDiscount(billId = "B2", discountCode = "REDEEM") returns 698
Explanation: Customer C1 currently has 2 points from Example 1. Redeem cap = floor(700*20/100)=140. Redeem amount = min(2,140)=2. Payable becomes 700 - 2 = 698.
payBill(billId = "B2", amountPaid = 698) returns "PAID|final=698|pointsEarned=6|totalPoints=6|level=BRONZE"
Explanation: First deduct redeemed 2 points (2 - 2 = 0), then earn floor(698/100)=6. Total points become 6; level remains BRONZE.
Example 3: Invalid payment amount does not change state or points
createBill(customerId = "C2", cartItems = List.of("mouse|499|1")) returns "B3"
Explanation: Subtotal = 499.
applyDiscount(billId = "B3", discountCode = "P10") returns 450
Explanation: floor(499*10/100)=49; payable becomes 499 - 49 = 450.
payBill(billId = "B3", amountPaid = 449) returns "ERROR"
Explanation: Amount paid must match payable exactly; bill remains open and no points/level changes occur.