Events can now set an optional walk-in price (events.walk_in_price): null falls back to the ticket price, 0 is a free walk-in. It is set in the Create/Edit Event modal (EN/ES) and carried through create, update and duplicate, but it is internal: public event responses and the attendee-facing ticket/dashboard endpoints drop it, and only admin, organizer and staff callers receive it. The door screen no longer trusts the amount the client sends. It sends a quantity and the server prices the charge from the event record: walk-ins pay the walk-in price, existing tickets the ticket price. A typed amount is only honoured with amountOverride from admin/organizer and is written to audit_logs in the same transaction. The charged amount stays snapshotted on the payment row. POS is a new door tender for the physical card terminal. Staff pick POS, see the amount to key in, charge the card, then confirm with "Mark as paid"; it is then recorded like any other door payment (provider and method 'pos'). It can be switched off globally or per event via payment options, and shows up in the payments filters, bookings, revenue summaries and door takings. PosChargePanel is where an automatic terminal push would go later; nothing calls a terminal today. tickets.booking_source (online | walk_in | admin) records how a booking was made. The migration backfills walk-ins from the door screen's idempotency records; everything else stays online. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
45 lines
2.0 KiB
TypeScript
45 lines
2.0 KiB
TypeScript
// Walk-in (door) pricing.
|
|
//
|
|
// An event may set a separate price for people who buy at the door. It is an
|
|
// internal number: staff charge it on the door screen, but it never appears on
|
|
// the public event page, listings or JSON-LD, so every public serializer must
|
|
// drop it (see omitWalkInPrice).
|
|
//
|
|
// walkInPrice null -> not set, walk-ins pay the regular ticket price
|
|
// walkInPrice 0 -> free walk-in (a real value, distinct from null)
|
|
// walkInPrice n -> walk-ins pay n, in the event's currency
|
|
|
|
export type WalkInPriceSource = 'walk_in' | 'ticket';
|
|
|
|
/** Roles that may see an event's walk-in price: event managers and door staff. */
|
|
export const WALK_IN_PRICE_ROLES = ['admin', 'organizer', 'staff'] as const;
|
|
|
|
export function canSeeWalkInPrice(role: string | null | undefined): boolean {
|
|
return !!role && (WALK_IN_PRICE_ROLES as readonly string[]).includes(role);
|
|
}
|
|
|
|
/** Postgres decimals arrive as strings; null/undefined/garbage stay null. */
|
|
export function parseWalkInPrice(value: unknown): number | null {
|
|
if (value === null || value === undefined || value === '') return null;
|
|
const n = typeof value === 'string' ? parseFloat(value) : Number(value);
|
|
return Number.isFinite(n) ? n : null;
|
|
}
|
|
|
|
/** The per-ticket price a walk-in is charged, and where it came from. */
|
|
export function resolveWalkInPrice(event: { price: unknown; walkInPrice?: unknown }): {
|
|
unitPrice: number;
|
|
source: WalkInPriceSource;
|
|
} {
|
|
const walkIn = parseWalkInPrice(event.walkInPrice);
|
|
if (walkIn !== null) return { unitPrice: walkIn, source: 'walk_in' };
|
|
const price = typeof event.price === 'string' ? parseFloat(event.price) : Number(event.price);
|
|
return { unitPrice: Number.isFinite(price) ? price : 0, source: 'ticket' };
|
|
}
|
|
|
|
/** Copy of an event row without its walk-in price, for anything a non-staff caller can read. */
|
|
export function omitWalkInPrice<T extends Record<string, any> | null | undefined>(event: T): T {
|
|
if (!event) return event;
|
|
const { walkInPrice: _omitted, ...rest } = event as Record<string, any>;
|
|
return rest as T;
|
|
}
|