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>
826 lines
40 KiB
TypeScript
826 lines
40 KiB
TypeScript
import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core';
|
|
import { pgTable, uuid, varchar, text as pgText, timestamp, decimal, integer as pgInteger, boolean as pgBoolean } from 'drizzle-orm/pg-core';
|
|
|
|
// Type to determine which schema to use
|
|
const dbType = process.env.DB_TYPE || 'sqlite';
|
|
|
|
// ==================== SQLite Schema ====================
|
|
export const sqliteUsers = sqliteTable('users', {
|
|
id: text('id').primaryKey(),
|
|
email: text('email').notNull().unique(),
|
|
password: text('password'), // Nullable for unclaimed accounts
|
|
name: text('name').notNull(),
|
|
phone: text('phone'),
|
|
role: text('role', { enum: ['admin', 'organizer', 'staff', 'marketing', 'user'] }).notNull().default('user'),
|
|
languagePreference: text('language_preference'),
|
|
// New fields for progressive accounts and OAuth
|
|
isClaimed: integer('is_claimed', { mode: 'boolean' }).notNull().default(true),
|
|
googleId: text('google_id'),
|
|
rucNumber: text('ruc_number'),
|
|
accountStatus: text('account_status', { enum: ['active', 'unclaimed', 'suspended'] }).notNull().default('active'),
|
|
// Incremented to invalidate previously issued JWTs (logout-everywhere, password change/reset)
|
|
tokenVersion: integer('token_version').notNull().default(0),
|
|
// Better Auth core + admin plugin fields (auth-schema.ts maps the same columns)
|
|
emailVerified: integer('email_verified', { mode: 'boolean' }).notNull().default(false),
|
|
image: text('image'),
|
|
banned: integer('banned', { mode: 'boolean' }).notNull().default(false),
|
|
banReason: text('ban_reason'),
|
|
banExpires: integer('ban_expires', { mode: 'timestamp_ms' }),
|
|
createdAt: text('created_at').notNull(),
|
|
updatedAt: text('updated_at').notNull(),
|
|
});
|
|
|
|
// Magic link tokens for passwordless login
|
|
export const sqliteMagicLinkTokens = sqliteTable('magic_link_tokens', {
|
|
id: text('id').primaryKey(),
|
|
userId: text('user_id').notNull().references(() => sqliteUsers.id),
|
|
token: text('token').notNull().unique(),
|
|
type: text('type', { enum: ['login', 'reset_password', 'claim_account', 'email_verification'] }).notNull(),
|
|
expiresAt: text('expires_at').notNull(),
|
|
usedAt: text('used_at'),
|
|
createdAt: text('created_at').notNull(),
|
|
});
|
|
|
|
// User sessions for session management
|
|
export const sqliteUserSessions = sqliteTable('user_sessions', {
|
|
id: text('id').primaryKey(),
|
|
userId: text('user_id').notNull().references(() => sqliteUsers.id),
|
|
token: text('token').notNull().unique(),
|
|
userAgent: text('user_agent'),
|
|
ipAddress: text('ip_address'),
|
|
lastActiveAt: text('last_active_at').notNull(),
|
|
expiresAt: text('expires_at').notNull(),
|
|
createdAt: text('created_at').notNull(),
|
|
});
|
|
|
|
// Invoices table
|
|
export const sqliteInvoices = sqliteTable('invoices', {
|
|
id: text('id').primaryKey(),
|
|
paymentId: text('payment_id').notNull().references(() => sqlitePayments.id),
|
|
userId: text('user_id').notNull().references(() => sqliteUsers.id),
|
|
invoiceNumber: text('invoice_number').notNull().unique(),
|
|
rucNumber: text('ruc_number'),
|
|
legalName: text('legal_name'),
|
|
amount: real('amount').notNull(),
|
|
currency: text('currency').notNull().default('PYG'),
|
|
pdfUrl: text('pdf_url'),
|
|
status: text('status', { enum: ['generated', 'voided'] }).notNull().default('generated'),
|
|
createdAt: text('created_at').notNull(),
|
|
});
|
|
|
|
export const sqliteEvents = sqliteTable('events', {
|
|
id: text('id').primaryKey(),
|
|
slug: text('slug').unique(),
|
|
title: text('title').notNull(),
|
|
titleEs: text('title_es'),
|
|
description: text('description').notNull(),
|
|
descriptionEs: text('description_es'),
|
|
shortDescription: text('short_description'),
|
|
shortDescriptionEs: text('short_description_es'),
|
|
startDatetime: text('start_datetime').notNull(),
|
|
endDatetime: text('end_datetime'),
|
|
location: text('location').notNull(),
|
|
locationUrl: text('location_url'),
|
|
price: real('price').notNull().default(0),
|
|
currency: text('currency').notNull().default('PYG'),
|
|
capacity: integer('capacity').notNull().default(50),
|
|
status: text('status', { enum: ['draft', 'published', 'unlisted', 'cancelled', 'completed', 'archived'] }).notNull().default('draft'),
|
|
bannerUrl: text('banner_url'),
|
|
externalBookingEnabled: integer('external_booking_enabled', { mode: 'boolean' }).notNull().default(false),
|
|
externalBookingUrl: text('external_booking_url'),
|
|
createdAt: text('created_at').notNull(),
|
|
updatedAt: text('updated_at').notNull(),
|
|
});
|
|
|
|
// Historical slugs that still resolve (and redirect) to an event's canonical slug
|
|
export const sqliteEventSlugAliases = sqliteTable('event_slug_aliases', {
|
|
slug: text('slug').primaryKey(),
|
|
eventId: text('event_id').notNull().references(() => sqliteEvents.id),
|
|
createdAt: text('created_at').notNull(),
|
|
});
|
|
|
|
export const sqliteTickets = sqliteTable('tickets', {
|
|
id: text('id').primaryKey(),
|
|
bookingId: text('booking_id'), // Groups multiple tickets from same booking
|
|
userId: text('user_id').notNull().references(() => sqliteUsers.id),
|
|
eventId: text('event_id').notNull().references(() => sqliteEvents.id),
|
|
attendeeFirstName: text('attendee_first_name').notNull(),
|
|
attendeeLastName: text('attendee_last_name'),
|
|
attendeeEmail: text('attendee_email'),
|
|
attendeePhone: text('attendee_phone'),
|
|
attendeeRuc: text('attendee_ruc'), // Paraguayan tax ID for invoicing
|
|
preferredLanguage: text('preferred_language'),
|
|
status: text('status', { enum: ['pending', 'confirmed', 'cancelled', 'checked_in', 'on_hold'] }).notNull().default('pending'),
|
|
checkinAt: text('checkin_at'),
|
|
checkedInByAdminId: text('checked_in_by_admin_id').references(() => sqliteUsers.id), // Who performed the check-in
|
|
qrCode: text('qr_code'),
|
|
adminNote: text('admin_note'),
|
|
isGuest: integer('is_guest', { mode: 'boolean' }).notNull().default(false),
|
|
// Paid: revenue counted; Unpaid: balance due (collect at door); Comp: free guest, no revenue
|
|
paymentStatus: text('payment_status', { enum: ['paid', 'unpaid', 'comp'] }).notNull().default('unpaid'),
|
|
createdAt: text('created_at').notNull(),
|
|
});
|
|
|
|
export const sqlitePayments = sqliteTable('payments', {
|
|
id: text('id').primaryKey(),
|
|
ticketId: text('ticket_id').notNull().references(() => sqliteTickets.id),
|
|
provider: text('provider', { enum: ['bancard', 'lightning', 'cash', 'bank_transfer', 'tpago'] }).notNull(),
|
|
amount: real('amount').notNull(),
|
|
currency: text('currency').notNull().default('PYG'),
|
|
status: text('status', { enum: ['pending', 'pending_approval', 'paid', 'refunded', 'failed', 'cancelled', 'on_hold'] }).notNull().default('pending'),
|
|
reference: text('reference'),
|
|
lnbitsInvoice: text('lnbits_invoice'), // BOLT11 string, stored so an unpaid invoice can be redisplayed without a new LNbits call
|
|
lnbitsExpiresAt: text('lnbits_expires_at'), // When lnbitsInvoice expires
|
|
lnbitsAmountSats: integer('lnbits_amount_sats'), // Sats amount for display (from LNbits' fiat conversion)
|
|
userMarkedPaidAt: text('user_marked_paid_at'), // When user clicked "I Have Paid"
|
|
payerName: text('payer_name'), // Name of payer if different from attendee
|
|
paidAt: text('paid_at'),
|
|
paidByAdminId: text('paid_by_admin_id'),
|
|
adminNote: text('admin_note'), // Internal admin notes
|
|
reminderSentAt: text('reminder_sent_at'), // When payment reminder email was sent
|
|
// Where the money was taken: 'presale' (online/admin, the default) or 'door'
|
|
// (recorded by staff on the door check-in screen). Splits pre-sale vs door revenue.
|
|
source: text('source', { enum: ['presale', 'door'] }).notNull().default('presale'),
|
|
// Door tender used, for the end-of-night cash-up. Null for pre-sale payments.
|
|
// 'guest' is a zero-amount comp entry and carries no revenue.
|
|
method: text('method', { enum: ['cash', 'bitcoin', 'transfer', 'guest'] }),
|
|
createdAt: text('created_at').notNull(),
|
|
updatedAt: text('updated_at').notNull(),
|
|
});
|
|
|
|
// Idempotency records for door check-in actions.
|
|
//
|
|
// The door screen fires check-ins / walk-in creations optimistically and retries
|
|
// on flaky venue wifi, so every action carries a client-generated key. The first
|
|
// request stores its response here; replays return that stored response instead
|
|
// of creating a second ticket, payment or check-in. `undoState` holds exactly
|
|
// what the action changed so the 10-second Undo can reverse it precisely.
|
|
export const sqliteIdempotencyKeys = sqliteTable('idempotency_keys', {
|
|
key: text('key').primaryKey(),
|
|
scope: text('scope').notNull(),
|
|
result: text('result').notNull(), // JSON response body of the original request
|
|
undoState: text('undo_state'), // JSON describing how to reverse the action
|
|
undoneAt: text('undone_at'),
|
|
createdAt: text('created_at').notNull(),
|
|
});
|
|
|
|
// Payment Options Configuration Table (global settings)
|
|
export const sqlitePaymentOptions = sqliteTable('payment_options', {
|
|
id: text('id').primaryKey(),
|
|
// TPago configuration
|
|
tpagoEnabled: integer('tpago_enabled', { mode: 'boolean' }).notNull().default(false),
|
|
tpagoLink: text('tpago_link'),
|
|
tpagoLink2: text('tpago_link_2'),
|
|
tpagoLink3: text('tpago_link_3'),
|
|
tpagoLink4: text('tpago_link_4'),
|
|
tpagoLink5: text('tpago_link_5'),
|
|
tpagoInstructions: text('tpago_instructions'),
|
|
tpagoInstructionsEs: text('tpago_instructions_es'),
|
|
// Bank Transfer configuration
|
|
bankTransferEnabled: integer('bank_transfer_enabled', { mode: 'boolean' }).notNull().default(false),
|
|
bankName: text('bank_name'),
|
|
bankAccountHolder: text('bank_account_holder'),
|
|
bankAccountNumber: text('bank_account_number'),
|
|
bankAlias: text('bank_alias'),
|
|
bankPhone: text('bank_phone'),
|
|
bankNotes: text('bank_notes'),
|
|
bankNotesEs: text('bank_notes_es'),
|
|
// Lightning configuration
|
|
lightningEnabled: integer('lightning_enabled', { mode: 'boolean' }).notNull().default(true),
|
|
// Cash configuration
|
|
cashEnabled: integer('cash_enabled', { mode: 'boolean' }).notNull().default(true),
|
|
cashInstructions: text('cash_instructions'),
|
|
cashInstructionsEs: text('cash_instructions_es'),
|
|
// Booking settings
|
|
allowDuplicateBookings: integer('allow_duplicate_bookings', { mode: 'boolean' }).notNull().default(false),
|
|
// Metadata
|
|
updatedAt: text('updated_at').notNull(),
|
|
updatedBy: text('updated_by').references(() => sqliteUsers.id),
|
|
});
|
|
|
|
// Event-specific payment overrides
|
|
export const sqliteEventPaymentOverrides = sqliteTable('event_payment_overrides', {
|
|
id: text('id').primaryKey(),
|
|
eventId: text('event_id').notNull().references(() => sqliteEvents.id),
|
|
// Override flags (null means use global)
|
|
tpagoEnabled: integer('tpago_enabled', { mode: 'boolean' }),
|
|
tpagoLink: text('tpago_link'),
|
|
tpagoLink2: text('tpago_link_2'),
|
|
tpagoLink3: text('tpago_link_3'),
|
|
tpagoLink4: text('tpago_link_4'),
|
|
tpagoLink5: text('tpago_link_5'),
|
|
tpagoInstructions: text('tpago_instructions'),
|
|
tpagoInstructionsEs: text('tpago_instructions_es'),
|
|
bankTransferEnabled: integer('bank_transfer_enabled', { mode: 'boolean' }),
|
|
bankName: text('bank_name'),
|
|
bankAccountHolder: text('bank_account_holder'),
|
|
bankAccountNumber: text('bank_account_number'),
|
|
bankAlias: text('bank_alias'),
|
|
bankPhone: text('bank_phone'),
|
|
bankNotes: text('bank_notes'),
|
|
bankNotesEs: text('bank_notes_es'),
|
|
lightningEnabled: integer('lightning_enabled', { mode: 'boolean' }),
|
|
cashEnabled: integer('cash_enabled', { mode: 'boolean' }),
|
|
cashInstructions: text('cash_instructions'),
|
|
cashInstructionsEs: text('cash_instructions_es'),
|
|
// Metadata
|
|
createdAt: text('created_at').notNull(),
|
|
updatedAt: text('updated_at').notNull(),
|
|
});
|
|
|
|
export const sqliteContacts = sqliteTable('contacts', {
|
|
id: text('id').primaryKey(),
|
|
name: text('name').notNull(),
|
|
email: text('email').notNull(),
|
|
message: text('message').notNull(),
|
|
status: text('status', { enum: ['new', 'read', 'replied'] }).notNull().default('new'),
|
|
createdAt: text('created_at').notNull(),
|
|
});
|
|
|
|
export const sqliteEmailSubscribers = sqliteTable('email_subscribers', {
|
|
id: text('id').primaryKey(),
|
|
email: text('email').notNull().unique(),
|
|
name: text('name'),
|
|
status: text('status', { enum: ['active', 'unsubscribed'] }).notNull().default('active'),
|
|
createdAt: text('created_at').notNull(),
|
|
});
|
|
|
|
export const sqliteMedia = sqliteTable('media', {
|
|
id: text('id').primaryKey(),
|
|
fileUrl: text('file_url').notNull(),
|
|
type: text('type', { enum: ['image', 'video', 'document'] }).notNull(),
|
|
relatedId: text('related_id'),
|
|
relatedType: text('related_type'),
|
|
createdAt: text('created_at').notNull(),
|
|
});
|
|
|
|
export const sqliteAuditLogs = sqliteTable('audit_logs', {
|
|
id: text('id').primaryKey(),
|
|
userId: text('user_id').references(() => sqliteUsers.id),
|
|
action: text('action').notNull(),
|
|
target: text('target'),
|
|
targetId: text('target_id'),
|
|
details: text('details'),
|
|
timestamp: text('timestamp').notNull(),
|
|
});
|
|
|
|
export const sqliteEmailTemplates = sqliteTable('email_templates', {
|
|
id: text('id').primaryKey(),
|
|
name: text('name').notNull().unique(),
|
|
slug: text('slug').notNull().unique(),
|
|
subject: text('subject').notNull(),
|
|
subjectEs: text('subject_es'),
|
|
bodyHtml: text('body_html').notNull(),
|
|
bodyHtmlEs: text('body_html_es'),
|
|
bodyText: text('body_text'),
|
|
bodyTextEs: text('body_text_es'),
|
|
description: text('description'),
|
|
variables: text('variables'), // JSON array of available variables
|
|
isSystem: integer('is_system', { mode: 'boolean' }).notNull().default(false),
|
|
isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true),
|
|
createdAt: text('created_at').notNull(),
|
|
updatedAt: text('updated_at').notNull(),
|
|
});
|
|
|
|
export const sqliteEmailLogs = sqliteTable('email_logs', {
|
|
id: text('id').primaryKey(),
|
|
templateId: text('template_id').references(() => sqliteEmailTemplates.id),
|
|
eventId: text('event_id').references(() => sqliteEvents.id),
|
|
recipientEmail: text('recipient_email').notNull(),
|
|
recipientName: text('recipient_name'),
|
|
subject: text('subject').notNull(),
|
|
bodyHtml: text('body_html'),
|
|
status: text('status', { enum: ['pending', 'sent', 'failed', 'bounced'] }).notNull().default('pending'),
|
|
errorMessage: text('error_message'),
|
|
sentAt: text('sent_at'),
|
|
sentBy: text('sent_by').references(() => sqliteUsers.id),
|
|
createdAt: text('created_at').notNull(),
|
|
resendAttempts: integer('resend_attempts').notNull().default(0),
|
|
lastResentAt: text('last_resent_at'),
|
|
});
|
|
|
|
export const sqliteEmailSettings = sqliteTable('email_settings', {
|
|
id: text('id').primaryKey(),
|
|
key: text('key').notNull().unique(),
|
|
value: text('value').notNull(),
|
|
updatedAt: text('updated_at').notNull(),
|
|
});
|
|
|
|
// Durable email queue. Jobs survive process restarts; a startup recovery step
|
|
// resets any 'processing' rows back to 'pending'.
|
|
export const sqliteEmailQueue = sqliteTable('email_queue', {
|
|
id: text('id').primaryKey(),
|
|
params: text('params').notNull(), // JSON-encoded TemplateEmailJobParams
|
|
status: text('status', { enum: ['pending', 'processing', 'sent', 'failed'] }).notNull().default('pending'),
|
|
attempts: integer('attempts').notNull().default(0),
|
|
lastError: text('last_error'),
|
|
createdAt: text('created_at').notNull(),
|
|
processedAt: text('processed_at'),
|
|
});
|
|
|
|
// Legal Pages table for admin-editable legal content
|
|
export const sqliteLegalPages = sqliteTable('legal_pages', {
|
|
id: text('id').primaryKey(),
|
|
slug: text('slug').notNull().unique(),
|
|
title: text('title').notNull(), // English title
|
|
titleEs: text('title_es'), // Spanish title
|
|
contentText: text('content_text').notNull(), // Plain text edited by admin (English)
|
|
contentTextEs: text('content_text_es'), // Plain text edited by admin (Spanish)
|
|
contentMarkdown: text('content_markdown').notNull(), // Generated markdown for public display (English)
|
|
contentMarkdownEs: text('content_markdown_es'), // Generated markdown for public display (Spanish)
|
|
updatedAt: text('updated_at').notNull(),
|
|
updatedBy: text('updated_by').references(() => sqliteUsers.id),
|
|
createdAt: text('created_at').notNull(),
|
|
});
|
|
|
|
// FAQ questions table (admin-managed, shown on /faq and optionally on homepage)
|
|
export const sqliteFaqQuestions = sqliteTable('faq_questions', {
|
|
id: text('id').primaryKey(),
|
|
question: text('question').notNull(),
|
|
questionEs: text('question_es'),
|
|
answer: text('answer').notNull(),
|
|
answerEs: text('answer_es'),
|
|
enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
|
|
showOnHomepage: integer('show_on_homepage', { mode: 'boolean' }).notNull().default(false),
|
|
rank: integer('rank').notNull().default(0),
|
|
createdAt: text('created_at').notNull(),
|
|
updatedAt: text('updated_at').notNull(),
|
|
});
|
|
|
|
// Legal Settings table for legal page placeholder values
|
|
export const sqliteLegalSettings = sqliteTable('legal_settings', {
|
|
id: text('id').primaryKey(),
|
|
companyName: text('company_name'),
|
|
legalEntityName: text('legal_entity_name'),
|
|
rucNumber: text('ruc_number'),
|
|
companyAddress: text('company_address'),
|
|
companyCity: text('company_city'),
|
|
companyCountry: text('company_country'),
|
|
supportEmail: text('support_email'),
|
|
legalEmail: text('legal_email'),
|
|
governingLaw: text('governing_law'),
|
|
jurisdictionCity: text('jurisdiction_city'),
|
|
updatedAt: text('updated_at').notNull(),
|
|
updatedBy: text('updated_by').references(() => sqliteUsers.id),
|
|
});
|
|
|
|
// Site Settings table for global website configuration
|
|
export const sqliteSiteSettings = sqliteTable('site_settings', {
|
|
id: text('id').primaryKey(),
|
|
// Timezone configuration
|
|
timezone: text('timezone').notNull().default('America/Asuncion'),
|
|
// Site info
|
|
siteName: text('site_name').notNull().default('Spanglish'),
|
|
siteDescription: text('site_description'),
|
|
siteDescriptionEs: text('site_description_es'),
|
|
// Contact info
|
|
contactEmail: text('contact_email'),
|
|
contactPhone: text('contact_phone'),
|
|
// Social links (can also be stored here as fallback)
|
|
facebookUrl: text('facebook_url'),
|
|
instagramUrl: text('instagram_url'),
|
|
twitterUrl: text('twitter_url'),
|
|
linkedinUrl: text('linkedin_url'),
|
|
// Featured event - manually promoted event shown on homepage/linktree
|
|
featuredEventId: text('featured_event_id').references(() => sqliteEvents.id),
|
|
// Other settings
|
|
maintenanceMode: integer('maintenance_mode', { mode: 'boolean' }).notNull().default(false),
|
|
maintenanceMessage: text('maintenance_message'),
|
|
maintenanceMessageEs: text('maintenance_message_es'),
|
|
// Metadata
|
|
updatedAt: text('updated_at').notNull(),
|
|
updatedBy: text('updated_by').references(() => sqliteUsers.id),
|
|
});
|
|
|
|
// ==================== PostgreSQL Schema ====================
|
|
export const pgUsers = pgTable('users', {
|
|
id: uuid('id').primaryKey(),
|
|
email: varchar('email', { length: 255 }).notNull().unique(),
|
|
password: varchar('password', { length: 255 }), // Nullable for unclaimed accounts
|
|
name: varchar('name', { length: 255 }).notNull(),
|
|
phone: varchar('phone', { length: 50 }),
|
|
role: varchar('role', { length: 20 }).notNull().default('user'),
|
|
languagePreference: varchar('language_preference', { length: 10 }),
|
|
// New fields for progressive accounts and OAuth
|
|
isClaimed: pgInteger('is_claimed').notNull().default(1),
|
|
googleId: varchar('google_id', { length: 255 }),
|
|
rucNumber: varchar('ruc_number', { length: 15 }),
|
|
accountStatus: varchar('account_status', { length: 20 }).notNull().default('active'),
|
|
// Incremented to invalidate previously issued JWTs (logout-everywhere, password change/reset)
|
|
tokenVersion: pgInteger('token_version').notNull().default(0),
|
|
// Better Auth core + admin plugin fields (auth-schema.ts maps the same columns)
|
|
emailVerified: pgBoolean('email_verified').notNull().default(false),
|
|
image: pgText('image'),
|
|
banned: pgBoolean('banned').notNull().default(false),
|
|
banReason: pgText('ban_reason'),
|
|
banExpires: timestamp('ban_expires'),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
updatedAt: timestamp('updated_at').notNull(),
|
|
});
|
|
|
|
// Magic link tokens for passwordless login
|
|
export const pgMagicLinkTokens = pgTable('magic_link_tokens', {
|
|
id: uuid('id').primaryKey(),
|
|
userId: uuid('user_id').notNull().references(() => pgUsers.id),
|
|
token: varchar('token', { length: 255 }).notNull().unique(),
|
|
type: varchar('type', { length: 30 }).notNull(),
|
|
expiresAt: timestamp('expires_at').notNull(),
|
|
usedAt: timestamp('used_at'),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
});
|
|
|
|
// User sessions for session management
|
|
export const pgUserSessions = pgTable('user_sessions', {
|
|
id: uuid('id').primaryKey(),
|
|
userId: uuid('user_id').notNull().references(() => pgUsers.id),
|
|
token: varchar('token', { length: 500 }).notNull().unique(),
|
|
userAgent: pgText('user_agent'),
|
|
ipAddress: varchar('ip_address', { length: 45 }),
|
|
lastActiveAt: timestamp('last_active_at').notNull(),
|
|
expiresAt: timestamp('expires_at').notNull(),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
});
|
|
|
|
// Invoices table
|
|
export const pgInvoices = pgTable('invoices', {
|
|
id: uuid('id').primaryKey(),
|
|
paymentId: uuid('payment_id').notNull().references(() => pgPayments.id),
|
|
userId: uuid('user_id').notNull().references(() => pgUsers.id),
|
|
invoiceNumber: varchar('invoice_number', { length: 50 }).notNull().unique(),
|
|
rucNumber: varchar('ruc_number', { length: 15 }),
|
|
legalName: varchar('legal_name', { length: 255 }),
|
|
amount: decimal('amount', { precision: 10, scale: 2 }).notNull(),
|
|
currency: varchar('currency', { length: 10 }).notNull().default('PYG'),
|
|
pdfUrl: varchar('pdf_url', { length: 500 }),
|
|
status: varchar('status', { length: 20 }).notNull().default('generated'),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
});
|
|
|
|
export const pgEvents = pgTable('events', {
|
|
id: uuid('id').primaryKey(),
|
|
slug: varchar('slug', { length: 255 }).unique(),
|
|
title: varchar('title', { length: 255 }).notNull(),
|
|
titleEs: varchar('title_es', { length: 255 }),
|
|
description: pgText('description').notNull(),
|
|
descriptionEs: pgText('description_es'),
|
|
shortDescription: varchar('short_description', { length: 300 }),
|
|
shortDescriptionEs: varchar('short_description_es', { length: 300 }),
|
|
startDatetime: timestamp('start_datetime', { withTimezone: true }).notNull(),
|
|
endDatetime: timestamp('end_datetime', { withTimezone: true }),
|
|
location: varchar('location', { length: 500 }).notNull(),
|
|
locationUrl: varchar('location_url', { length: 500 }),
|
|
price: decimal('price', { precision: 10, scale: 2 }).notNull().default('0'),
|
|
currency: varchar('currency', { length: 10 }).notNull().default('PYG'),
|
|
capacity: pgInteger('capacity').notNull().default(50),
|
|
status: varchar('status', { length: 20 }).notNull().default('draft'),
|
|
bannerUrl: varchar('banner_url', { length: 500 }),
|
|
externalBookingEnabled: pgInteger('external_booking_enabled').notNull().default(0),
|
|
externalBookingUrl: varchar('external_booking_url', { length: 500 }),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
updatedAt: timestamp('updated_at').notNull(),
|
|
});
|
|
|
|
// Historical slugs that still resolve (and redirect) to an event's canonical slug
|
|
export const pgEventSlugAliases = pgTable('event_slug_aliases', {
|
|
slug: varchar('slug', { length: 255 }).primaryKey(),
|
|
eventId: uuid('event_id').notNull().references(() => pgEvents.id),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
});
|
|
|
|
export const pgTickets = pgTable('tickets', {
|
|
id: uuid('id').primaryKey(),
|
|
bookingId: uuid('booking_id'), // Groups multiple tickets from same booking
|
|
userId: uuid('user_id').notNull().references(() => pgUsers.id),
|
|
eventId: uuid('event_id').notNull().references(() => pgEvents.id),
|
|
attendeeFirstName: varchar('attendee_first_name', { length: 255 }).notNull(),
|
|
attendeeLastName: varchar('attendee_last_name', { length: 255 }),
|
|
attendeeEmail: varchar('attendee_email', { length: 255 }),
|
|
attendeePhone: varchar('attendee_phone', { length: 50 }),
|
|
attendeeRuc: varchar('attendee_ruc', { length: 15 }), // Paraguayan tax ID for invoicing
|
|
preferredLanguage: varchar('preferred_language', { length: 10 }),
|
|
status: varchar('status', { length: 20 }).notNull().default('pending'),
|
|
checkinAt: timestamp('checkin_at'),
|
|
checkedInByAdminId: uuid('checked_in_by_admin_id').references(() => pgUsers.id), // Who performed the check-in
|
|
qrCode: varchar('qr_code', { length: 255 }),
|
|
adminNote: pgText('admin_note'),
|
|
isGuest: pgInteger('is_guest').notNull().default(0),
|
|
// Paid: revenue counted; Unpaid: balance due (collect at door); Comp: free guest, no revenue
|
|
paymentStatus: varchar('payment_status', { length: 10 }).notNull().default('unpaid'),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
});
|
|
|
|
export const pgPayments = pgTable('payments', {
|
|
id: uuid('id').primaryKey(),
|
|
ticketId: uuid('ticket_id').notNull().references(() => pgTickets.id),
|
|
provider: varchar('provider', { length: 50 }).notNull(),
|
|
amount: decimal('amount', { precision: 10, scale: 2 }).notNull(),
|
|
currency: varchar('currency', { length: 10 }).notNull().default('PYG'),
|
|
status: varchar('status', { length: 20 }).notNull().default('pending'),
|
|
reference: varchar('reference', { length: 255 }),
|
|
lnbitsInvoice: pgText('lnbits_invoice'), // BOLT11 string, stored so an unpaid invoice can be redisplayed without a new LNbits call
|
|
lnbitsExpiresAt: timestamp('lnbits_expires_at'), // When lnbitsInvoice expires
|
|
lnbitsAmountSats: pgInteger('lnbits_amount_sats'), // Sats amount for display (from LNbits' fiat conversion)
|
|
userMarkedPaidAt: timestamp('user_marked_paid_at'),
|
|
payerName: varchar('payer_name', { length: 255 }), // Name of payer if different from attendee
|
|
paidAt: timestamp('paid_at'),
|
|
paidByAdminId: uuid('paid_by_admin_id'),
|
|
adminNote: pgText('admin_note'),
|
|
reminderSentAt: timestamp('reminder_sent_at'), // When payment reminder email was sent
|
|
// Where the money was taken: 'presale' (online/admin, the default) or 'door'
|
|
// (recorded by staff on the door check-in screen). Splits pre-sale vs door revenue.
|
|
source: varchar('source', { length: 20 }).notNull().default('presale'),
|
|
// Door tender used, for the end-of-night cash-up. Null for pre-sale payments.
|
|
// 'guest' is a zero-amount comp entry and carries no revenue.
|
|
method: varchar('method', { length: 20 }),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
updatedAt: timestamp('updated_at').notNull(),
|
|
});
|
|
|
|
// Idempotency records for door check-in actions (see sqliteIdempotencyKeys).
|
|
export const pgIdempotencyKeys = pgTable('idempotency_keys', {
|
|
key: varchar('key', { length: 128 }).primaryKey(),
|
|
scope: varchar('scope', { length: 64 }).notNull(),
|
|
result: pgText('result').notNull(),
|
|
undoState: pgText('undo_state'),
|
|
undoneAt: timestamp('undone_at'),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
});
|
|
|
|
// Payment Options Configuration Table (global settings)
|
|
export const pgPaymentOptions = pgTable('payment_options', {
|
|
id: uuid('id').primaryKey(),
|
|
tpagoEnabled: pgInteger('tpago_enabled').notNull().default(0),
|
|
tpagoLink: varchar('tpago_link', { length: 500 }),
|
|
tpagoLink2: varchar('tpago_link_2', { length: 500 }),
|
|
tpagoLink3: varchar('tpago_link_3', { length: 500 }),
|
|
tpagoLink4: varchar('tpago_link_4', { length: 500 }),
|
|
tpagoLink5: varchar('tpago_link_5', { length: 500 }),
|
|
tpagoInstructions: pgText('tpago_instructions'),
|
|
tpagoInstructionsEs: pgText('tpago_instructions_es'),
|
|
bankTransferEnabled: pgInteger('bank_transfer_enabled').notNull().default(0),
|
|
bankName: varchar('bank_name', { length: 255 }),
|
|
bankAccountHolder: varchar('bank_account_holder', { length: 255 }),
|
|
bankAccountNumber: varchar('bank_account_number', { length: 100 }),
|
|
bankAlias: varchar('bank_alias', { length: 100 }),
|
|
bankPhone: varchar('bank_phone', { length: 50 }),
|
|
bankNotes: pgText('bank_notes'),
|
|
bankNotesEs: pgText('bank_notes_es'),
|
|
lightningEnabled: pgInteger('lightning_enabled').notNull().default(1),
|
|
cashEnabled: pgInteger('cash_enabled').notNull().default(1),
|
|
cashInstructions: pgText('cash_instructions'),
|
|
cashInstructionsEs: pgText('cash_instructions_es'),
|
|
allowDuplicateBookings: pgInteger('allow_duplicate_bookings').notNull().default(0),
|
|
updatedAt: timestamp('updated_at').notNull(),
|
|
updatedBy: uuid('updated_by').references(() => pgUsers.id),
|
|
});
|
|
|
|
// Event-specific payment overrides
|
|
export const pgEventPaymentOverrides = pgTable('event_payment_overrides', {
|
|
id: uuid('id').primaryKey(),
|
|
eventId: uuid('event_id').notNull().references(() => pgEvents.id),
|
|
tpagoEnabled: pgInteger('tpago_enabled'),
|
|
tpagoLink: varchar('tpago_link', { length: 500 }),
|
|
tpagoLink2: varchar('tpago_link_2', { length: 500 }),
|
|
tpagoLink3: varchar('tpago_link_3', { length: 500 }),
|
|
tpagoLink4: varchar('tpago_link_4', { length: 500 }),
|
|
tpagoLink5: varchar('tpago_link_5', { length: 500 }),
|
|
tpagoInstructions: pgText('tpago_instructions'),
|
|
tpagoInstructionsEs: pgText('tpago_instructions_es'),
|
|
bankTransferEnabled: pgInteger('bank_transfer_enabled'),
|
|
bankName: varchar('bank_name', { length: 255 }),
|
|
bankAccountHolder: varchar('bank_account_holder', { length: 255 }),
|
|
bankAccountNumber: varchar('bank_account_number', { length: 100 }),
|
|
bankAlias: varchar('bank_alias', { length: 100 }),
|
|
bankPhone: varchar('bank_phone', { length: 50 }),
|
|
bankNotes: pgText('bank_notes'),
|
|
bankNotesEs: pgText('bank_notes_es'),
|
|
lightningEnabled: pgInteger('lightning_enabled'),
|
|
cashEnabled: pgInteger('cash_enabled'),
|
|
cashInstructions: pgText('cash_instructions'),
|
|
cashInstructionsEs: pgText('cash_instructions_es'),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
updatedAt: timestamp('updated_at').notNull(),
|
|
});
|
|
|
|
export const pgContacts = pgTable('contacts', {
|
|
id: uuid('id').primaryKey(),
|
|
name: varchar('name', { length: 255 }).notNull(),
|
|
email: varchar('email', { length: 255 }).notNull(),
|
|
message: pgText('message').notNull(),
|
|
status: varchar('status', { length: 20 }).notNull().default('new'),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
});
|
|
|
|
export const pgEmailSubscribers = pgTable('email_subscribers', {
|
|
id: uuid('id').primaryKey(),
|
|
email: varchar('email', { length: 255 }).notNull().unique(),
|
|
name: varchar('name', { length: 255 }),
|
|
status: varchar('status', { length: 20 }).notNull().default('active'),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
});
|
|
|
|
export const pgMedia = pgTable('media', {
|
|
id: uuid('id').primaryKey(),
|
|
fileUrl: varchar('file_url', { length: 500 }).notNull(),
|
|
type: varchar('type', { length: 20 }).notNull(),
|
|
relatedId: uuid('related_id'),
|
|
relatedType: varchar('related_type', { length: 50 }),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
});
|
|
|
|
export const pgAuditLogs = pgTable('audit_logs', {
|
|
id: uuid('id').primaryKey(),
|
|
userId: uuid('user_id').references(() => pgUsers.id),
|
|
action: varchar('action', { length: 100 }).notNull(),
|
|
target: varchar('target', { length: 100 }),
|
|
targetId: uuid('target_id'),
|
|
details: pgText('details'),
|
|
timestamp: timestamp('timestamp').notNull(),
|
|
});
|
|
|
|
export const pgEmailTemplates = pgTable('email_templates', {
|
|
id: uuid('id').primaryKey(),
|
|
name: varchar('name', { length: 255 }).notNull().unique(),
|
|
slug: varchar('slug', { length: 100 }).notNull().unique(),
|
|
subject: varchar('subject', { length: 500 }).notNull(),
|
|
subjectEs: varchar('subject_es', { length: 500 }),
|
|
bodyHtml: pgText('body_html').notNull(),
|
|
bodyHtmlEs: pgText('body_html_es'),
|
|
bodyText: pgText('body_text'),
|
|
bodyTextEs: pgText('body_text_es'),
|
|
description: pgText('description'),
|
|
variables: pgText('variables'), // JSON array of available variables
|
|
isSystem: pgInteger('is_system').notNull().default(0),
|
|
isActive: pgInteger('is_active').notNull().default(1),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
updatedAt: timestamp('updated_at').notNull(),
|
|
});
|
|
|
|
export const pgEmailLogs = pgTable('email_logs', {
|
|
id: uuid('id').primaryKey(),
|
|
templateId: uuid('template_id').references(() => pgEmailTemplates.id),
|
|
eventId: uuid('event_id').references(() => pgEvents.id),
|
|
recipientEmail: varchar('recipient_email', { length: 255 }).notNull(),
|
|
recipientName: varchar('recipient_name', { length: 255 }),
|
|
subject: varchar('subject', { length: 500 }).notNull(),
|
|
bodyHtml: pgText('body_html'),
|
|
status: varchar('status', { length: 20 }).notNull().default('pending'),
|
|
errorMessage: pgText('error_message'),
|
|
sentAt: timestamp('sent_at'),
|
|
sentBy: uuid('sent_by').references(() => pgUsers.id),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
resendAttempts: pgInteger('resend_attempts').notNull().default(0),
|
|
lastResentAt: timestamp('last_resent_at'),
|
|
});
|
|
|
|
export const pgEmailSettings = pgTable('email_settings', {
|
|
id: uuid('id').primaryKey(),
|
|
key: varchar('key', { length: 100 }).notNull().unique(),
|
|
value: pgText('value').notNull(),
|
|
updatedAt: timestamp('updated_at').notNull(),
|
|
});
|
|
|
|
// Durable email queue. Jobs survive process restarts; a startup recovery step
|
|
// resets any 'processing' rows back to 'pending'.
|
|
export const pgEmailQueue = pgTable('email_queue', {
|
|
id: uuid('id').primaryKey(),
|
|
params: pgText('params').notNull(), // JSON-encoded TemplateEmailJobParams
|
|
status: varchar('status', { length: 20 }).notNull().default('pending'),
|
|
attempts: pgInteger('attempts').notNull().default(0),
|
|
lastError: pgText('last_error'),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
processedAt: timestamp('processed_at'),
|
|
});
|
|
|
|
// Legal Pages table for admin-editable legal content
|
|
export const pgLegalPages = pgTable('legal_pages', {
|
|
id: uuid('id').primaryKey(),
|
|
slug: varchar('slug', { length: 100 }).notNull().unique(),
|
|
title: varchar('title', { length: 255 }).notNull(), // English title
|
|
titleEs: varchar('title_es', { length: 255 }), // Spanish title
|
|
contentText: pgText('content_text').notNull(), // Plain text edited by admin (English)
|
|
contentTextEs: pgText('content_text_es'), // Plain text edited by admin (Spanish)
|
|
contentMarkdown: pgText('content_markdown').notNull(), // Generated markdown for public display (English)
|
|
contentMarkdownEs: pgText('content_markdown_es'), // Generated markdown for public display (Spanish)
|
|
updatedAt: timestamp('updated_at').notNull(),
|
|
updatedBy: uuid('updated_by').references(() => pgUsers.id),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
});
|
|
|
|
// FAQ questions table (admin-managed)
|
|
export const pgFaqQuestions = pgTable('faq_questions', {
|
|
id: uuid('id').primaryKey(),
|
|
question: pgText('question').notNull(),
|
|
questionEs: pgText('question_es'),
|
|
answer: pgText('answer').notNull(),
|
|
answerEs: pgText('answer_es'),
|
|
enabled: pgInteger('enabled').notNull().default(1),
|
|
showOnHomepage: pgInteger('show_on_homepage').notNull().default(0),
|
|
rank: pgInteger('rank').notNull().default(0),
|
|
createdAt: timestamp('created_at').notNull(),
|
|
updatedAt: timestamp('updated_at').notNull(),
|
|
});
|
|
|
|
// Legal Settings table for legal page placeholder values
|
|
export const pgLegalSettings = pgTable('legal_settings', {
|
|
id: uuid('id').primaryKey(),
|
|
companyName: varchar('company_name', { length: 255 }),
|
|
legalEntityName: varchar('legal_entity_name', { length: 255 }),
|
|
rucNumber: varchar('ruc_number', { length: 50 }),
|
|
companyAddress: pgText('company_address'),
|
|
companyCity: varchar('company_city', { length: 100 }),
|
|
companyCountry: varchar('company_country', { length: 100 }),
|
|
supportEmail: varchar('support_email', { length: 255 }),
|
|
legalEmail: varchar('legal_email', { length: 255 }),
|
|
governingLaw: varchar('governing_law', { length: 255 }),
|
|
jurisdictionCity: varchar('jurisdiction_city', { length: 100 }),
|
|
updatedAt: timestamp('updated_at').notNull(),
|
|
updatedBy: uuid('updated_by').references(() => pgUsers.id),
|
|
});
|
|
|
|
// Site Settings table for global website configuration
|
|
export const pgSiteSettings = pgTable('site_settings', {
|
|
id: uuid('id').primaryKey(),
|
|
// Timezone configuration
|
|
timezone: varchar('timezone', { length: 100 }).notNull().default('America/Asuncion'),
|
|
// Site info
|
|
siteName: varchar('site_name', { length: 255 }).notNull().default('Spanglish'),
|
|
siteDescription: pgText('site_description'),
|
|
siteDescriptionEs: pgText('site_description_es'),
|
|
// Contact info
|
|
contactEmail: varchar('contact_email', { length: 255 }),
|
|
contactPhone: varchar('contact_phone', { length: 50 }),
|
|
// Social links
|
|
facebookUrl: varchar('facebook_url', { length: 500 }),
|
|
instagramUrl: varchar('instagram_url', { length: 500 }),
|
|
twitterUrl: varchar('twitter_url', { length: 500 }),
|
|
linkedinUrl: varchar('linkedin_url', { length: 500 }),
|
|
// Featured event - manually promoted event shown on homepage/linktree
|
|
featuredEventId: uuid('featured_event_id').references(() => pgEvents.id),
|
|
// Other settings
|
|
maintenanceMode: pgInteger('maintenance_mode').notNull().default(0),
|
|
maintenanceMessage: pgText('maintenance_message'),
|
|
maintenanceMessageEs: pgText('maintenance_message_es'),
|
|
// Metadata
|
|
updatedAt: timestamp('updated_at').notNull(),
|
|
updatedBy: uuid('updated_by').references(() => pgUsers.id),
|
|
});
|
|
|
|
// Export the appropriate schema based on DB_TYPE
|
|
export const users = dbType === 'postgres' ? pgUsers : sqliteUsers;
|
|
export const events = dbType === 'postgres' ? pgEvents : sqliteEvents;
|
|
export const eventSlugAliases = dbType === 'postgres' ? pgEventSlugAliases : sqliteEventSlugAliases;
|
|
export const tickets = dbType === 'postgres' ? pgTickets : sqliteTickets;
|
|
export const payments = dbType === 'postgres' ? pgPayments : sqlitePayments;
|
|
export const idempotencyKeys = dbType === 'postgres' ? pgIdempotencyKeys : sqliteIdempotencyKeys;
|
|
export const contacts = dbType === 'postgres' ? pgContacts : sqliteContacts;
|
|
export const emailSubscribers = dbType === 'postgres' ? pgEmailSubscribers : sqliteEmailSubscribers;
|
|
export const media = dbType === 'postgres' ? pgMedia : sqliteMedia;
|
|
export const auditLogs = dbType === 'postgres' ? pgAuditLogs : sqliteAuditLogs;
|
|
export const emailTemplates = dbType === 'postgres' ? pgEmailTemplates : sqliteEmailTemplates;
|
|
export const emailLogs = dbType === 'postgres' ? pgEmailLogs : sqliteEmailLogs;
|
|
export const emailSettings = dbType === 'postgres' ? pgEmailSettings : sqliteEmailSettings;
|
|
export const emailQueue = dbType === 'postgres' ? pgEmailQueue : sqliteEmailQueue;
|
|
export const paymentOptions = dbType === 'postgres' ? pgPaymentOptions : sqlitePaymentOptions;
|
|
export const eventPaymentOverrides = dbType === 'postgres' ? pgEventPaymentOverrides : sqliteEventPaymentOverrides;
|
|
export const magicLinkTokens = dbType === 'postgres' ? pgMagicLinkTokens : sqliteMagicLinkTokens;
|
|
export const userSessions = dbType === 'postgres' ? pgUserSessions : sqliteUserSessions;
|
|
export const invoices = dbType === 'postgres' ? pgInvoices : sqliteInvoices;
|
|
export const legalSettings = dbType === 'postgres' ? pgLegalSettings : sqliteLegalSettings;
|
|
export const siteSettings = dbType === 'postgres' ? pgSiteSettings : sqliteSiteSettings;
|
|
export const legalPages = dbType === 'postgres' ? pgLegalPages : sqliteLegalPages;
|
|
export const faqQuestions = dbType === 'postgres' ? pgFaqQuestions : sqliteFaqQuestions;
|
|
|
|
// Type exports
|
|
export type User = typeof sqliteUsers.$inferSelect;
|
|
export type NewUser = typeof sqliteUsers.$inferInsert;
|
|
export type Event = typeof sqliteEvents.$inferSelect;
|
|
export type NewEvent = typeof sqliteEvents.$inferInsert;
|
|
export type Ticket = typeof sqliteTickets.$inferSelect;
|
|
export type NewTicket = typeof sqliteTickets.$inferInsert;
|
|
export type Payment = typeof sqlitePayments.$inferSelect;
|
|
export type NewPayment = typeof sqlitePayments.$inferInsert;
|
|
export type Contact = typeof sqliteContacts.$inferSelect;
|
|
export type NewContact = typeof sqliteContacts.$inferInsert;
|
|
export type EmailTemplate = typeof sqliteEmailTemplates.$inferSelect;
|
|
export type NewEmailTemplate = typeof sqliteEmailTemplates.$inferInsert;
|
|
export type EmailLog = typeof sqliteEmailLogs.$inferSelect;
|
|
export type NewEmailLog = typeof sqliteEmailLogs.$inferInsert;
|
|
export type PaymentOptions = typeof sqlitePaymentOptions.$inferSelect;
|
|
export type NewPaymentOptions = typeof sqlitePaymentOptions.$inferInsert;
|
|
export type EventPaymentOverride = typeof sqliteEventPaymentOverrides.$inferSelect;
|
|
export type NewEventPaymentOverride = typeof sqliteEventPaymentOverrides.$inferInsert;
|
|
export type MagicLinkToken = typeof sqliteMagicLinkTokens.$inferSelect;
|
|
export type NewMagicLinkToken = typeof sqliteMagicLinkTokens.$inferInsert;
|
|
export type UserSession = typeof sqliteUserSessions.$inferSelect;
|
|
export type NewUserSession = typeof sqliteUserSessions.$inferInsert;
|
|
export type Invoice = typeof sqliteInvoices.$inferSelect;
|
|
export type NewInvoice = typeof sqliteInvoices.$inferInsert;
|
|
export type SiteSettings = typeof sqliteSiteSettings.$inferSelect;
|
|
export type NewSiteSettings = typeof sqliteSiteSettings.$inferInsert;
|
|
export type LegalPage = typeof sqliteLegalPages.$inferSelect;
|
|
export type NewLegalPage = typeof sqliteLegalPages.$inferInsert;
|
|
export type FaqQuestion = typeof sqliteFaqQuestions.$inferSelect;
|
|
export type NewFaqQuestion = typeof sqliteFaqQuestions.$inferInsert;
|
|
export type LegalSettings = typeof sqliteLegalSettings.$inferSelect;
|
|
export type NewLegalSettings = typeof sqliteLegalSettings.$inferInsert; |