Theme-Park Wristband Payments: Expiring Credits, Zone Fees, Refunds
StripeSoftware Engineer · Technical ScreenLast updated
Process a chronological log of theme-park wristband events that spend expiring ride credits before a linked card, apply per-zone card surcharges, and split refunds between the card and new store credit.
A theme park issues each guest a wristband that can hold several ride-credit cards (promotional balances with an expiry tick) plus one linked payment card for whatever a ride credit doesn't cover. Build the event processor that keeps every guest's balances correct.
Function Signature
def process_events(events: list[str], zone_surcharges: list[str]) -> list[str]:
events is a chronological list of comma-separated lines in one of three formats:
t,ADD_CREDIT,guest_id,card_id,amount,expires_at
t,RIDE,charge_id,guest_id,amount,zone_id
t,REFUND,charge_id,refund_amount
All amounts are non-negative integer cents. zone_surcharges entries look like "100-199:0.10", meaning a ride whose zone_id falls in [100, 199] pays a 10% surcharge on whatever part of the charge lands on the card. Return one output string per event, in order.
Rules
ADD_CREDITopens or tops up ride-credit cardcard_idforguest_idwith balanceamount, usable only whilet < expires_at. Output"".RIDEfirst drains the guest's usable, non-expired credit cards (as of this event'st), picking the card with the earliestexpires_atfirst, breaking ties by the smallercard_idlexicographically. Whatever remains after credits run out is billed to the card, with the zone surcharge applied to that remainder only (round half cents up). Output"{charge_id},{credit_used},{card_charged}".REFUNDreturns up torefund_amountof a charge's original (pre-surcharge) ticket price, capped at whatever hasn't been refunded yet. It comes first out of the charge's card-paid portion, reversed at the same surcharge rate the charge used; any leftover comes out of the credit-paid portion and is issued back to the guest as a brand-new, non-expiring store-credit card. Output"{charge_id},{card_refunded},{credit_refunded}".
Example
events = [
"1,ADD_CREDIT,G1,C1,2000,100",
"2,RIDE,R1,G1,5000,150",
"3,REFUND,R1,1800",
]
zone_surcharges = ["100-199:0.10", "200-299:0.05"]
process_events(events, zone_surcharges)
# -> ["", "R1,2000,3300", "R1,1980,0"]
The ride spends the full 2000 credit, then bills the remaining 3000 at a 10% zone-150 surcharge (3300). The refund of 1800 comes entirely from the card-paid portion, reversed at 10% (1980), since the card-paid base (3000) hasn't been touched yet.
Constraints
- Up to 10^5 events;
surcharge_rateshas at most 100 non-overlapping ranges. - A
REFUNDalways references acharge_idthat already appeared in aRIDEevent.
Hints
Hint 1
Give every guest a dict of card_id -> {balance, expires_at}. Build the usable-card list fresh for each RIDE and sort by (expires_at, card_id).
Hint 2
Only apply the surcharge to the part of the amount that actually lands on the card, and only compute a rate when that part is non-zero.
Hint 3
Per charge, remember the split (credit_paid, card_base, card_paid, rate) plus how much of the card_base has already been refunded, so repeated REFUNDs on the same charge stay consistent.
Hint 4
A refund's leftover-after-card-base should become a brand-new card with expires_at = infinity rather than trying to restore the original, possibly-already-expired, credit cards.
Follow-up questions
- How would you support a guest moving their wristband's remaining credits to a different guest?
- A park manager wants a daily report of total surcharge revenue per zone. How would you compute that without re-scanning the whole event log?
- What changes if two RIDE events for the same guest can arrive with the same timestamp and must be processed atomically against the same credit pool?
- How would you redesign this if credit cards could also expire mid-charge, i.e., a long-running multi-day event stream must re-check expiry as processing time advances?
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.