Rebuild the Scanner page into a unified door check-in screen.
Most attendees arrive without their QR open, and taking money at the door meant leaving the scanner for the event dashboard, where the Add Ticket modal demanded an email and recorded no payment method. The screen now leads with manual name search, keeps the camera one tap away behind a fullscreen overlay, and creates and charges walk-ins inline. Check-in and payment are one action: anything done here is born confirmed, paid (or comp) and checked in through a single endpoint, POST /api/events/:eventId/door-checkin. There are no confirm dialogs anywhere, because they stall the queue; a ten-second Undo replaces them, reversing exactly what the action changed via the undo state recorded alongside its idempotency key. Writes fire in the background with retries, so venue wifi never blocks the person at the door, and a capacity limit only warns, since staff at the door are the authority. Every write carries a client-generated idempotency key, inserted in the same transaction as the writes it guards, so a double tap or a retry after a timeout cannot produce a second ticket, payment or check-in. Search runs entirely in memory over one preloaded list: names are matched accent- and case-insensitively in both directions, per word, prefix before substring, with a mostly-numeric query searching phone digits so two people with the same name can be told apart. Door money is recorded as payments.source 'door' plus payments.method (cash, bitcoin, transfer or guest) while provider keeps its existing value, so capacity counting, the stale-booking sweeps and the admin payment lists are unaffected and revenue can still be split pre-sale versus door. Bitcoin records the payment as made, on the same trust model as cash, with no invoice generated; lib/doorPayments.ts is where a real Lightning flow slots in later. Also fixes the SQLite tickets DDL, which still created the pre-split attendee_name column with NOT NULL email and phone. Only fresh databases were affected -- existing ones were relaxed by later ALTERs -- but on those, door walk-ins (and any other ticket) could not be inserted at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a0161a67d2
commit
e296e80e48
@@ -133,17 +133,25 @@ async function migrate() {
|
||||
`);
|
||||
|
||||
await (db as any).run(sql`
|
||||
-- Matches db/schema.ts. The legacy attendee_name / NOT NULL email+phone
|
||||
-- shape only survives in databases created before the split into
|
||||
-- first/last name, where the ALTERs below relaxed it; a fresh database
|
||||
-- must not recreate constraints the app no longer satisfies (door
|
||||
-- walk-ins have neither an email nor a phone).
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id),
|
||||
event_id TEXT NOT NULL REFERENCES events(id),
|
||||
attendee_name TEXT NOT NULL,
|
||||
attendee_email TEXT NOT NULL,
|
||||
attendee_phone TEXT NOT NULL,
|
||||
attendee_first_name TEXT NOT NULL,
|
||||
attendee_last_name TEXT,
|
||||
attendee_email TEXT,
|
||||
attendee_phone TEXT,
|
||||
attendee_ruc TEXT,
|
||||
preferred_language TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
checkin_at TEXT,
|
||||
qr_code TEXT,
|
||||
admin_note TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
@@ -259,6 +267,25 @@ async function migrate() {
|
||||
try {
|
||||
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN lnbits_amount_sats INTEGER`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
// Door check-in screen: split pre-sale vs door revenue and record the tender
|
||||
try {
|
||||
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN source TEXT NOT NULL DEFAULT 'presale'`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
try {
|
||||
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN method TEXT`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
|
||||
// Idempotency records for door check-in actions (retries / double taps)
|
||||
await (db as any).run(sql`
|
||||
CREATE TABLE IF NOT EXISTS idempotency_keys (
|
||||
key TEXT PRIMARY KEY,
|
||||
scope TEXT NOT NULL,
|
||||
result TEXT NOT NULL,
|
||||
undo_state TEXT,
|
||||
undone_at TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// Invoices table
|
||||
await (db as any).run(sql`
|
||||
@@ -836,6 +863,25 @@ async function migrate() {
|
||||
try {
|
||||
await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN lnbits_amount_sats INTEGER`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
// Door check-in screen: split pre-sale vs door revenue and record the tender
|
||||
try {
|
||||
await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN source VARCHAR(20) NOT NULL DEFAULT 'presale'`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
try {
|
||||
await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN method VARCHAR(20)`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
|
||||
// Idempotency records for door check-in actions (retries / double taps)
|
||||
await (db as any).execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS idempotency_keys (
|
||||
key VARCHAR(128) PRIMARY KEY,
|
||||
scope VARCHAR(64) NOT NULL,
|
||||
result TEXT NOT NULL,
|
||||
undo_state TEXT,
|
||||
undone_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// Invoices table
|
||||
await (db as any).execute(sql`
|
||||
@@ -1203,6 +1249,8 @@ async function migrate() {
|
||||
`CREATE INDEX IF NOT EXISTS tickets_status_idx ON tickets(status)`,
|
||||
`CREATE INDEX IF NOT EXISTS payments_ticket_id_idx ON payments(ticket_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS payments_status_idx ON payments(status)`,
|
||||
`CREATE INDEX IF NOT EXISTS payments_source_idx ON payments(source)`,
|
||||
`CREATE INDEX IF NOT EXISTS idempotency_keys_created_at_idx ON idempotency_keys(created_at)`,
|
||||
`CREATE INDEX IF NOT EXISTS email_logs_event_id_idx ON email_logs(event_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS magic_link_tokens_token_idx ON magic_link_tokens(token)`,
|
||||
`CREATE INDEX IF NOT EXISTS auth_sessions_user_id_idx ON auth_sessions(user_id)`,
|
||||
|
||||
Reference in New Issue
Block a user