Harbor Hall On-Sale: Holds, Best Available and Resale
HardAI-assisted coding · AI-Assisted Debugging
Harbor Hall On-Sale: Holds, Best Available and Resale
You've joined the box-office platform team. Tonight is the on-sale for Harbor Hall, Night 1 (3,332 seats). The ticketing backend already lets buyers hold specific seats and check out, but last weekend's presale went badly: seats stayed locked for buyers who had walked away, a promo code ran dry long before its limit, and one buyer ended up with nine tickets. You have the incident tickets, a roadmap with three features behind them, and a codebase to get through them. All money is integer cents.
Codebase
| File | What it does |
|---|---|
ticketing/api.py | run(operations) and the Dispatcher that validates the shape of each operation and routes it |
ticketing/service.py | TicketService: one method per operation; advance(ts) runs before every operation |
ticketing/holds.py | HoldBook: creates, releases and checks out holds; _unwind undoes a hold that ends without a sale |
ticketing/inventory.py | SeatInventory: which seats are blocked, held or sold |
ticketing/limits.py | LimitTracker: cached per-buyer counters of held seats and owned tickets |
ticketing/promos.py | Promo table and PromoBook (reserved / used counters, discount maths) |
ticketing/tickets.py | Issued tickets and the T-n / O-n id counters |
ticketing/venue.py | Sections, row widths, prices, seat labels and range parsing, the accessible pods |
ticketing/selection.py | choose_block: best-available seat selection (stub, Part 2) |
ticketing/resale.py | ResaleMarket (stub, Part 3) |
ticketing/loadgen.py | rush: synthetic on-sale traffic for load tests (Part 4). Do not modify. |
ticketing/config.py, errors.py, checks.py, models.py | Constants, error codes, argument validators, records |
tests/ | Example tests (some currently fail) |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh sale and returns one result per operation. Every operation is [name, ts, ...args]; ts is an integer number of seconds and never decreases from one operation to the next. Errors are returned as "error: <code>" strings, never raised. An unknown operation name returns "error: unknown operation"; a missing or non-integer ts, the wrong number of arguments, or a malformed argument (empty buyer, a non-string seat spec, a seat named twice in one request, ...) returns "error: invalid arguments".
| Operation | Arguments after ts | Returns | Errors, in the order they are checked |
|---|---|---|---|
block | specs | number of seats blocked | unknown seat, seat unavailable |
hold | buyer, specs, [promo] | hold view | unknown seat, limit exceeded, unknown promo, promo minimum not met, promo exhausted, seat unavailable |
best (Part 2) | buyer, tier, n, [promo] | hold view | limit exceeded, unknown promo, promo minimum not met, promo exhausted, not enough seats |
hold_accessible (Part 3) | buyer, party, [promo] | hold view | limit exceeded, promo errors as above, no accessible seats |
checkout | hold_id, buyer | order | hold not found, not your hold, hold expired, hold closed |
release | hold_id, buyer | true | same four as checkout |
availability | tier ("A", "B" or "C") | number of open seats in the tier | |
buyer_summary | buyer | {"held": n, "owned": n} | |
promo_status | code | {"limit", "reserved", "used"} | unknown promo |
tickets | buyer | [[ticket id, seat label], ...] in ticket-number order | |
list_resale, buy_resale, cancel_listing (Part 3) | see Part 3 | ||
rush (Part 4) | seed, count, step | summary object |
Every error is validated in the order shown: buyer-level rules (limit, then promo) are checked before any seat is looked at, because the box office does not search inventory for a buyer who cannot buy. invalid arguments always wins over everything else.
Seats. A seat label is "<SECTION>-<row>-<number>", e.g. "ORC-3-12". A *spec* is one label or an inclusive range "ORC-3-5..9". Rows and seat numbers start at 1; the hall widens away from the stage. A spec that names a section, row or seat number outside the table below, a reversed range (5..3) or anything that is not of the form above is unknown seat.
| Section | Tier | Rows | Seats per row |
|---|---|---|---|
PIT | A | 1-8 | 36 |
ORC | A | 1-24 | 28 (rows 1-6), 30 (7-12), 32 (13-18), 34 (19-24) |
MEZ | B | 1-20 | 44 (rows 1-10), 46 (11-20) |
BAL | C | 1-25 | 56 |
Seat lists in results (seats, ticket lists in orders) are in venue order: section order above, then row, then number. A seat that is not blocked, held or sold is *free*. block marks free seats as house kills that are never sold (all-or-nothing; blocking a seat that is not free is seat unavailable).
Money. List prices: tier A 14950, tier B 9525, tier C 4575. A service fee of 350 is added per ticket. A *hold view* is {"hold": "H-1", "seats": [...], "expires_at": ts + 600}; hold ids are H-1, H-2, ... in the order holds are successfully created. checkout returns {"order": "O-1", "tickets": ["T-1", ...], "subtotal", "discount", "fees", "total"} where total = subtotal - discount + fees. Ticket ids are T-1, T-2, ... in the order tickets are issued.
Promos (table in promos.py; one code per order, a code is optional on hold/best/hold_accessible). percent promos take subtotal * value // 100 off (rounded down); fixed promos take min(value, subtotal) off. Fees are never discounted. A promo needs at least min_qty seats in the order (promo minimum not met). Each hold reserves one use of its code regardless of how many seats it has. A code has max_uses, and while max_uses - used - reserved is 0 it is promo exhausted.
Buyer limit. A buyer may hold plus own at most 6 tickets: a request that would push held + owned above 6 is limit exceeded.
Worked example (what works today)
[["hold", 1, "alice", ["PIT-2-18..19"]],
["checkout", 50, "H-1", "alice"],
["buyer_summary", 51, "alice"]]returns
[{"hold": "H-1", "seats": ["PIT-2-18", "PIT-2-19"], "expires_at": 601},
{"order": "O-1", "tickets": ["T-1", "T-2"], "subtotal": 29900, "discount": 0, "fees": 700, "total": 30600},
{"held": 0, "owned": 2}]Part 1 — Incident: holds that never let go (BOX-412, BOX-418, BOX-427)
Goal: make a hold really temporary and make the buyer-limit and promo counters tell the truth, so that nothing a hold touches stays stuck.
Reports from the presale:
- "My cart timed out ten minutes ago and the seats are *still* shown as taken." (
BOX-412) Holds are created with anexpires_atand then never looked at again. - "
HARBOR10is limited to 50 uses but stopped working after 25 checkouts." (BOX-418) - "One account bought three orders of 3 and the system never said no." (
BOX-427)
Rules
- A hold created at
tsexpires atts + 600. It is usable whilets' < expires_at; atts' >= expires_atit has lapsed. - Expiry is lazy: before executing *any* operation (reads and operations that will fail included), every active hold with
expires_at <= tslapses, oldestexpires_atfirst, ties by lower hold number. Lapsing undoes everything the hold did: its seats are free again, the buyer's held count drops, and a promo use it reserved is released. A lapsed hold answershold expiredtocheckout/release. - Check-out turns the hold's seats into owned tickets (
heldgoes down,ownedgoes up by the same amount) and turns its promo reservation into a used promo (reserved-1,used+1). - A hold that was checked out or released is
hold closed.hold not foundis for ids that never existed;not your holdis checked before the hold's status. - Everything else in the table above must keep working as written.
Failing example tests: tests/test_holds.py.
Example
[["hold", 10, "alice", ["BAL-1-1..2"], "HARBOR10"],
["promo_status", 609, "HARBOR10"],
["checkout", 610, "H-1", "alice"],
["promo_status", 610, "HARBOR10"],
["buyer_summary", 610, "alice"],
["hold", 611, "bob", ["BAL-1-1..2"]]]returns
[{"hold": "H-1", "seats": ["BAL-1-1", "BAL-1-2"], "expires_at": 610},
{"limit": 3, "reserved": 1, "used": 0},
"error: hold expired",
{"limit": 3, "reserved": 0, "used": 0},
{"held": 0, "owned": 0},
{"hold": "H-2", "seats": ["BAL-1-1", "BAL-1-2"], "expires_at": 1211}]Part 2 — Best available (BOX-440)
Goal: implement ["best", ts, buyer, tier, n, promo?]: choose n side-by-side seats for the buyer in the given tier and hold them exactly as hold would (same expiry, buyer limit, promo reservation, same hold view). tier is "A", "B" or "C"; n is an integer >= 1 (anything else is invalid arguments). Checks happen in the order invalid arguments, limit exceeded, promo errors, not enough seats (no block satisfies the rules below). A failed best changes nothing.
Rules
- A *candidate* is
nconsecutive seat numbers in one row of one section of the tier, all free. (Tier A covers bothPITandORC.) - No orphan seats (guarded sections only).
ORCandMEZare *guarded* (Section.guardinvenue.py);PITandBALare not. In a guarded section, discard a candidate if, after taking it, exactly one free seat would be left between the block and the nearest non-free seat or the end of the row, on either side. In other words: count the free seats immediately to the left of the block (L) and immediately to the right (R); the candidate is discarded ifL == 1orR == 1.L = 0orR >= 2are fine. There is no fallback: if every candidate is discarded the answer isnot enough seats. - Among the remaining candidates pick the one with the smallest
(offset, section index, row, first seat number)compared in that order, where, for a block starting at seatsin a row ofwseats,offset = |2*s + n - 1 - (w + 1)|(twice the distance between the middle of the block and the middle of the row, so everything stays an integer). "Section index" is the order of the table above. - The result lists the
nseats of the winning block.
Example (row MEZ-2 has only seats 20-23 free; row MEZ-1 is blocked)
[["block", 1, ["MEZ-1-1..44", "MEZ-2-1..19", "MEZ-2-24..44"]],
["best", 2, "dana", "B", 3],
["best", 3, "erin", "B", 2],
["availability", 4, "B"]]returns
[84,
{"hold": "H-1", "seats": ["MEZ-3-21", "MEZ-3-22", "MEZ-3-23"], "expires_at": 602},
{"hold": "H-2", "seats": ["MEZ-2-22", "MEZ-2-23"], "expires_at": 603},
811]Dana's three seats in MEZ-2-20..22 or -21..23 would each leave one orphan seat, so she is pushed to row 3. Erin's pair at MEZ-2-22..23 leaves two free seats on the left and none on the right, which is allowed, and ties row 3 on offset, so the lower row wins.
Part 3 — Accessible pods and the resale marketplace (ACC-88, BOX-455)
Goal: two requirements changes land together. Accessibility wants wheelchair spaces and their companion seats kept off the general sale until an hour in, and the business wants a resale marketplace with a face-value price cap.
3a. Accessible pods
venue.ACCESSIBLE_PODS lists four pods. A pod is an accessible space plus its two companion seats immediately to its right (for (ORC, row 1, 2): seats 2, 3 and 4). Until now pods were ordinary seats; from this Part on:
- Withheld seats. While
ts < 3600(config.ACCESSIBLE_RELEASE_TS) a *free* pod seat is *withheld*: it is not open to the general sale. Fromts >= 3600it is an ordinary seat. Whether a seat is withheld depends only on thetsof the operation looking at it, never on what happened earlier. (A pod seat that is held or sold is simply taken.) holdwalks the requested seats in venue order and the first problem decides: a taken seat isseat unavailable, a free withheld seat isseat withheld.besttreats withheld seats exactly as taken ones, both when looking for blocks and when countingLandR.availabilitydoes not count withheld free seats.blockis unchanged.["hold_accessible", ts, buyer, party, promo?](partyan integer 1-3, elseinvalid arguments): the seats are the pod's accessible space plus its firstparty - 1companions, from the first pod inACCESSIBLE_PODSorder whose needed seats are all free (withheld does not matter here, this is how the accessible desk sells them). Checks:invalid arguments,limit exceeded, promo errors,no accessible seats. Everything else works likehold, except that these holds last 1200 seconds (config.ACCESSIBLE_HOLD_SECONDS):expires_at = ts + 1200.
3b. Resale marketplace
| Operation | Arguments after ts | Returns | Errors, in order |
|---|---|---|---|
list_resale | seller, ticket_id, ask | listing id L-1, L-2, ... | invalid arguments (ask must be an integer >= 1), ticket not found, not your ticket, not resellable, already listed, price over cap |
buy_resale | buyer, listing_id | {"ticket", "price", "fee", "paid"} | listing not found, listing closed, own listing, limit exceeded |
cancel_listing | seller, listing_id | true | listing not found, not your listing, listing closed |
- Face-value cap.
askmay be at most the ticket's *face value*: the list price of its seat (Ticket.face_cents), whatever the original buyer actually paid after promos.ask == faceis allowed. - A ticket on an accessible space (the first seat of a pod) is
not resellable; companion seats are fine. A ticket can have only one open listing at a time (already listed); a sold or cancelled listing islisting closedand the ticket may be listed again, including by its new owner. buy_resaletransfers the ticket (same id, same seat) to the buyer. The buyer pays the ask plus a marketplace fee of 10% of the ask rounded up to the cent:paid = ask + fee. The buyer's limit applies (held + owned + 1 <= 6); the seller's owned count drops by one.- Listings never expire and listing a ticket does not change anyone's limit count.
Earlier Parts keep their behaviour. Note what 3a changes: holds no longer all last the same time, and whether a seat is available depends on ts.
Examples
[["hold_accessible", 100, "fay", 2],
["hold", 101, "gus", ["ORC-1-4"]],
["hold", 102, "gus", ["ORC-1-2"]],
["availability", 103, "A"],
["hold", 3600, "gus", ["ORC-1-4"]],
["availability", 3600, "A"]]returns
[{"hold": "H-1", "seats": ["ORC-1-2", "ORC-1-3"], "expires_at": 1300},
"error: seat withheld",
"error: seat unavailable",
1023,
{"hold": "H-2", "seats": ["ORC-1-4"], "expires_at": 4200},
1031]Tier A has 1032 seats; 9 of them are in pods. At ts 103 two are held and seven stay withheld (1032 - 2 - 7). Fay's accessible hold lapsed at 1300; at 3600 the pods are open to everyone and only Gus's new hold is taken (1032 - 1).
[["hold", 1, "alice", ["PIT-3-18", "PIT-3-19"], "HARBOR10"],
["checkout", 2, "H-1", "alice"],
["list_resale", 4, "alice", "T-1", 16000],
["list_resale", 5, "alice", "T-1", 14950],
["buy_resale", 6, "bob", "L-1"],
["buyer_summary", 7, "alice"],
["tickets", 9, "bob"],
["buy_resale", 10, "carol", "L-1"]]returns
[{"hold": "H-1", "seats": ["PIT-3-18", "PIT-3-19"], "expires_at": 601},
{"order": "O-1", "tickets": ["T-1", "T-2"], "subtotal": 29900, "discount": 2990, "fees": 700, "total": 27610},
"error: price over cap",
"L-1",
{"ticket": "T-1", "price": 14950, "fee": 1495, "paid": 16445},
{"held": 0, "owned": 1},
[["T-1", "PIT-3-18"]],
"error: listing closed"]Part 4 — The on-sale rush (stretch)
Goal: the load test for tonight (["rush", ts, seed, count, step], already wired to loadgen.run_rush) simulates thousands of visitors through the real dispatcher and has to finish inside the time limit. Make the whole system fast enough without changing any result.
rush plays count visitors, one every step seconds starting at ts; each one does a best request (one in ten with the promo VIP50), sometimes followed by a checkout, a release of their latest hold, or an availability read. It returns {"best_ok", "tickets", "refused": {error code: count}, "open": {tier: seats}}; its numbers are only right if everything above is right. Hidden tests run several thousand visitors per run, ending with ordinary operations that check the final state. Anything that walks all seats, all holds or all tickets on every operation will be too slow. loadgen.py is read-only; don't change how it behaves.
Notes
venue.py,tickets.pyandmodels.pyare known-good;loadgen.pymust not change.- You may use the AI assistant freely; you are expected to verify what it produces.
- Later Parts must not break earlier ones; hidden tests re-check earlier behaviour after later Parts.
- Parts 1-3 is a strong result. Part 4 is a stretch.
Example 1
- Input:
- [["hold",10,"alice",["BAL-1-1..2"],"HARBOR10"],["promo_status",609,"HARBOR10"],["checkout",610,"H-1","alice"],["promo_status",610,"HARBOR10"],["hold",611,"bob",["BAL-1-1..2"]]]
- Output:
- [{"hold":"H-1","seats":["BAL-1-1","BAL-1-2"],"expires_at":610},{"limit":3,"reserved":1,"used":0},"error: hold expired",{"limit":3,"reserved":0,"used":0},{"hold":"H-2","seats":["BAL-1-1","BAL-1-2"],"expires_at":1211}]
- Explanation
- The hold made at 10 lapses at 610: checking out at that second fails, the promo use it reserved is released and the seats can be held again.
Example 2
- Input:
- [["block",1,["MEZ-1-1..44","MEZ-2-1..19","MEZ-2-24..44"]],["best",2,"dana","B",3],["best",3,"erin","B",2]]
- Output:
- [84,{"hold":"H-1","seats":["MEZ-3-21","MEZ-3-22","MEZ-3-23"],"expires_at":602},{"hold":"H-2","seats":["MEZ-2-22","MEZ-2-23"],"expires_at":603}]
- Explanation
- Three seats anywhere in the four free seats of MEZ-2 would leave a single orphan seat, so Dana is placed in row 3; two seats at 22-23 leave 2 and 0, which is fine.
Example 3
- Input:
- [["hold",1,"alice",["PIT-3-18","PIT-3-19"],"HARBOR10"],["checkout",2,"H-1","alice"],["list_resale",4,"alice","T-1",16000],["list_resale",5,"alice","T-1",14950],["buy_resale",6,"bob","L-1"]]
- Output:
- [{"hold":"H-1","seats":["PIT-3-18","PIT-3-19"],"expires_at":601},{"order":"O-1","tickets":["T-1","T-2"],"subtotal":29900,"discount":2990,"fees":700,"total":27610},"error: price over cap","L-1",{"ticket":"T-1","price":14950,"fee":1495,"paid":16445}]
- Explanation
- The resale cap is the seat's list price (14950), not what Alice paid after her promo; Bob pays the ask plus a 10% marketplace fee.
Guidelines
- Python 3 standard library only
- Money is integer cents; there is no floating-point money anywhere
- Timestamps are integer seconds and never decrease from one operation to the next
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised
- Part 4 inputs simulate thousands of visitors with tens of thousands of internal operations in a single run