first commit
This commit is contained in:
33
backend/src/db/index.ts
Normal file
33
backend/src/db/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { drizzle as drizzleSqlite } from 'drizzle-orm/better-sqlite3';
|
||||
import { drizzle as drizzlePg } from 'drizzle-orm/node-postgres';
|
||||
import Database from 'better-sqlite3';
|
||||
import pg from 'pg';
|
||||
import * as schema from './schema.js';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { dirname } from 'path';
|
||||
|
||||
const dbType = process.env.DB_TYPE || 'sqlite';
|
||||
|
||||
let db: ReturnType<typeof drizzleSqlite> | ReturnType<typeof drizzlePg>;
|
||||
|
||||
if (dbType === 'postgres') {
|
||||
const pool = new pg.Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgresql://localhost:5432/spanglish',
|
||||
});
|
||||
db = drizzlePg(pool, { schema });
|
||||
} else {
|
||||
const dbPath = process.env.DATABASE_URL || './data/spanglish.db';
|
||||
|
||||
// Ensure data directory exists
|
||||
const dir = dirname(dbPath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const sqlite = new Database(dbPath);
|
||||
sqlite.pragma('journal_mode = WAL');
|
||||
db = drizzleSqlite(sqlite, { schema });
|
||||
}
|
||||
|
||||
export { db };
|
||||
export * from './schema.js';
|
||||
624
backend/src/db/migrate.ts
Normal file
624
backend/src/db/migrate.ts
Normal file
@@ -0,0 +1,624 @@
|
||||
import { db } from './index.js';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
const dbType = process.env.DB_TYPE || 'sqlite';
|
||||
|
||||
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,
|
||||
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,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
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 */ }
|
||||
|
||||
// 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 */ }
|
||||
|
||||
// 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)
|
||||
)
|
||||
`);
|
||||
|
||||
// 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
|
||||
)
|
||||
`);
|
||||
|
||||
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
|
||||
)
|
||||
`);
|
||||
} 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,
|
||||
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),
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
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 */ }
|
||||
|
||||
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,
|
||||
paid_at TIMESTAMP,
|
||||
paid_by_admin_id UUID,
|
||||
admin_note TEXT,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// 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)
|
||||
)
|
||||
`);
|
||||
|
||||
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
|
||||
)
|
||||
`);
|
||||
|
||||
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
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
console.log('Migrations completed successfully!');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
migrate().catch((err) => {
|
||||
console.error('Migration failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
518
backend/src/db/schema.ts
Normal file
518
backend/src/db/schema.ts
Normal file
@@ -0,0 +1,518 @@
|
||||
import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core';
|
||||
import { pgTable, uuid, varchar, text as pgText, timestamp, decimal, integer as pgInteger } 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'),
|
||||
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(),
|
||||
title: text('title').notNull(),
|
||||
titleEs: text('title_es'),
|
||||
description: text('description').notNull(),
|
||||
descriptionEs: text('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', 'cancelled', 'completed', 'archived'] }).notNull().default('draft'),
|
||||
bannerUrl: text('banner_url'),
|
||||
createdAt: text('created_at').notNull(),
|
||||
updatedAt: text('updated_at').notNull(),
|
||||
});
|
||||
|
||||
export const sqliteTickets = sqliteTable('tickets', {
|
||||
id: text('id').primaryKey(),
|
||||
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'] }).notNull().default('pending'),
|
||||
checkinAt: text('checkin_at'),
|
||||
qrCode: text('qr_code'),
|
||||
adminNote: text('admin_note'),
|
||||
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'] }).notNull().default('pending'),
|
||||
reference: text('reference'),
|
||||
userMarkedPaidAt: text('user_marked_paid_at'), // When user clicked "I Have Paid"
|
||||
paidAt: text('paid_at'),
|
||||
paidByAdminId: text('paid_by_admin_id'),
|
||||
adminNote: text('admin_note'), // Internal admin notes
|
||||
createdAt: text('created_at').notNull(),
|
||||
updatedAt: text('updated_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'),
|
||||
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'),
|
||||
// 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'),
|
||||
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(),
|
||||
});
|
||||
|
||||
export const sqliteEmailSettings = sqliteTable('email_settings', {
|
||||
id: text('id').primaryKey(),
|
||||
key: text('key').notNull().unique(),
|
||||
value: text('value').notNull(),
|
||||
updatedAt: text('updated_at').notNull(),
|
||||
});
|
||||
|
||||
// ==================== 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'),
|
||||
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(),
|
||||
title: varchar('title', { length: 255 }).notNull(),
|
||||
titleEs: varchar('title_es', { length: 255 }),
|
||||
description: pgText('description').notNull(),
|
||||
descriptionEs: pgText('description_es'),
|
||||
startDatetime: timestamp('start_datetime').notNull(),
|
||||
endDatetime: timestamp('end_datetime'),
|
||||
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 }),
|
||||
createdAt: timestamp('created_at').notNull(),
|
||||
updatedAt: timestamp('updated_at').notNull(),
|
||||
});
|
||||
|
||||
export const pgTickets = pgTable('tickets', {
|
||||
id: uuid('id').primaryKey(),
|
||||
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'),
|
||||
qrCode: varchar('qr_code', { length: 255 }),
|
||||
adminNote: pgText('admin_note'),
|
||||
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 }),
|
||||
userMarkedPaidAt: timestamp('user_marked_paid_at'),
|
||||
paidAt: timestamp('paid_at'),
|
||||
paidByAdminId: uuid('paid_by_admin_id'),
|
||||
adminNote: pgText('admin_note'),
|
||||
createdAt: timestamp('created_at').notNull(),
|
||||
updatedAt: timestamp('updated_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 }),
|
||||
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'),
|
||||
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 }),
|
||||
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(),
|
||||
});
|
||||
|
||||
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(),
|
||||
});
|
||||
|
||||
// Export the appropriate schema based on DB_TYPE
|
||||
export const users = dbType === 'postgres' ? pgUsers : sqliteUsers;
|
||||
export const events = dbType === 'postgres' ? pgEvents : sqliteEvents;
|
||||
export const tickets = dbType === 'postgres' ? pgTickets : sqliteTickets;
|
||||
export const payments = dbType === 'postgres' ? pgPayments : sqlitePayments;
|
||||
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 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;
|
||||
|
||||
// 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;
|
||||
Reference in New Issue
Block a user