- Add resend_attempts and last_resent_at to email_logs schema and migrations - Add POST /api/emails/logs/:id/resend and emailService.resendFromLog - Add resendLog API and EmailLog.resendAttempts/lastResentAt - Add All/Failed sub-tabs, resend button for all emails, re-sent indicator in logs and detail modal Made-with: Cursor
887 lines
30 KiB
TypeScript
887 lines
30 KiB
TypeScript
import 'dotenv/config';
|
|
import { db } from './index.js';
|
|
import { sql } from 'drizzle-orm';
|
|
|
|
const dbType = process.env.DB_TYPE || 'sqlite';
|
|
console.log(`Database type: ${dbType}`);
|
|
console.log(`Database URL: ${process.env.DATABASE_URL?.substring(0, 30)}...`);
|
|
|
|
async function migrate() {
|
|
console.log('Running migrations...');
|
|
|
|
if (dbType === 'sqlite') {
|
|
// SQLite migrations
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT PRIMARY KEY,
|
|
email TEXT NOT NULL UNIQUE,
|
|
password TEXT,
|
|
name TEXT NOT NULL,
|
|
phone TEXT,
|
|
role TEXT NOT NULL DEFAULT 'user',
|
|
language_preference TEXT,
|
|
is_claimed INTEGER NOT NULL DEFAULT 1,
|
|
google_id TEXT,
|
|
ruc_number TEXT,
|
|
account_status TEXT NOT NULL DEFAULT 'active',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Add new user columns if they don't exist (for existing databases)
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE users ADD COLUMN is_claimed INTEGER NOT NULL DEFAULT 1`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE users ADD COLUMN google_id TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE users ADD COLUMN ruc_number TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE users ADD COLUMN account_status TEXT NOT NULL DEFAULT 'active'`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
// Magic link tokens table
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS magic_link_tokens (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id),
|
|
token TEXT NOT NULL UNIQUE,
|
|
type TEXT NOT NULL,
|
|
expires_at TEXT NOT NULL,
|
|
used_at TEXT,
|
|
created_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
// User sessions table
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS user_sessions (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id),
|
|
token TEXT NOT NULL UNIQUE,
|
|
user_agent TEXT,
|
|
ip_address TEXT,
|
|
last_active_at TEXT NOT NULL,
|
|
expires_at TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS events (
|
|
id TEXT PRIMARY KEY,
|
|
title TEXT NOT NULL,
|
|
title_es TEXT,
|
|
description TEXT NOT NULL,
|
|
description_es TEXT,
|
|
short_description TEXT,
|
|
short_description_es TEXT,
|
|
start_datetime TEXT NOT NULL,
|
|
end_datetime TEXT,
|
|
location TEXT NOT NULL,
|
|
location_url TEXT,
|
|
price REAL NOT NULL DEFAULT 0,
|
|
currency TEXT NOT NULL DEFAULT 'PYG',
|
|
capacity INTEGER NOT NULL DEFAULT 50,
|
|
status TEXT NOT NULL DEFAULT 'draft',
|
|
banner_url TEXT,
|
|
external_booking_enabled INTEGER NOT NULL DEFAULT 0,
|
|
external_booking_url TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Add external booking columns to events if they don't exist (for existing databases)
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE events ADD COLUMN external_booking_enabled INTEGER NOT NULL DEFAULT 0`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE events ADD COLUMN external_booking_url TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
// Add short description columns to events
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE events ADD COLUMN short_description TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE events ADD COLUMN short_description_es TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
await (db as any).run(sql`
|
|
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,
|
|
preferred_language TEXT,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
checkin_at TEXT,
|
|
qr_code TEXT,
|
|
created_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Add new columns if they don't exist (for existing databases)
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_name TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_email TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_phone TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN preferred_language TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN admin_note TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
// Migration: Add first/last name columns
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_first_name TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_last_name TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_ruc TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
// Migration: Copy data from attendee_name to attendee_first_name if attendee_first_name is empty
|
|
try {
|
|
await (db as any).run(sql`
|
|
UPDATE tickets
|
|
SET attendee_first_name = attendee_name
|
|
WHERE attendee_first_name IS NULL AND attendee_name IS NOT NULL
|
|
`);
|
|
} catch (e) { /* migration may have already run */ }
|
|
|
|
// Migration: Add checked_in_by_admin_id column to tickets
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN checked_in_by_admin_id TEXT REFERENCES users(id)`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
// Migration: Add booking_id column to tickets for multi-ticket bookings
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN booking_id TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
// Make attendee_email and attendee_phone nullable (recreate table if needed or just allow nulls for new entries)
|
|
// SQLite doesn't support altering column constraints, so we'll just ensure new entries work
|
|
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS payments (
|
|
id TEXT PRIMARY KEY,
|
|
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
|
provider TEXT NOT NULL,
|
|
amount REAL NOT NULL,
|
|
currency TEXT NOT NULL DEFAULT 'PYG',
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
reference TEXT,
|
|
paid_at TEXT,
|
|
paid_by_admin_id TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Add new columns if they don't exist (for existing databases)
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN paid_at TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN paid_by_admin_id TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN user_marked_paid_at TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN admin_note TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN payer_name TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN reminder_sent_at TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
// Invoices table
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS invoices (
|
|
id TEXT PRIMARY KEY,
|
|
payment_id TEXT NOT NULL REFERENCES payments(id),
|
|
user_id TEXT NOT NULL REFERENCES users(id),
|
|
invoice_number TEXT NOT NULL UNIQUE,
|
|
ruc_number TEXT,
|
|
legal_name TEXT,
|
|
amount REAL NOT NULL,
|
|
currency TEXT NOT NULL DEFAULT 'PYG',
|
|
pdf_url TEXT,
|
|
status TEXT NOT NULL DEFAULT 'generated',
|
|
created_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Payment options table
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS payment_options (
|
|
id TEXT PRIMARY KEY,
|
|
tpago_enabled INTEGER NOT NULL DEFAULT 0,
|
|
tpago_link TEXT,
|
|
tpago_instructions TEXT,
|
|
tpago_instructions_es TEXT,
|
|
bank_transfer_enabled INTEGER NOT NULL DEFAULT 0,
|
|
bank_name TEXT,
|
|
bank_account_holder TEXT,
|
|
bank_account_number TEXT,
|
|
bank_alias TEXT,
|
|
bank_phone TEXT,
|
|
bank_notes TEXT,
|
|
bank_notes_es TEXT,
|
|
lightning_enabled INTEGER NOT NULL DEFAULT 1,
|
|
cash_enabled INTEGER NOT NULL DEFAULT 1,
|
|
cash_instructions TEXT,
|
|
cash_instructions_es TEXT,
|
|
updated_at TEXT NOT NULL,
|
|
updated_by TEXT REFERENCES users(id)
|
|
)
|
|
`);
|
|
|
|
// Add allow_duplicate_bookings column to payment_options if it doesn't exist
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE payment_options ADD COLUMN allow_duplicate_bookings INTEGER NOT NULL DEFAULT 0`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
// Event payment overrides table
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS event_payment_overrides (
|
|
id TEXT PRIMARY KEY,
|
|
event_id TEXT NOT NULL REFERENCES events(id),
|
|
tpago_enabled INTEGER,
|
|
tpago_link TEXT,
|
|
tpago_instructions TEXT,
|
|
tpago_instructions_es TEXT,
|
|
bank_transfer_enabled INTEGER,
|
|
bank_name TEXT,
|
|
bank_account_holder TEXT,
|
|
bank_account_number TEXT,
|
|
bank_alias TEXT,
|
|
bank_phone TEXT,
|
|
bank_notes TEXT,
|
|
bank_notes_es TEXT,
|
|
lightning_enabled INTEGER,
|
|
cash_enabled INTEGER,
|
|
cash_instructions TEXT,
|
|
cash_instructions_es TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS contacts (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
email TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'new',
|
|
created_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS email_subscribers (
|
|
id TEXT PRIMARY KEY,
|
|
email TEXT NOT NULL UNIQUE,
|
|
name TEXT,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
created_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS media (
|
|
id TEXT PRIMARY KEY,
|
|
file_url TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
related_id TEXT,
|
|
related_type TEXT,
|
|
created_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT REFERENCES users(id),
|
|
action TEXT NOT NULL,
|
|
target TEXT,
|
|
target_id TEXT,
|
|
details TEXT,
|
|
timestamp TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Email system tables
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS email_templates (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL UNIQUE,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
subject TEXT NOT NULL,
|
|
subject_es TEXT,
|
|
body_html TEXT NOT NULL,
|
|
body_html_es TEXT,
|
|
body_text TEXT,
|
|
body_text_es TEXT,
|
|
description TEXT,
|
|
variables TEXT,
|
|
is_system INTEGER NOT NULL DEFAULT 0,
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS email_logs (
|
|
id TEXT PRIMARY KEY,
|
|
template_id TEXT REFERENCES email_templates(id),
|
|
event_id TEXT REFERENCES events(id),
|
|
recipient_email TEXT NOT NULL,
|
|
recipient_name TEXT,
|
|
subject TEXT NOT NULL,
|
|
body_html TEXT,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
error_message TEXT,
|
|
sent_at TEXT,
|
|
sent_by TEXT REFERENCES users(id),
|
|
created_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE email_logs ADD COLUMN resend_attempts INTEGER NOT NULL DEFAULT 0`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE email_logs ADD COLUMN last_resent_at TEXT`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS email_settings (
|
|
id TEXT PRIMARY KEY,
|
|
key TEXT NOT NULL UNIQUE,
|
|
value TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Site settings table
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS site_settings (
|
|
id TEXT PRIMARY KEY,
|
|
timezone TEXT NOT NULL DEFAULT 'America/Asuncion',
|
|
site_name TEXT NOT NULL DEFAULT 'Spanglish',
|
|
site_description TEXT,
|
|
site_description_es TEXT,
|
|
contact_email TEXT,
|
|
contact_phone TEXT,
|
|
facebook_url TEXT,
|
|
instagram_url TEXT,
|
|
twitter_url TEXT,
|
|
linkedin_url TEXT,
|
|
featured_event_id TEXT REFERENCES events(id),
|
|
maintenance_mode INTEGER NOT NULL DEFAULT 0,
|
|
maintenance_message TEXT,
|
|
maintenance_message_es TEXT,
|
|
updated_at TEXT NOT NULL,
|
|
updated_by TEXT REFERENCES users(id)
|
|
)
|
|
`);
|
|
|
|
// Add featured_event_id column to site_settings if it doesn't exist
|
|
try {
|
|
await (db as any).run(sql`ALTER TABLE site_settings ADD COLUMN featured_event_id TEXT REFERENCES events(id)`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
// Legal pages table for admin-editable legal content
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS legal_pages (
|
|
id TEXT PRIMARY KEY,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
title TEXT NOT NULL,
|
|
title_es TEXT,
|
|
content_text TEXT NOT NULL,
|
|
content_text_es TEXT,
|
|
content_markdown TEXT NOT NULL,
|
|
content_markdown_es TEXT,
|
|
updated_at TEXT NOT NULL,
|
|
updated_by TEXT REFERENCES users(id),
|
|
created_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
// FAQ questions table
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS faq_questions (
|
|
id TEXT PRIMARY KEY,
|
|
question TEXT NOT NULL,
|
|
question_es TEXT,
|
|
answer TEXT NOT NULL,
|
|
answer_es TEXT,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
show_on_homepage INTEGER NOT NULL DEFAULT 0,
|
|
rank INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Legal settings table for legal page placeholder values
|
|
await (db as any).run(sql`
|
|
CREATE TABLE IF NOT EXISTS legal_settings (
|
|
id TEXT PRIMARY KEY,
|
|
company_name TEXT,
|
|
legal_entity_name TEXT,
|
|
ruc_number TEXT,
|
|
company_address TEXT,
|
|
company_city TEXT,
|
|
company_country TEXT,
|
|
support_email TEXT,
|
|
legal_email TEXT,
|
|
governing_law TEXT,
|
|
jurisdiction_city TEXT,
|
|
updated_at TEXT NOT NULL,
|
|
updated_by TEXT REFERENCES users(id)
|
|
)
|
|
`);
|
|
} else {
|
|
// PostgreSQL migrations
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id UUID PRIMARY KEY,
|
|
email VARCHAR(255) NOT NULL UNIQUE,
|
|
password VARCHAR(255),
|
|
name VARCHAR(255) NOT NULL,
|
|
phone VARCHAR(50),
|
|
role VARCHAR(20) NOT NULL DEFAULT 'user',
|
|
language_preference VARCHAR(10),
|
|
is_claimed INTEGER NOT NULL DEFAULT 1,
|
|
google_id VARCHAR(255),
|
|
ruc_number VARCHAR(15),
|
|
account_status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Add new user columns for existing PostgreSQL databases
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE users ADD COLUMN is_claimed INTEGER NOT NULL DEFAULT 1`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE users ADD COLUMN google_id VARCHAR(255)`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE users ADD COLUMN ruc_number VARCHAR(15)`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE users ADD COLUMN account_status VARCHAR(20) NOT NULL DEFAULT 'active'`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
// Magic link tokens table
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS magic_link_tokens (
|
|
id UUID PRIMARY KEY,
|
|
user_id UUID NOT NULL REFERENCES users(id),
|
|
token VARCHAR(255) NOT NULL UNIQUE,
|
|
type VARCHAR(30) NOT NULL,
|
|
expires_at TIMESTAMP NOT NULL,
|
|
used_at TIMESTAMP,
|
|
created_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
// User sessions table
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS user_sessions (
|
|
id UUID PRIMARY KEY,
|
|
user_id UUID NOT NULL REFERENCES users(id),
|
|
token VARCHAR(500) NOT NULL UNIQUE,
|
|
user_agent TEXT,
|
|
ip_address VARCHAR(45),
|
|
last_active_at TIMESTAMP NOT NULL,
|
|
expires_at TIMESTAMP NOT NULL,
|
|
created_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS events (
|
|
id UUID PRIMARY KEY,
|
|
title VARCHAR(255) NOT NULL,
|
|
title_es VARCHAR(255),
|
|
description TEXT NOT NULL,
|
|
description_es TEXT,
|
|
short_description VARCHAR(300),
|
|
short_description_es VARCHAR(300),
|
|
start_datetime TIMESTAMP NOT NULL,
|
|
end_datetime TIMESTAMP,
|
|
location VARCHAR(500) NOT NULL,
|
|
location_url VARCHAR(500),
|
|
price DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
|
currency VARCHAR(10) NOT NULL DEFAULT 'PYG',
|
|
capacity INTEGER NOT NULL DEFAULT 50,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'draft',
|
|
banner_url VARCHAR(500),
|
|
external_booking_enabled INTEGER NOT NULL DEFAULT 0,
|
|
external_booking_url VARCHAR(500),
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Add external booking columns to events if they don't exist (for existing databases)
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE events ADD COLUMN external_booking_enabled INTEGER NOT NULL DEFAULT 0`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE events ADD COLUMN external_booking_url VARCHAR(500)`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
// Add short description columns to events
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE events ADD COLUMN short_description VARCHAR(300)`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE events ADD COLUMN short_description_es VARCHAR(300)`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS tickets (
|
|
id UUID PRIMARY KEY,
|
|
user_id UUID NOT NULL REFERENCES users(id),
|
|
event_id UUID NOT NULL REFERENCES events(id),
|
|
attendee_first_name VARCHAR(255) NOT NULL,
|
|
attendee_last_name VARCHAR(255),
|
|
attendee_email VARCHAR(255),
|
|
attendee_phone VARCHAR(50),
|
|
attendee_ruc VARCHAR(15),
|
|
preferred_language VARCHAR(10),
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
checkin_at TIMESTAMP,
|
|
qr_code VARCHAR(255),
|
|
admin_note TEXT,
|
|
created_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Add attendee_ruc column if it doesn't exist
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE tickets ADD COLUMN attendee_ruc VARCHAR(15)`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
// Add checked_in_by_admin_id column to tickets
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE tickets ADD COLUMN checked_in_by_admin_id UUID REFERENCES users(id)`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
// Migration: Add booking_id column to tickets for multi-ticket bookings
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE tickets ADD COLUMN booking_id UUID`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS payments (
|
|
id UUID PRIMARY KEY,
|
|
ticket_id UUID NOT NULL REFERENCES tickets(id),
|
|
provider VARCHAR(50) NOT NULL,
|
|
amount DECIMAL(10, 2) NOT NULL,
|
|
currency VARCHAR(10) NOT NULL DEFAULT 'PYG',
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
reference VARCHAR(255),
|
|
user_marked_paid_at TIMESTAMP,
|
|
payer_name VARCHAR(255),
|
|
paid_at TIMESTAMP,
|
|
paid_by_admin_id UUID,
|
|
admin_note TEXT,
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Add payer_name column if it doesn't exist
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN payer_name VARCHAR(255)`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN reminder_sent_at TIMESTAMP`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
// Invoices table
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS invoices (
|
|
id UUID PRIMARY KEY,
|
|
payment_id UUID NOT NULL REFERENCES payments(id),
|
|
user_id UUID NOT NULL REFERENCES users(id),
|
|
invoice_number VARCHAR(50) NOT NULL UNIQUE,
|
|
ruc_number VARCHAR(15),
|
|
legal_name VARCHAR(255),
|
|
amount DECIMAL(10, 2) NOT NULL,
|
|
currency VARCHAR(10) NOT NULL DEFAULT 'PYG',
|
|
pdf_url VARCHAR(500),
|
|
status VARCHAR(20) NOT NULL DEFAULT 'generated',
|
|
created_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS payment_options (
|
|
id UUID PRIMARY KEY,
|
|
tpago_enabled INTEGER NOT NULL DEFAULT 0,
|
|
tpago_link VARCHAR(500),
|
|
tpago_instructions TEXT,
|
|
tpago_instructions_es TEXT,
|
|
bank_transfer_enabled INTEGER NOT NULL DEFAULT 0,
|
|
bank_name VARCHAR(255),
|
|
bank_account_holder VARCHAR(255),
|
|
bank_account_number VARCHAR(100),
|
|
bank_alias VARCHAR(100),
|
|
bank_phone VARCHAR(50),
|
|
bank_notes TEXT,
|
|
bank_notes_es TEXT,
|
|
lightning_enabled INTEGER NOT NULL DEFAULT 1,
|
|
cash_enabled INTEGER NOT NULL DEFAULT 1,
|
|
cash_instructions TEXT,
|
|
cash_instructions_es TEXT,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
updated_by UUID REFERENCES users(id)
|
|
)
|
|
`);
|
|
|
|
// Add allow_duplicate_bookings column to payment_options if it doesn't exist
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE payment_options ADD COLUMN allow_duplicate_bookings INTEGER NOT NULL DEFAULT 0`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS event_payment_overrides (
|
|
id UUID PRIMARY KEY,
|
|
event_id UUID NOT NULL REFERENCES events(id),
|
|
tpago_enabled INTEGER,
|
|
tpago_link VARCHAR(500),
|
|
tpago_instructions TEXT,
|
|
tpago_instructions_es TEXT,
|
|
bank_transfer_enabled INTEGER,
|
|
bank_name VARCHAR(255),
|
|
bank_account_holder VARCHAR(255),
|
|
bank_account_number VARCHAR(100),
|
|
bank_alias VARCHAR(100),
|
|
bank_phone VARCHAR(50),
|
|
bank_notes TEXT,
|
|
bank_notes_es TEXT,
|
|
lightning_enabled INTEGER,
|
|
cash_enabled INTEGER,
|
|
cash_instructions TEXT,
|
|
cash_instructions_es TEXT,
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS contacts (
|
|
id UUID PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL,
|
|
email VARCHAR(255) NOT NULL,
|
|
message TEXT NOT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'new',
|
|
created_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS email_subscribers (
|
|
id UUID PRIMARY KEY,
|
|
email VARCHAR(255) NOT NULL UNIQUE,
|
|
name VARCHAR(255),
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
created_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS media (
|
|
id UUID PRIMARY KEY,
|
|
file_url VARCHAR(500) NOT NULL,
|
|
type VARCHAR(20) NOT NULL,
|
|
related_id UUID,
|
|
related_type VARCHAR(50),
|
|
created_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
id UUID PRIMARY KEY,
|
|
user_id UUID REFERENCES users(id),
|
|
action VARCHAR(100) NOT NULL,
|
|
target VARCHAR(100),
|
|
target_id UUID,
|
|
details TEXT,
|
|
timestamp TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Email system tables
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS email_templates (
|
|
id UUID PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL UNIQUE,
|
|
slug VARCHAR(100) NOT NULL UNIQUE,
|
|
subject VARCHAR(500) NOT NULL,
|
|
subject_es VARCHAR(500),
|
|
body_html TEXT NOT NULL,
|
|
body_html_es TEXT,
|
|
body_text TEXT,
|
|
body_text_es TEXT,
|
|
description TEXT,
|
|
variables TEXT,
|
|
is_system INTEGER NOT NULL DEFAULT 0,
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS email_logs (
|
|
id UUID PRIMARY KEY,
|
|
template_id UUID REFERENCES email_templates(id),
|
|
event_id UUID REFERENCES events(id),
|
|
recipient_email VARCHAR(255) NOT NULL,
|
|
recipient_name VARCHAR(255),
|
|
subject VARCHAR(500) NOT NULL,
|
|
body_html TEXT,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
error_message TEXT,
|
|
sent_at TIMESTAMP,
|
|
sent_by UUID REFERENCES users(id),
|
|
created_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE email_logs ADD COLUMN resend_attempts INTEGER NOT NULL DEFAULT 0`);
|
|
} catch (e) { /* column may already exist */ }
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE email_logs ADD COLUMN last_resent_at TIMESTAMP`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS email_settings (
|
|
id UUID PRIMARY KEY,
|
|
key VARCHAR(100) NOT NULL UNIQUE,
|
|
value TEXT NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Site settings table
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS site_settings (
|
|
id UUID PRIMARY KEY,
|
|
timezone VARCHAR(100) NOT NULL DEFAULT 'America/Asuncion',
|
|
site_name VARCHAR(255) NOT NULL DEFAULT 'Spanglish',
|
|
site_description TEXT,
|
|
site_description_es TEXT,
|
|
contact_email VARCHAR(255),
|
|
contact_phone VARCHAR(50),
|
|
facebook_url VARCHAR(500),
|
|
instagram_url VARCHAR(500),
|
|
twitter_url VARCHAR(500),
|
|
linkedin_url VARCHAR(500),
|
|
featured_event_id UUID REFERENCES events(id),
|
|
maintenance_mode INTEGER NOT NULL DEFAULT 0,
|
|
maintenance_message TEXT,
|
|
maintenance_message_es TEXT,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
updated_by UUID REFERENCES users(id)
|
|
)
|
|
`);
|
|
|
|
// Add featured_event_id column to site_settings if it doesn't exist
|
|
try {
|
|
await (db as any).execute(sql`ALTER TABLE site_settings ADD COLUMN featured_event_id UUID REFERENCES events(id)`);
|
|
} catch (e) { /* column may already exist */ }
|
|
|
|
// Legal pages table for admin-editable legal content
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS legal_pages (
|
|
id UUID PRIMARY KEY,
|
|
slug VARCHAR(100) NOT NULL UNIQUE,
|
|
title VARCHAR(255) NOT NULL,
|
|
title_es VARCHAR(255),
|
|
content_text TEXT NOT NULL,
|
|
content_text_es TEXT,
|
|
content_markdown TEXT NOT NULL,
|
|
content_markdown_es TEXT,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
updated_by UUID REFERENCES users(id),
|
|
created_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
// FAQ questions table
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS faq_questions (
|
|
id UUID PRIMARY KEY,
|
|
question TEXT NOT NULL,
|
|
question_es TEXT,
|
|
answer TEXT NOT NULL,
|
|
answer_es TEXT,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
show_on_homepage INTEGER NOT NULL DEFAULT 0,
|
|
rank INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Legal settings table for legal page placeholder values
|
|
await (db as any).execute(sql`
|
|
CREATE TABLE IF NOT EXISTS legal_settings (
|
|
id UUID PRIMARY KEY,
|
|
company_name VARCHAR(255),
|
|
legal_entity_name VARCHAR(255),
|
|
ruc_number VARCHAR(50),
|
|
company_address TEXT,
|
|
company_city VARCHAR(100),
|
|
company_country VARCHAR(100),
|
|
support_email VARCHAR(255),
|
|
legal_email VARCHAR(255),
|
|
governing_law VARCHAR(255),
|
|
jurisdiction_city VARCHAR(100),
|
|
updated_at TIMESTAMP NOT NULL,
|
|
updated_by UUID REFERENCES users(id)
|
|
)
|
|
`);
|
|
}
|
|
|
|
console.log('Migrations completed successfully!');
|
|
process.exit(0);
|
|
}
|
|
|
|
migrate().catch((err) => {
|
|
console.error('Migration failed:', err);
|
|
process.exit(1);
|
|
});
|