Katkat

Theme-Park Wristband Payments: Expiring Credits, Zone Fees, Refunds

StripeSoftware Engineer · Technical Screen

Last 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_CREDIT opens or tops up ride-credit card card_id for guest_id with balance amount, usable only while t < expires_at. Output "".
  • RIDE first drains the guest's usable, non-expired credit cards (as of this event's t), picking the card with the earliest expires_at first, breaking ties by the smaller card_id lexicographically. 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}".
  • REFUND returns up to refund_amount of 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_rates has at most 100 non-overlapping ranges.
  • A REFUND always references a charge_id that already appeared in a RIDE event.

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?