# Inventory And Holds

ATM's private database enforces scarce inventory. Public PDS records, AppViews,
RSVPs and relay ordering never reserve seats.

## Capacity model

Every ticket type has its own private pool. Its optional `quantityTotal` cap is
unlimited when omitted. `setEventCapacity` adds, changes or clears one shared
event pool across all event types: provide exactly one of `capacity` or
`unlimited: true`. Per-type caps may sum above the event cap; effective
availability is the minimum remaining capacity across the type's pools.
`availableQuantity` is omitted when no finite pool bounds the type. Omission
means unlimited, not sold out. Event capacity is not a public record field.
Capacity cannot be lowered below already sold plus held quantities.

The implementation uses `ticket_types`, `ticket_capacity_groups`,
`ticket_type_capacity_links`, `ticket_holds`, `ticket_hold_items`,
`issued_tickets`, `ticket_events`, `ticket_check_in_lists`,
`ticket_check_ins`, `ticket_scanner_grants` and private lifecycle audit/outboxes.
Pool rows are implementation state, not independently editable app catalog.

## Reservation and settlement

```text
transaction
  lock event/type/pool authority in deterministic order
  re-read pool memberships and validate permissions, sale windows and codes
  reserve quantity atomically in every applicable finite pool
  persist immutable hold, pricing, buyer and delivery bindings
commit
  return ATM checkout URL and private app status capability
```

Checkout quantity is fixed by the hold. Reuse the same idempotency key and
request after an uncertain response. Do not create a replacement purchase while
the original outcome is unknown. `CheckoutPayerChanged` requires reconciliation
of the final buyer; it does not authorize another sale.

Paid reservations use a 30-minute base checkout window, with processor-session
recovery/replacement bounded by an immutable reservation deadline. Use ATM's
returned `expiresAt` and current lifecycle state instead of a hard-coded client
timer. A redirect, cancellation page, timeout or provisional expiry alone does
not prove the processor session is dead. Capacity release must follow ATM's
guarded cancellation/expiry reconciliation so a payable session cannot outlive
its reservation.

Settlement promotes the held quantity to issued tickets exactly once. Refunds,
voids and event cancellation revoke admission and preserve audit history.
Durable jobs/outboxes recover app notifications, waitlist offers and RSVP
cleanup independently of inventory transactions.

## Free claims and release

`claimFreeTicket` transactionally issues zero-price types without checkout or a
payment row. It requires app service-auth, `buyerDid`, a route-scoped
`buyerAssertionJwt`, an item array, and a valid private `customerEmail` for
fresh delivery. Wire omission of email exists only for exact replay of legacy
completed no-email claims.

`releaseFreeTicket` requires a verified buyer assertion and caller-stable
`idempotencyKey`. It voids only that buyer's active free-claim tickets, releases
capacity, revokes passes and resumes waitlist/RSVP cleanup. Checked-in,
transferred and paid tickets are not released this way. A no-op succeeds with
`releasedCount: 0`. Reuse the exact release key after an uncertain outcome so
newly claimed tickets cannot be consumed by the retry. Existing-ticket lifecycle
and scanner authority do not depend solely on the current module toggle.

## Waitlists and purchase limits

`joinWaitlist` queues a sold-out ticket type without consuming capacity.
When capacity returns, ATM reserves an offer hold and emits
`ticket.waitlist.offered`; `claimWaitlistOffer` uses that hold and never
auto-charges a buyer. If supplying `buyerDid`, also supply its verified
assertion; guest flows use the private email identity.

Private event purchase policy distinguishes multiple tickets, exactly one per
order, and one per person across orders. Respect ATM's quantity/identity errors;
an app-local counter is not an inventory or per-person authority.
