Split-Tender Payment Events with Gift Card Expiry, ZIP Surcharges and Refunds
StripeSoftware Engineer · Technical ScreenLast updated
Implement a payment event processor for split-tender retail purchases, where each charge spends a customer's gift cards before billing the rest to a credit card. It tests precise simulation: earliest-expiry card priority with lexicographic tie-breaks, ZIP-range surcharges on the credit card portion, refunds split between card and store credit, and exact output formatting.
A retail customer often pays for one purchase with several payment methods, for example covering a 100-dollar purchase with a 40-dollar gift card plus 60 dollars on a credit card. This is called a split-tender transaction, and point-of-sale payment systems for retail merchants must support it.
Write the part of such a system that processes split-tender payments. For each purchase, the system first spends the customer's available gift card balance and charges whatever remains to the customer's credit card. Given a chronological stream of payment events, compute the result of every event.
The rules come in four layers, and your function must apply all of them together: basic split tendering, gift card expiration and priority, regional credit card surcharges, and refunds.
Function Signature
def process_events(events: list[str], surcharge_rates: list[str]) -> list[str]:
events holds one line per event, in chronological order. Each line uses one of three comma-separated formats:
timestamp,ADD_GIFTCARD,customer_id,card_id,amount,valid_before
timestamp,CHARGE,charge_id,customer_id,amount,zip_code
timestamp,REFUND,charge_id,refund_amount
Each entry of surcharge_rates has the format start_zip-end_zip:rate. For example, 90000-99999:0.05 means that a charge whose ZIP code is between 90000 and 99999 inclusive pays a 5% surcharge on its credit card portion.
Return a list with exactly one string per event, in the same order as events.
Rules
All money amounts in the input and the output are non-negative integers counting cents: 4000 means 40.00 dollars. Output amounts are written as plain base-10 integers.
Layer 1 — Gift cards and basic split tendering
- Customers start with no gift cards.
ADD_GIFTCARDgives customercustomer_ida new gift cardcard_idwhose balance isamount. Each card keeps its own balance, and a customer may hold any number of cards at once. A card belongs to one customer and is never used for another customer's charges. - The output for an
ADD_GIFTCARDevent is the empty string"". - A
CHARGEofamountforcustomer_idis paid from that customer's usable gift cards first (layer 2 says which cards are usable and in which order). In priority order, take from each card the smaller of its balance and the amount still unpaid, and subtract it from that card's balance, so one card is fully drained before the next one is touched. Whatever is still unpaid after the usable cards run out is the credit card portion; if the cards cover everything, the credit card portion is 0. - The output for a
CHARGEevent is the final credit card charge, followed by one,card_id:amount_useditem for every card that supplied a positive amount to this charge:
CC_CHARGE_AMOUNT,GIFT_CARD_ID_1:AMOUNT_USED,GIFT_CARD_ID_2:AMOUNT_USED
The card items are listed in ascending card_id order, not in the order the cards were drained. If no card was used, the output is only the credit card amount, for example "1000". CC_CHARGE_AMOUNT (written CC_CHARGE below) includes the surcharge from layer 3.
Layer 2 — Expiration and card priority
- A card created by
ADD_GIFTCARDis valid at timeTonly ifT < valid_before. WhenT >= valid_before, the card is expired and its remaining balance is void for good. - Cards created by refunds (layer 4) have no expiration date and are always valid. Every
ADD_GIFTCARDline carries an integervalid_before, so refund-created cards are the only cards without an expiration date. - For a charge at time
T, the usable cards are the customer's cards that are valid atTand have a positive balance. They are used in this priority order:
- cards with a finite
valid_before, earliestvalid_beforefirst; - after all of those, cards with no expiration date;
- cards that share the same expiration, including two cards that both have no expiration date, in ascending
card_idorder.
Layer 3 — Regional surcharges
- Find the range in
surcharge_ratesthat contains the charge'szip_code, comparing ZIP codes as numbers. If no range contains it, the rate is 0. - The surcharge applies only to the credit card portion; amounts paid by gift cards are never surcharged.
CC_CHARGE = credit_card_portion + credit_card_portion × rate. For example, a credit card portion of10000at rate0.05givesCC_CHARGE = 10500. - Every input guarantees that the exact value of
credit_card_portion × rateis a whole number of cents. Output that exact value; no rounding is involved.
Layer 4 — Refunds
For a REFUND of refund_amount against an earlier charge_id:
- Real money first:
CC_REFUNDis the smaller ofrefund_amountand theCC_CHARGEthat the referenced charge actually put on the credit card, surcharge included. - Store credit second:
GC_REFUND = refund_amount - CC_REFUND. IfGC_REFUNDis positive, create a new gift card for the customer of the original charge, withcard_idequal toREFUND_followed by the charge id (forchg_1it isREFUND_chg_1), balanceGC_REFUND, and no expiration date. It takes part in later charges like any other card. IfGC_REFUNDis 0, no card is created. - A refund never puts money back on the gift cards that the original charge drew from.
- The output for a
REFUNDevent isCC_REFUND,GC_REFUND, for example"10500,1500"or"3000,0".
Comparing card ids
Wherever card_id values are ordered, compare them as strings by character code (ASCII order, which is Python's default string comparison). Uppercase letters sort before lowercase letters, so REFUND_chg_1 comes before gc_2.
Constraints
1 <= len(events) <= 10^4and0 <= len(surcharge_rates) <= 1000.- Every line is well formed. Timestamps are integers with
0 <= timestamp <= 10^9and are non-decreasing alongevents; events with equal timestamps take effect in list order. 1 <= valid_before <= 10^9. A card may be added already expired; it is then never usable.1 <= amount <= 10^8inADD_GIFTCARDandCHARGElines.customer_id,card_idandcharge_idare 1 to 20 characters drawn from ASCII letters, digits and underscores.card_idvalues inADD_GIFTCARDlines are unique and never start withREFUND_.charge_idvalues are unique acrossCHARGElines.zip_codeis exactly five digits and may start with 0. Each range has five-digit bounds withstart_zip <= end_zip, and no two ranges overlap.- Each
rateis a decimal number with at most 4 digits after the decimal point and0 <= rate <= 1. - Every
REFUNDreferences aCHARGEthat appears earlier inevents, and eachcharge_idis refunded at most once.refund_amountis at least 1 and at most the total originally paid for that charge: itsamountplus its surcharge. - Every output amount is at most
2 * 10^8, which fits in a signed 32-bit integer. The sum of one customer's balances across all cards can exceed2^31 - 1but stays below2^53.
Examples
Example 1
Input:
events = [
"1,ADD_GIFTCARD,cust1,gc1,5000,10",
"2,ADD_GIFTCARD,cust1,gc2,4000,5",
"3,CHARGE,chg1,cust1,6000,10000",
"11,CHARGE,chg2,cust1,1000,10000"
]
surcharge_rates = []
Output: ["", "", "0,gc1:2000,gc2:4000", "1000"]
At time 3 both cards are valid. gc2 expires earlier (5 versus 10), so it is drained for 4000 first, and gc1 supplies the remaining 2000. The output lists gc1 before gc2 because the items are sorted by card_id. At time 11 both cards have expired (11 >= 10 and 11 >= 5), so the 3000 left on gc1 is void and the whole 1000 goes to the credit card. There are no surcharge ranges, so the rate is 0.
Example 2
Input:
events = [
"1,ADD_GIFTCARD,cust1,gc1,5000,10",
"2,CHARGE,chg1,cust1,15000,90210",
"3,REFUND,chg1,12000"
]
surcharge_rates = ["90000-99999:0.05"]
Output: ["", "10500,gc1:5000", "10500,1500"]
gc1 is valid at time 2 and covers 5000 of the 15000, leaving a credit card portion of 10000. ZIP 90210 falls in 90000-99999, so the surcharge is 10000 × 0.05 = 500 and CC_CHARGE is 10500. The refund of 12000 returns 10500 to the credit card, which is everything charged there, and the remaining 1500 becomes a new non-expiring card REFUND_chg1 for cust1 with balance 1500.
Example 3
Input:
events = [
"1,ADD_GIFTCARD,cust_1,gc_1,10000,10",
"2,CHARGE,chg_1,cust_1,20000,15000",
"3,REFUND,chg_1,21000",
"15,CHARGE,chg_2,cust_1,5000,15000",
"16,ADD_GIFTCARD,cust_1,gc_2,3000,20",
"17,CHARGE,chg_3,cust_1,9000,15000"
]
surcharge_rates = ["10000-20000:0.10"]
Output: [
"",
"11000,gc_1:10000",
"11000,10000",
"0,REFUND_chg_1:5000",
"",
"1100,REFUND_chg_1:5000,gc_2:3000"
]
- Time 2:
gc_1is drained for 10000. The credit card portion is 10000, and the 10% surcharge makesCC_CHARGE11000. - Time 3: the refund of 21000 returns 11000 to the credit card and creates the non-expiring card
REFUND_chg_1with balance 10000. - Time 15:
gc_1is empty and also expired.REFUND_chg_1covers all 5000, keeping 5000 of its balance, and the surcharge on a credit card portion of 0 is 0. - Time 17:
gc_2has a finite expiration, so it is used before the non-expiringREFUND_chg_1.gc_2gives 3000,REFUND_chg_1gives its remaining 5000, and the last 1000 goes to the credit card, which becomes 1100 with the surcharge. In the output,REFUND_chg_1is listed beforegc_2because uppercaseRsorts before lowercaseg.
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.