diff --git a/backend/drizzle/0000_steady_wendell_vaughn.sql b/backend/drizzle/0000_steady_wendell_vaughn.sql
new file mode 100644
index 0000000..6b7de57
--- /dev/null
+++ b/backend/drizzle/0000_steady_wendell_vaughn.sql
@@ -0,0 +1,270 @@
+CREATE TABLE `audit_logs` (
+ `id` text PRIMARY KEY NOT NULL,
+ `user_id` text,
+ `action` text NOT NULL,
+ `target` text,
+ `target_id` text,
+ `details` text,
+ `timestamp` text NOT NULL,
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
+);
+--> statement-breakpoint
+CREATE TABLE `contacts` (
+ `id` text PRIMARY KEY NOT NULL,
+ `name` text NOT NULL,
+ `email` text NOT NULL,
+ `message` text NOT NULL,
+ `status` text DEFAULT 'new' NOT NULL,
+ `created_at` text NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE `email_logs` (
+ `id` text PRIMARY KEY NOT NULL,
+ `template_id` text,
+ `event_id` text,
+ `recipient_email` text NOT NULL,
+ `recipient_name` text,
+ `subject` text NOT NULL,
+ `body_html` text,
+ `status` text DEFAULT 'pending' NOT NULL,
+ `error_message` text,
+ `sent_at` text,
+ `sent_by` text,
+ `created_at` text NOT NULL,
+ FOREIGN KEY (`template_id`) REFERENCES `email_templates`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`event_id`) REFERENCES `events`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`sent_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
+);
+--> statement-breakpoint
+CREATE TABLE `email_settings` (
+ `id` text PRIMARY KEY NOT NULL,
+ `key` text NOT NULL,
+ `value` text NOT NULL,
+ `updated_at` text NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE `email_subscribers` (
+ `id` text PRIMARY KEY NOT NULL,
+ `email` text NOT NULL,
+ `name` text,
+ `status` text DEFAULT 'active' NOT NULL,
+ `created_at` text NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE `email_templates` (
+ `id` text PRIMARY KEY NOT NULL,
+ `name` text NOT NULL,
+ `slug` text NOT NULL,
+ `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 DEFAULT false NOT NULL,
+ `is_active` integer DEFAULT true NOT NULL,
+ `created_at` text NOT NULL,
+ `updated_at` text NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE `event_payment_overrides` (
+ `id` text PRIMARY KEY NOT NULL,
+ `event_id` text NOT NULL,
+ `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,
+ FOREIGN KEY (`event_id`) REFERENCES `events`(`id`) ON UPDATE no action ON DELETE no action
+);
+--> statement-breakpoint
+CREATE TABLE `events` (
+ `id` text PRIMARY KEY NOT NULL,
+ `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 DEFAULT 0 NOT NULL,
+ `currency` text DEFAULT 'PYG' NOT NULL,
+ `capacity` integer DEFAULT 50 NOT NULL,
+ `status` text DEFAULT 'draft' NOT NULL,
+ `banner_url` text,
+ `external_booking_enabled` integer DEFAULT false NOT NULL,
+ `external_booking_url` text,
+ `created_at` text NOT NULL,
+ `updated_at` text NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE `invoices` (
+ `id` text PRIMARY KEY NOT NULL,
+ `payment_id` text NOT NULL,
+ `user_id` text NOT NULL,
+ `invoice_number` text NOT NULL,
+ `ruc_number` text,
+ `legal_name` text,
+ `amount` real NOT NULL,
+ `currency` text DEFAULT 'PYG' NOT NULL,
+ `pdf_url` text,
+ `status` text DEFAULT 'generated' NOT NULL,
+ `created_at` text NOT NULL,
+ FOREIGN KEY (`payment_id`) REFERENCES `payments`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
+);
+--> statement-breakpoint
+CREATE TABLE `magic_link_tokens` (
+ `id` text PRIMARY KEY NOT NULL,
+ `user_id` text NOT NULL,
+ `token` text NOT NULL,
+ `type` text NOT NULL,
+ `expires_at` text NOT NULL,
+ `used_at` text,
+ `created_at` text NOT NULL,
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
+);
+--> statement-breakpoint
+CREATE TABLE `media` (
+ `id` text PRIMARY KEY NOT NULL,
+ `file_url` text NOT NULL,
+ `type` text NOT NULL,
+ `related_id` text,
+ `related_type` text,
+ `created_at` text NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE `payment_options` (
+ `id` text PRIMARY KEY NOT NULL,
+ `tpago_enabled` integer DEFAULT false NOT NULL,
+ `tpago_link` text,
+ `tpago_instructions` text,
+ `tpago_instructions_es` text,
+ `bank_transfer_enabled` integer DEFAULT false NOT NULL,
+ `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 DEFAULT true NOT NULL,
+ `cash_enabled` integer DEFAULT true NOT NULL,
+ `cash_instructions` text,
+ `cash_instructions_es` text,
+ `allow_duplicate_bookings` integer DEFAULT false NOT NULL,
+ `updated_at` text NOT NULL,
+ `updated_by` text,
+ FOREIGN KEY (`updated_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
+);
+--> statement-breakpoint
+CREATE TABLE `payments` (
+ `id` text PRIMARY KEY NOT NULL,
+ `ticket_id` text NOT NULL,
+ `provider` text NOT NULL,
+ `amount` real NOT NULL,
+ `currency` text DEFAULT 'PYG' NOT NULL,
+ `status` text DEFAULT 'pending' NOT NULL,
+ `reference` text,
+ `user_marked_paid_at` text,
+ `paid_at` text,
+ `paid_by_admin_id` text,
+ `admin_note` text,
+ `created_at` text NOT NULL,
+ `updated_at` text NOT NULL,
+ FOREIGN KEY (`ticket_id`) REFERENCES `tickets`(`id`) ON UPDATE no action ON DELETE no action
+);
+--> statement-breakpoint
+CREATE TABLE `site_settings` (
+ `id` text PRIMARY KEY NOT NULL,
+ `timezone` text DEFAULT 'America/Asuncion' NOT NULL,
+ `site_name` text DEFAULT 'Spanglish' NOT NULL,
+ `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,
+ `maintenance_mode` integer DEFAULT false NOT NULL,
+ `maintenance_message` text,
+ `maintenance_message_es` text,
+ `updated_at` text NOT NULL,
+ `updated_by` text,
+ FOREIGN KEY (`updated_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
+);
+--> statement-breakpoint
+CREATE TABLE `tickets` (
+ `id` text PRIMARY KEY NOT NULL,
+ `user_id` text NOT NULL,
+ `event_id` text NOT NULL,
+ `attendee_first_name` text NOT NULL,
+ `attendee_last_name` text,
+ `attendee_email` text,
+ `attendee_phone` text,
+ `attendee_ruc` text,
+ `preferred_language` text,
+ `status` text DEFAULT 'pending' NOT NULL,
+ `checkin_at` text,
+ `checked_in_by_admin_id` text,
+ `qr_code` text,
+ `admin_note` text,
+ `created_at` text NOT NULL,
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`event_id`) REFERENCES `events`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`checked_in_by_admin_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
+);
+--> statement-breakpoint
+CREATE TABLE `user_sessions` (
+ `id` text PRIMARY KEY NOT NULL,
+ `user_id` text NOT NULL,
+ `token` text NOT NULL,
+ `user_agent` text,
+ `ip_address` text,
+ `last_active_at` text NOT NULL,
+ `expires_at` text NOT NULL,
+ `created_at` text NOT NULL,
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
+);
+--> statement-breakpoint
+CREATE TABLE `users` (
+ `id` text PRIMARY KEY NOT NULL,
+ `email` text NOT NULL,
+ `password` text,
+ `name` text NOT NULL,
+ `phone` text,
+ `role` text DEFAULT 'user' NOT NULL,
+ `language_preference` text,
+ `is_claimed` integer DEFAULT true NOT NULL,
+ `google_id` text,
+ `ruc_number` text,
+ `account_status` text DEFAULT 'active' NOT NULL,
+ `created_at` text NOT NULL,
+ `updated_at` text NOT NULL
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX `email_settings_key_unique` ON `email_settings` (`key`);--> statement-breakpoint
+CREATE UNIQUE INDEX `email_subscribers_email_unique` ON `email_subscribers` (`email`);--> statement-breakpoint
+CREATE UNIQUE INDEX `email_templates_name_unique` ON `email_templates` (`name`);--> statement-breakpoint
+CREATE UNIQUE INDEX `email_templates_slug_unique` ON `email_templates` (`slug`);--> statement-breakpoint
+CREATE UNIQUE INDEX `invoices_invoice_number_unique` ON `invoices` (`invoice_number`);--> statement-breakpoint
+CREATE UNIQUE INDEX `magic_link_tokens_token_unique` ON `magic_link_tokens` (`token`);--> statement-breakpoint
+CREATE UNIQUE INDEX `user_sessions_token_unique` ON `user_sessions` (`token`);--> statement-breakpoint
+CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
\ No newline at end of file
diff --git a/backend/drizzle/meta/0000_snapshot.json b/backend/drizzle/meta/0000_snapshot.json
new file mode 100644
index 0000000..5d39f01
--- /dev/null
+++ b/backend/drizzle/meta/0000_snapshot.json
@@ -0,0 +1,1836 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "cb538639-03b3-45bd-b4f2-851ccc69411d",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "tables": {
+ "audit_logs": {
+ "name": "audit_logs",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "target": {
+ "name": "target",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "target_id": {
+ "name": "target_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "details": {
+ "name": "details",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "audit_logs_user_id_users_id_fk": {
+ "name": "audit_logs_user_id_users_id_fk",
+ "tableFrom": "audit_logs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "contacts": {
+ "name": "contacts",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'new'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "email_logs": {
+ "name": "email_logs",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "template_id": {
+ "name": "template_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "recipient_email": {
+ "name": "recipient_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "recipient_name": {
+ "name": "recipient_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "subject": {
+ "name": "subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "body_html": {
+ "name": "body_html",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pending'"
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sent_at": {
+ "name": "sent_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sent_by": {
+ "name": "sent_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "email_logs_template_id_email_templates_id_fk": {
+ "name": "email_logs_template_id_email_templates_id_fk",
+ "tableFrom": "email_logs",
+ "tableTo": "email_templates",
+ "columnsFrom": [
+ "template_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "email_logs_event_id_events_id_fk": {
+ "name": "email_logs_event_id_events_id_fk",
+ "tableFrom": "email_logs",
+ "tableTo": "events",
+ "columnsFrom": [
+ "event_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "email_logs_sent_by_users_id_fk": {
+ "name": "email_logs_sent_by_users_id_fk",
+ "tableFrom": "email_logs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "sent_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "email_settings": {
+ "name": "email_settings",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "email_settings_key_unique": {
+ "name": "email_settings_key_unique",
+ "columns": [
+ "key"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "email_subscribers": {
+ "name": "email_subscribers",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'active'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "email_subscribers_email_unique": {
+ "name": "email_subscribers_email_unique",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "email_templates": {
+ "name": "email_templates",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "subject": {
+ "name": "subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "subject_es": {
+ "name": "subject_es",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "body_html": {
+ "name": "body_html",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "body_html_es": {
+ "name": "body_html_es",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "body_text": {
+ "name": "body_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "body_text_es": {
+ "name": "body_text_es",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "variables": {
+ "name": "variables",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_system": {
+ "name": "is_system",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "email_templates_name_unique": {
+ "name": "email_templates_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ },
+ "email_templates_slug_unique": {
+ "name": "email_templates_slug_unique",
+ "columns": [
+ "slug"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "event_payment_overrides": {
+ "name": "event_payment_overrides",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "tpago_enabled": {
+ "name": "tpago_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tpago_link": {
+ "name": "tpago_link",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tpago_instructions": {
+ "name": "tpago_instructions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tpago_instructions_es": {
+ "name": "tpago_instructions_es",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bank_transfer_enabled": {
+ "name": "bank_transfer_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bank_name": {
+ "name": "bank_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bank_account_holder": {
+ "name": "bank_account_holder",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bank_account_number": {
+ "name": "bank_account_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bank_alias": {
+ "name": "bank_alias",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bank_phone": {
+ "name": "bank_phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bank_notes": {
+ "name": "bank_notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bank_notes_es": {
+ "name": "bank_notes_es",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lightning_enabled": {
+ "name": "lightning_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "cash_enabled": {
+ "name": "cash_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "cash_instructions": {
+ "name": "cash_instructions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "cash_instructions_es": {
+ "name": "cash_instructions_es",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "event_payment_overrides_event_id_events_id_fk": {
+ "name": "event_payment_overrides_event_id_events_id_fk",
+ "tableFrom": "event_payment_overrides",
+ "tableTo": "events",
+ "columnsFrom": [
+ "event_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "events": {
+ "name": "events",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title_es": {
+ "name": "title_es",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description_es": {
+ "name": "description_es",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "start_datetime": {
+ "name": "start_datetime",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "end_datetime": {
+ "name": "end_datetime",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "location": {
+ "name": "location",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "location_url": {
+ "name": "location_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "price": {
+ "name": "price",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "currency": {
+ "name": "currency",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'PYG'"
+ },
+ "capacity": {
+ "name": "capacity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 50
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'draft'"
+ },
+ "banner_url": {
+ "name": "banner_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "external_booking_enabled": {
+ "name": "external_booking_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "external_booking_url": {
+ "name": "external_booking_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "invoices": {
+ "name": "invoices",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "payment_id": {
+ "name": "payment_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "invoice_number": {
+ "name": "invoice_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "ruc_number": {
+ "name": "ruc_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "legal_name": {
+ "name": "legal_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "amount": {
+ "name": "amount",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "currency": {
+ "name": "currency",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'PYG'"
+ },
+ "pdf_url": {
+ "name": "pdf_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'generated'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "invoices_invoice_number_unique": {
+ "name": "invoices_invoice_number_unique",
+ "columns": [
+ "invoice_number"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "invoices_payment_id_payments_id_fk": {
+ "name": "invoices_payment_id_payments_id_fk",
+ "tableFrom": "invoices",
+ "tableTo": "payments",
+ "columnsFrom": [
+ "payment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "invoices_user_id_users_id_fk": {
+ "name": "invoices_user_id_users_id_fk",
+ "tableFrom": "invoices",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "magic_link_tokens": {
+ "name": "magic_link_tokens",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "used_at": {
+ "name": "used_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "magic_link_tokens_token_unique": {
+ "name": "magic_link_tokens_token_unique",
+ "columns": [
+ "token"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "magic_link_tokens_user_id_users_id_fk": {
+ "name": "magic_link_tokens_user_id_users_id_fk",
+ "tableFrom": "magic_link_tokens",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "media": {
+ "name": "media",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "file_url": {
+ "name": "file_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "related_id": {
+ "name": "related_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "related_type": {
+ "name": "related_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "payment_options": {
+ "name": "payment_options",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "tpago_enabled": {
+ "name": "tpago_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "tpago_link": {
+ "name": "tpago_link",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tpago_instructions": {
+ "name": "tpago_instructions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tpago_instructions_es": {
+ "name": "tpago_instructions_es",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bank_transfer_enabled": {
+ "name": "bank_transfer_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "bank_name": {
+ "name": "bank_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bank_account_holder": {
+ "name": "bank_account_holder",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bank_account_number": {
+ "name": "bank_account_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bank_alias": {
+ "name": "bank_alias",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bank_phone": {
+ "name": "bank_phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bank_notes": {
+ "name": "bank_notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bank_notes_es": {
+ "name": "bank_notes_es",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lightning_enabled": {
+ "name": "lightning_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "cash_enabled": {
+ "name": "cash_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "cash_instructions": {
+ "name": "cash_instructions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "cash_instructions_es": {
+ "name": "cash_instructions_es",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "allow_duplicate_bookings": {
+ "name": "allow_duplicate_bookings",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "payment_options_updated_by_users_id_fk": {
+ "name": "payment_options_updated_by_users_id_fk",
+ "tableFrom": "payment_options",
+ "tableTo": "users",
+ "columnsFrom": [
+ "updated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "payments": {
+ "name": "payments",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "ticket_id": {
+ "name": "ticket_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "amount": {
+ "name": "amount",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "currency": {
+ "name": "currency",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'PYG'"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pending'"
+ },
+ "reference": {
+ "name": "reference",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "user_marked_paid_at": {
+ "name": "user_marked_paid_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "paid_at": {
+ "name": "paid_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "paid_by_admin_id": {
+ "name": "paid_by_admin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "admin_note": {
+ "name": "admin_note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "payments_ticket_id_tickets_id_fk": {
+ "name": "payments_ticket_id_tickets_id_fk",
+ "tableFrom": "payments",
+ "tableTo": "tickets",
+ "columnsFrom": [
+ "ticket_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "site_settings": {
+ "name": "site_settings",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'America/Asuncion'"
+ },
+ "site_name": {
+ "name": "site_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'Spanglish'"
+ },
+ "site_description": {
+ "name": "site_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "site_description_es": {
+ "name": "site_description_es",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "contact_email": {
+ "name": "contact_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "contact_phone": {
+ "name": "contact_phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "facebook_url": {
+ "name": "facebook_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "instagram_url": {
+ "name": "instagram_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "twitter_url": {
+ "name": "twitter_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "linkedin_url": {
+ "name": "linkedin_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "maintenance_mode": {
+ "name": "maintenance_mode",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "maintenance_message": {
+ "name": "maintenance_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "maintenance_message_es": {
+ "name": "maintenance_message_es",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "site_settings_updated_by_users_id_fk": {
+ "name": "site_settings_updated_by_users_id_fk",
+ "tableFrom": "site_settings",
+ "tableTo": "users",
+ "columnsFrom": [
+ "updated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "tickets": {
+ "name": "tickets",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "attendee_first_name": {
+ "name": "attendee_first_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "attendee_last_name": {
+ "name": "attendee_last_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "attendee_email": {
+ "name": "attendee_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "attendee_phone": {
+ "name": "attendee_phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "attendee_ruc": {
+ "name": "attendee_ruc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "preferred_language": {
+ "name": "preferred_language",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pending'"
+ },
+ "checkin_at": {
+ "name": "checkin_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "checked_in_by_admin_id": {
+ "name": "checked_in_by_admin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "qr_code": {
+ "name": "qr_code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "admin_note": {
+ "name": "admin_note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tickets_user_id_users_id_fk": {
+ "name": "tickets_user_id_users_id_fk",
+ "tableFrom": "tickets",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "tickets_event_id_events_id_fk": {
+ "name": "tickets_event_id_events_id_fk",
+ "tableFrom": "tickets",
+ "tableTo": "events",
+ "columnsFrom": [
+ "event_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "tickets_checked_in_by_admin_id_users_id_fk": {
+ "name": "tickets_checked_in_by_admin_id_users_id_fk",
+ "tableFrom": "tickets",
+ "tableTo": "users",
+ "columnsFrom": [
+ "checked_in_by_admin_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "user_sessions": {
+ "name": "user_sessions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_active_at": {
+ "name": "last_active_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "user_sessions_token_unique": {
+ "name": "user_sessions_token_unique",
+ "columns": [
+ "token"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "user_sessions_user_id_users_id_fk": {
+ "name": "user_sessions_user_id_users_id_fk",
+ "tableFrom": "user_sessions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "users": {
+ "name": "users",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'user'"
+ },
+ "language_preference": {
+ "name": "language_preference",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_claimed": {
+ "name": "is_claimed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "google_id": {
+ "name": "google_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "ruc_number": {
+ "name": "ruc_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "account_status": {
+ "name": "account_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'active'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ }
+ },
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json
new file mode 100644
index 0000000..6549e1b
--- /dev/null
+++ b/backend/drizzle/meta/_journal.json
@@ -0,0 +1,13 @@
+{
+ "version": "7",
+ "dialect": "sqlite",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "6",
+ "when": 1769975033554,
+ "tag": "0000_steady_wendell_vaughn",
+ "breakpoints": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/backend/src/db/index.ts b/backend/src/db/index.ts
index 2d4e359..ca31dd6 100644
--- a/backend/src/db/index.ts
+++ b/backend/src/db/index.ts
@@ -1,3 +1,4 @@
+import 'dotenv/config';
import { drizzle as drizzleSqlite } from 'drizzle-orm/better-sqlite3';
import { drizzle as drizzlePg } from 'drizzle-orm/node-postgres';
import Database from 'better-sqlite3';
@@ -29,5 +30,51 @@ if (dbType === 'postgres') {
db = drizzleSqlite(sqlite, { schema });
}
-export { db };
+// ==================== Database Compatibility Helpers ====================
+// These functions abstract the differences between SQLite and PostgreSQL Drizzle drivers:
+// - SQLite uses .get() for single result, .all() for multiple
+// - PostgreSQL returns arrays directly (no .get()/.all() methods)
+
+/**
+ * Get a single result from a query (works with both SQLite and PostgreSQL)
+ * @param query - A Drizzle query builder (e.g., db.select().from(table).where(...))
+ * @returns The first result or null
+ */
+export async function dbGet(query: any): Promise {
+ if (dbType === 'postgres') {
+ const results = await query;
+ return results[0] || null;
+ }
+ // SQLite - use .get()
+ return query.get() || null;
+}
+
+/**
+ * Get all results from a query (works with both SQLite and PostgreSQL)
+ * @param query - A Drizzle query builder (e.g., db.select().from(table).where(...))
+ * @returns Array of results
+ */
+export async function dbAll(query: any): Promise {
+ if (dbType === 'postgres') {
+ return await query;
+ }
+ // SQLite - use .all()
+ return query.all();
+}
+
+/**
+ * Check if using PostgreSQL
+ */
+export function isPostgres(): boolean {
+ return dbType === 'postgres';
+}
+
+/**
+ * Check if using SQLite
+ */
+export function isSqlite(): boolean {
+ return dbType === 'sqlite';
+}
+
+export { db, dbType };
export * from './schema.js';
diff --git a/backend/src/db/migrate.ts b/backend/src/db/migrate.ts
index 0cf85c1..1775104 100644
--- a/backend/src/db/migrate.ts
+++ b/backend/src/db/migrate.ts
@@ -1,7 +1,10 @@
+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...');
@@ -384,6 +387,23 @@ async function migrate() {
updated_by TEXT REFERENCES users(id)
)
`);
+
+ // 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
+ )
+ `);
} else {
// PostgreSQL migrations
await (db as any).execute(sql`
@@ -716,6 +736,23 @@ async function migrate() {
updated_by UUID REFERENCES users(id)
)
`);
+
+ // 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
+ )
+ `);
}
console.log('Migrations completed successfully!');
diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts
index 1c0851e..30762c8 100644
--- a/backend/src/db/schema.ts
+++ b/backend/src/db/schema.ts
@@ -249,6 +249,21 @@ export const sqliteEmailSettings = sqliteTable('email_settings', {
updatedAt: text('updated_at').notNull(),
});
+// 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(),
+});
+
// Site Settings table for global website configuration
export const sqliteSiteSettings = sqliteTable('site_settings', {
id: text('id').primaryKey(),
@@ -512,6 +527,21 @@ export const pgEmailSettings = pgTable('email_settings', {
updatedAt: timestamp('updated_at').notNull(),
});
+// 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(),
+});
+
// Site Settings table for global website configuration
export const pgSiteSettings = pgTable('site_settings', {
id: uuid('id').primaryKey(),
@@ -556,6 +586,7 @@ export const magicLinkTokens = dbType === 'postgres' ? pgMagicLinkTokens : sqlit
export const userSessions = dbType === 'postgres' ? pgUserSessions : sqliteUserSessions;
export const invoices = dbType === 'postgres' ? pgInvoices : sqliteInvoices;
export const siteSettings = dbType === 'postgres' ? pgSiteSettings : sqliteSiteSettings;
+export const legalPages = dbType === 'postgres' ? pgLegalPages : sqliteLegalPages;
// Type exports
export type User = typeof sqliteUsers.$inferSelect;
@@ -584,3 +615,5 @@ 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;
diff --git a/backend/src/index.ts b/backend/src/index.ts
index 1acc953..ac85087 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -20,6 +20,7 @@ import emailsRoutes from './routes/emails.js';
import paymentOptionsRoutes from './routes/payment-options.js';
import dashboardRoutes from './routes/dashboard.js';
import siteSettingsRoutes from './routes/site-settings.js';
+import legalPagesRoutes from './routes/legal-pages.js';
import emailService from './lib/email.js';
const app = new Hono();
@@ -1714,6 +1715,7 @@ app.route('/api/emails', emailsRoutes);
app.route('/api/payment-options', paymentOptionsRoutes);
app.route('/api/dashboard', dashboardRoutes);
app.route('/api/site-settings', siteSettingsRoutes);
+app.route('/api/legal-pages', legalPagesRoutes);
// 404 handler
app.notFound((c) => {
diff --git a/backend/src/lib/auth.ts b/backend/src/lib/auth.ts
index 925c09f..2d9a631 100644
--- a/backend/src/lib/auth.ts
+++ b/backend/src/lib/auth.ts
@@ -3,7 +3,7 @@ import * as argon2 from 'argon2';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import { Context } from 'hono';
-import { db, users, magicLinkTokens, userSessions } from '../db/index.js';
+import { db, dbGet, dbAll, users, magicLinkTokens, userSessions } from '../db/index.js';
import { eq, and, gt } from 'drizzle-orm';
import { generateId, getNow } from './utils.js';
@@ -72,16 +72,17 @@ export async function verifyMagicLinkToken(
): Promise<{ valid: boolean; userId?: string; error?: string }> {
const now = getNow();
- const tokenRecord = await (db as any)
- .select()
- .from(magicLinkTokens)
- .where(
- and(
- eq((magicLinkTokens as any).token, token),
- eq((magicLinkTokens as any).type, type)
+ const tokenRecord = await dbGet(
+ (db as any)
+ .select()
+ .from(magicLinkTokens)
+ .where(
+ and(
+ eq((magicLinkTokens as any).token, token),
+ eq((magicLinkTokens as any).type, type)
+ )
)
- )
- .get();
+ );
if (!tokenRecord) {
return { valid: false, error: 'Invalid token' };
@@ -132,16 +133,17 @@ export async function createUserSession(
export async function getUserSessions(userId: string) {
const now = getNow();
- return (db as any)
- .select()
- .from(userSessions)
- .where(
- and(
- eq((userSessions as any).userId, userId),
- gt((userSessions as any).expiresAt, now)
+ return dbAll(
+ (db as any)
+ .select()
+ .from(userSessions)
+ .where(
+ and(
+ eq((userSessions as any).userId, userId),
+ gt((userSessions as any).expiresAt, now)
+ )
)
- )
- .all();
+ );
}
// Invalidate a specific session
@@ -208,7 +210,7 @@ export async function verifyToken(token: string): Promise {
}
}
-export async function getAuthUser(c: Context) {
+export async function getAuthUser(c: Context): Promise {
const authHeader = c.req.header('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return null;
@@ -221,7 +223,9 @@ export async function getAuthUser(c: Context) {
return null;
}
- const user = await (db as any).select().from(users).where(eq((users as any).id, payload.sub)).get();
+ const user = await dbGet(
+ (db as any).select().from(users).where(eq((users as any).id, payload.sub))
+ );
return user || null;
}
@@ -243,6 +247,8 @@ export function requireAuth(roles?: string[]) {
}
export async function isFirstUser(): Promise {
- const result = await (db as any).select().from(users).limit(1).all();
+ const result = await dbAll(
+ (db as any).select().from(users).limit(1)
+ );
return !result || result.length === 0;
}
diff --git a/backend/src/lib/email.ts b/backend/src/lib/email.ts
index 047958b..a49fa58 100644
--- a/backend/src/lib/email.ts
+++ b/backend/src/lib/email.ts
@@ -1,10 +1,9 @@
// Email service for Spanglish platform
// Supports multiple email providers: Resend, SMTP (Nodemailer)
-import { db, emailTemplates, emailLogs, events, tickets, payments, users, paymentOptions, eventPaymentOverrides } from '../db/index.js';
+import { db, dbGet, dbAll, emailTemplates, emailLogs, events, tickets, payments, users, paymentOptions, eventPaymentOverrides } from '../db/index.js';
import { eq, and } from 'drizzle-orm';
-import { nanoid } from 'nanoid';
-import { getNow } from './utils.js';
+import { getNow, generateId } from './utils.js';
import {
replaceTemplateVariables,
wrapInBaseTemplate,
@@ -362,11 +361,12 @@ export const emailService = {
* Get a template by slug
*/
async getTemplate(slug: string): Promise {
- const template = await (db as any)
- .select()
- .from(emailTemplates)
- .where(eq((emailTemplates as any).slug, slug))
- .get();
+ const template = await dbGet(
+ (db as any)
+ .select()
+ .from(emailTemplates)
+ .where(eq((emailTemplates as any).slug, slug))
+ );
return template || null;
},
@@ -385,7 +385,7 @@ export const emailService = {
console.log(`[Email] Creating template: ${template.name}`);
await (db as any).insert(emailTemplates).values({
- id: nanoid(),
+ id: generateId(),
name: template.name,
slug: template.slug,
subject: template.subject,
@@ -470,7 +470,7 @@ export const emailService = {
const finalBodyText = bodyText ? replaceTemplateVariables(bodyText, allVariables) : undefined;
// Create log entry
- const logId = nanoid();
+ const logId = generateId();
const now = getNow();
await (db as any).insert(emailLogs).values({
@@ -525,21 +525,23 @@ export const emailService = {
*/
async sendBookingConfirmation(ticketId: string): Promise<{ success: boolean; error?: string }> {
// Get ticket with event info
- const ticket = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).id, ticketId))
- .get();
+ const ticket = await dbGet(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).id, ticketId))
+ );
if (!ticket) {
return { success: false, error: 'Ticket not found' };
}
- const event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get();
+ const event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ );
if (!event) {
return { success: false, error: 'Event not found' };
@@ -580,31 +582,34 @@ export const emailService = {
*/
async sendPaymentReceipt(paymentId: string): Promise<{ success: boolean; error?: string }> {
// Get payment with ticket and event info
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).id, paymentId))
- .get();
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).id, paymentId))
+ );
if (!payment) {
return { success: false, error: 'Payment not found' };
}
- const ticket = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).id, payment.ticketId))
- .get();
+ const ticket = await dbGet(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).id, payment.ticketId))
+ );
if (!ticket) {
return { success: false, error: 'Ticket not found' };
}
- const event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get();
+ const event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ );
if (!event) {
return { success: false, error: 'Event not found' };
@@ -643,17 +648,19 @@ export const emailService = {
*/
async getPaymentConfig(eventId: string): Promise> {
// Get global options
- const globalOptions = await (db as any)
- .select()
- .from(paymentOptions)
- .get();
+ const globalOptions = await dbGet(
+ (db as any)
+ .select()
+ .from(paymentOptions)
+ );
// Get event overrides
- const overrides = await (db as any)
- .select()
- .from(eventPaymentOverrides)
- .where(eq((eventPaymentOverrides as any).eventId, eventId))
- .get();
+ const overrides = await dbGet(
+ (db as any)
+ .select()
+ .from(eventPaymentOverrides)
+ .where(eq((eventPaymentOverrides as any).eventId, eventId))
+ );
// Defaults
const defaults = {
@@ -696,33 +703,36 @@ export const emailService = {
*/
async sendPaymentInstructions(ticketId: string): Promise<{ success: boolean; error?: string }> {
// Get ticket
- const ticket = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).id, ticketId))
- .get();
+ const ticket = await dbGet(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).id, ticketId))
+ );
if (!ticket) {
return { success: false, error: 'Ticket not found' };
}
// Get event
- const event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get();
+ const event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ );
if (!event) {
return { success: false, error: 'Event not found' };
}
// Get payment
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).ticketId, ticketId))
- .get();
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).ticketId, ticketId))
+ );
if (!payment) {
return { success: false, error: 'Payment not found' };
@@ -797,33 +807,36 @@ export const emailService = {
*/
async sendPaymentRejectionEmail(paymentId: string): Promise<{ success: boolean; error?: string }> {
// Get payment
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).id, paymentId))
- .get();
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).id, paymentId))
+ );
if (!payment) {
return { success: false, error: 'Payment not found' };
}
// Get ticket
- const ticket = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).id, payment.ticketId))
- .get();
+ const ticket = await dbGet(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).id, payment.ticketId))
+ );
if (!ticket) {
return { success: false, error: 'Ticket not found' };
}
// Get event
- const event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get();
+ const event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ );
if (!event) {
return { success: false, error: 'Event not found' };
@@ -872,11 +885,12 @@ export const emailService = {
const { eventId, templateSlug, customVariables = {}, recipientFilter = 'confirmed', sentBy } = params;
// Get event
- const event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, eventId))
- .get();
+ const event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, eventId))
+ );
if (!event) {
return { success: false, sentCount: 0, failedCount: 0, errors: ['Event not found'] };
@@ -897,7 +911,7 @@ export const emailService = {
);
}
- const eventTickets = await ticketQuery.all();
+ const eventTickets = await dbAll(ticketQuery);
if (eventTickets.length === 0) {
return { success: true, sentCount: 0, failedCount: 0, errors: ['No recipients found'] };
@@ -971,7 +985,7 @@ export const emailService = {
const finalBodyHtml = wrapInBaseTemplate(bodyHtml, allVariables);
// Create log entry
- const logId = nanoid();
+ const logId = generateId();
const now = getNow();
await (db as any).insert(emailLogs).values({
diff --git a/backend/src/lib/utils.ts b/backend/src/lib/utils.ts
index c1372c9..a116b91 100644
--- a/backend/src/lib/utils.ts
+++ b/backend/src/lib/utils.ts
@@ -1,15 +1,71 @@
import { nanoid } from 'nanoid';
+import { randomUUID } from 'crypto';
+/**
+ * Get database type (reads env var each time to handle module loading order)
+ */
+function getDbType(): string {
+ return process.env.DB_TYPE || 'sqlite';
+}
+
+/**
+ * Generate a unique ID appropriate for the database type.
+ * - SQLite: returns nanoid (21-char alphanumeric)
+ * - PostgreSQL: returns UUID v4
+ */
export function generateId(): string {
- return nanoid(21);
+ return getDbType() === 'postgres' ? randomUUID() : nanoid(21);
}
export function generateTicketCode(): string {
return `TKT-${nanoid(8).toUpperCase()}`;
}
-export function getNow(): string {
- return new Date().toISOString();
+/**
+ * Get current timestamp in the format appropriate for the database type.
+ * - SQLite: returns ISO string
+ * - PostgreSQL: returns Date object
+ */
+export function getNow(): string | Date {
+ const now = new Date();
+ return getDbType() === 'postgres' ? now : now.toISOString();
+}
+
+/**
+ * Convert a date value to the appropriate format for the database type.
+ * - SQLite: returns ISO string
+ * - PostgreSQL: returns Date object
+ */
+export function toDbDate(date: Date | string): string | Date {
+ const d = typeof date === 'string' ? new Date(date) : date;
+ return getDbType() === 'postgres' ? d : d.toISOString();
+}
+
+/**
+ * Convert a boolean value to the appropriate format for the database type.
+ * - SQLite: returns boolean (true/false) for mode: 'boolean'
+ * - PostgreSQL: returns integer (1/0) for pgInteger columns
+ */
+export function toDbBool(value: boolean): boolean | number {
+ return getDbType() === 'postgres' ? (value ? 1 : 0) : value;
+}
+
+/**
+ * Convert all boolean values in an object to the appropriate database format.
+ * Useful for converting request data before database insert/update.
+ */
+export function convertBooleansForDb>(obj: T): T {
+ if (getDbType() !== 'postgres') {
+ return obj; // SQLite handles booleans automatically
+ }
+
+ const result = { ...obj };
+ for (const key in result) {
+ if (typeof result[key] === 'boolean') {
+ (result as any)[key] = result[key] ? 1 : 0;
+ }
+ }
+ return result;
}
export function formatCurrency(amount: number, currency: string = 'PYG'): string {
diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts
index e870e8a..c88fa7d 100644
--- a/backend/src/routes/admin.ts
+++ b/backend/src/routes/admin.ts
@@ -1,5 +1,5 @@
import { Hono } from 'hono';
-import { db, users, events, tickets, payments, contacts, emailSubscribers } from '../db/index.js';
+import { db, dbGet, dbAll, users, events, tickets, payments, contacts, emailSubscribers } from '../db/index.js';
import { eq, and, gte, sql, desc } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { getNow } from '../lib/utils.js';
@@ -11,74 +11,84 @@ adminRouter.get('/dashboard', requireAuth(['admin', 'organizer']), async (c) =>
const now = getNow();
// Get upcoming events
- const upcomingEvents = await (db as any)
- .select()
- .from(events)
- .where(
- and(
- eq((events as any).status, 'published'),
- gte((events as any).startDatetime, now)
+ const upcomingEvents = await dbAll(
+ (db as any)
+ .select()
+ .from(events)
+ .where(
+ and(
+ eq((events as any).status, 'published'),
+ gte((events as any).startDatetime, now)
+ )
)
- )
- .orderBy((events as any).startDatetime)
- .limit(5)
- .all();
+ .orderBy((events as any).startDatetime)
+ .limit(5)
+ );
// Get recent tickets
- const recentTickets = await (db as any)
- .select()
- .from(tickets)
- .orderBy(desc((tickets as any).createdAt))
- .limit(10)
- .all();
+ const recentTickets = await dbAll(
+ (db as any)
+ .select()
+ .from(tickets)
+ .orderBy(desc((tickets as any).createdAt))
+ .limit(10)
+ );
// Get total stats
- const totalUsers = await (db as any)
- .select({ count: sql`count(*)` })
- .from(users)
- .get();
+ const totalUsers = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(users)
+ );
- const totalEvents = await (db as any)
- .select({ count: sql`count(*)` })
- .from(events)
- .get();
+ const totalEvents = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(events)
+ );
- const totalTickets = await (db as any)
- .select({ count: sql`count(*)` })
- .from(tickets)
- .get();
+ const totalTickets = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(tickets)
+ );
- const confirmedTickets = await (db as any)
- .select({ count: sql`count(*)` })
- .from(tickets)
- .where(eq((tickets as any).status, 'confirmed'))
- .get();
+ const confirmedTickets = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(tickets)
+ .where(eq((tickets as any).status, 'confirmed'))
+ );
- const pendingPayments = await (db as any)
- .select({ count: sql`count(*)` })
- .from(payments)
- .where(eq((payments as any).status, 'pending'))
- .get();
+ const pendingPayments = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(payments)
+ .where(eq((payments as any).status, 'pending'))
+ );
- const paidPayments = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).status, 'paid'))
- .all();
+ const paidPayments = await dbAll(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).status, 'paid'))
+ );
const totalRevenue = paidPayments.reduce((sum: number, p: any) => sum + (p.amount || 0), 0);
- const newContacts = await (db as any)
- .select({ count: sql`count(*)` })
- .from(contacts)
- .where(eq((contacts as any).status, 'new'))
- .get();
+ const newContacts = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(contacts)
+ .where(eq((contacts as any).status, 'new'))
+ );
- const totalSubscribers = await (db as any)
- .select({ count: sql`count(*)` })
- .from(emailSubscribers)
- .where(eq((emailSubscribers as any).status, 'active'))
- .get();
+ const totalSubscribers = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(emailSubscribers)
+ .where(eq((emailSubscribers as any).status, 'active'))
+ );
return c.json({
dashboard: {
@@ -101,37 +111,40 @@ adminRouter.get('/dashboard', requireAuth(['admin', 'organizer']), async (c) =>
// Get analytics data (admin)
adminRouter.get('/analytics', requireAuth(['admin']), async (c) => {
// Get events with ticket counts
- const allEvents = await (db as any).select().from(events).all();
+ const allEvents = await dbAll((db as any).select().from(events));
const eventStats = await Promise.all(
allEvents.map(async (event: any) => {
- const ticketCount = await (db as any)
- .select({ count: sql`count(*)` })
- .from(tickets)
- .where(eq((tickets as any).eventId, event.id))
- .get();
+ const ticketCount = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(tickets)
+ .where(eq((tickets as any).eventId, event.id))
+ );
- const confirmedCount = await (db as any)
- .select({ count: sql`count(*)` })
- .from(tickets)
- .where(
- and(
- eq((tickets as any).eventId, event.id),
- eq((tickets as any).status, 'confirmed')
+ const confirmedCount = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(tickets)
+ .where(
+ and(
+ eq((tickets as any).eventId, event.id),
+ eq((tickets as any).status, 'confirmed')
+ )
)
- )
- .get();
+ );
- const checkedInCount = await (db as any)
- .select({ count: sql`count(*)` })
- .from(tickets)
- .where(
- and(
- eq((tickets as any).eventId, event.id),
- eq((tickets as any).status, 'checked_in')
+ const checkedInCount = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(tickets)
+ .where(
+ and(
+ eq((tickets as any).eventId, event.id),
+ eq((tickets as any).status, 'checked_in')
+ )
)
- )
- .get();
+ );
return {
id: event.id,
@@ -163,28 +176,31 @@ adminRouter.get('/export/tickets', requireAuth(['admin']), async (c) => {
query = query.where(eq((tickets as any).eventId, eventId));
}
- const ticketList = await query.all();
+ const ticketList = await dbAll(query);
// Get user and event details for each ticket
const enrichedTickets = await Promise.all(
ticketList.map(async (ticket: any) => {
- const user = await (db as any)
- .select()
- .from(users)
- .where(eq((users as any).id, ticket.userId))
- .get();
+ const user = await dbGet(
+ (db as any)
+ .select()
+ .from(users)
+ .where(eq((users as any).id, ticket.userId))
+ );
- const event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get();
+ const event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ );
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).ticketId, ticket.id))
- .get();
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).ticketId, ticket.id))
+ );
return {
ticketId: ticket.id,
@@ -215,24 +231,26 @@ adminRouter.get('/export/financial', requireAuth(['admin']), async (c) => {
// Get all payments
let query = (db as any).select().from(payments);
- const allPayments = await query.all();
+ const allPayments = await dbAll(query);
// Enrich with event and ticket data
const enrichedPayments = await Promise.all(
allPayments.map(async (payment: any) => {
- const ticket = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).id, payment.ticketId))
- .get();
+ const ticket = await dbGet(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).id, payment.ticketId))
+ );
if (!ticket) return null;
- const event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get();
+ const event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ );
// Apply filters
if (eventId && ticket.eventId !== eventId) return null;
diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts
index 45464a7..ad34c63 100644
--- a/backend/src/routes/auth.ts
+++ b/backend/src/routes/auth.ts
@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
-import { db, users, magicLinkTokens, User } from '../db/index.js';
+import { db, dbGet, users, magicLinkTokens, User } from '../db/index.js';
import { eq } from 'drizzle-orm';
import {
hashPassword,
@@ -16,7 +16,7 @@ import {
invalidateAllUserSessions,
requireAuth,
} from '../lib/auth.js';
-import { generateId, getNow } from '../lib/utils.js';
+import { generateId, getNow, toDbBool } from '../lib/utils.js';
import { sendEmail } from '../lib/email.js';
// User type that includes all fields (some added in schema updates)
@@ -121,7 +121,9 @@ auth.post('/register', zValidator('json', registerSchema), async (c) => {
}
// Check if email exists
- const existing = await (db as any).select().from(users).where(eq((users as any).email, data.email)).get();
+ const existing = await dbGet(
+ (db as any).select().from(users).where(eq((users as any).email, data.email))
+ );
if (existing) {
// If user exists but is unclaimed, allow claiming
if (!existing.isClaimed || existing.accountStatus === 'unclaimed') {
@@ -149,7 +151,7 @@ auth.post('/register', zValidator('json', registerSchema), async (c) => {
phone: data.phone || null,
role: firstUser ? 'admin' : 'user',
languagePreference: data.languagePreference || null,
- isClaimed: true,
+ isClaimed: toDbBool(true),
googleId: null,
rucNumber: null,
accountStatus: 'active',
@@ -189,7 +191,9 @@ auth.post('/login', zValidator('json', loginSchema), async (c) => {
}, 429);
}
- const user = await (db as any).select().from(users).where(eq((users as any).email, data.email)).get();
+ const user = await dbGet(
+ (db as any).select().from(users).where(eq((users as any).email, data.email))
+ );
if (!user) {
recordFailedAttempt(data.email);
return c.json({ error: 'Invalid credentials' }, 401);
@@ -243,7 +247,9 @@ auth.post('/login', zValidator('json', loginSchema), async (c) => {
auth.post('/magic-link/request', zValidator('json', magicLinkRequestSchema), async (c) => {
const { email } = c.req.valid('json');
- const user = await (db as any).select().from(users).where(eq((users as any).email, email)).get();
+ const user = await dbGet(
+ (db as any).select().from(users).where(eq((users as any).email, email))
+ );
if (!user) {
// Don't reveal if email exists
@@ -288,7 +294,9 @@ auth.post('/magic-link/verify', zValidator('json', magicLinkVerifySchema), async
return c.json({ error: verification.error }, 400);
}
- const user = await (db as any).select().from(users).where(eq((users as any).id, verification.userId)).get();
+ const user = await dbGet(
+ (db as any).select().from(users).where(eq((users as any).id, verification.userId))
+ );
if (!user || user.accountStatus === 'suspended') {
return c.json({ error: 'Invalid token' }, 400);
@@ -317,7 +325,9 @@ auth.post('/magic-link/verify', zValidator('json', magicLinkVerifySchema), async
auth.post('/password-reset/request', zValidator('json', passwordResetRequestSchema), async (c) => {
const { email } = c.req.valid('json');
- const user = await (db as any).select().from(users).where(eq((users as any).email, email)).get();
+ const user = await dbGet(
+ (db as any).select().from(users).where(eq((users as any).email, email))
+ );
if (!user) {
// Don't reveal if email exists
@@ -389,7 +399,9 @@ auth.post('/password-reset/confirm', zValidator('json', passwordResetSchema), as
auth.post('/claim-account/request', zValidator('json', magicLinkRequestSchema), async (c) => {
const { email } = c.req.valid('json');
- const user = await (db as any).select().from(users).where(eq((users as any).email, email)).get();
+ const user = await dbGet(
+ (db as any).select().from(users).where(eq((users as any).email, email))
+ );
if (!user) {
return c.json({ message: 'If an unclaimed account exists with this email, a claim link has been sent.' });
@@ -439,7 +451,7 @@ auth.post('/claim-account/confirm', zValidator('json', claimAccountSchema), asyn
const now = getNow();
const updates: Record = {
- isClaimed: true,
+ isClaimed: toDbBool(true),
accountStatus: 'active',
updatedAt: now,
};
@@ -461,7 +473,9 @@ auth.post('/claim-account/confirm', zValidator('json', claimAccountSchema), asyn
.set(updates)
.where(eq((users as any).id, verification.userId));
- const user = await (db as any).select().from(users).where(eq((users as any).id, verification.userId)).get();
+ const user = await dbGet(
+ (db as any).select().from(users).where(eq((users as any).id, verification.userId))
+ );
const authToken = await createToken(user.id, user.email, user.role);
const refreshToken = await createRefreshToken(user.id);
@@ -510,11 +524,15 @@ auth.post('/google', zValidator('json', googleAuthSchema), async (c) => {
const { sub: googleId, email, name } = googleData;
// Check if user exists by email or google_id
- let user = await (db as any).select().from(users).where(eq((users as any).email, email)).get();
+ let user = await dbGet(
+ (db as any).select().from(users).where(eq((users as any).email, email))
+ );
if (!user) {
// Check by google_id
- user = await (db as any).select().from(users).where(eq((users as any).googleId, googleId)).get();
+ user = await dbGet(
+ (db as any).select().from(users).where(eq((users as any).googleId, googleId))
+ );
}
const now = getNow();
@@ -530,7 +548,7 @@ auth.post('/google', zValidator('json', googleAuthSchema), async (c) => {
.update(users)
.set({
googleId,
- isClaimed: true,
+ isClaimed: toDbBool(true),
accountStatus: 'active',
updatedAt: now,
})
@@ -538,7 +556,9 @@ auth.post('/google', zValidator('json', googleAuthSchema), async (c) => {
}
// Refresh user data
- user = await (db as any).select().from(users).where(eq((users as any).id, user.id)).get();
+ user = await dbGet(
+ (db as any).select().from(users).where(eq((users as any).id, user.id))
+ );
} else {
// Create new user
const firstUser = await isFirstUser();
@@ -552,7 +572,7 @@ auth.post('/google', zValidator('json', googleAuthSchema), async (c) => {
phone: null,
role: firstUser ? 'admin' : 'user',
languagePreference: null,
- isClaimed: true,
+ isClaimed: toDbBool(true),
googleId,
rucNumber: null,
accountStatus: 'active',
diff --git a/backend/src/routes/contacts.ts b/backend/src/routes/contacts.ts
index efc7581..360997b 100644
--- a/backend/src/routes/contacts.ts
+++ b/backend/src/routes/contacts.ts
@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
-import { db, contacts, emailSubscribers } from '../db/index.js';
+import { db, dbGet, dbAll, contacts, emailSubscribers } from '../db/index.js';
import { eq, desc } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { generateId, getNow } from '../lib/utils.js';
@@ -48,11 +48,12 @@ contactsRouter.post('/subscribe', zValidator('json', subscribeSchema), async (c)
const data = c.req.valid('json');
// Check if already subscribed
- const existing = await (db as any)
- .select()
- .from(emailSubscribers)
- .where(eq((emailSubscribers as any).email, data.email))
- .get();
+ const existing = await dbGet(
+ (db as any)
+ .select()
+ .from(emailSubscribers)
+ .where(eq((emailSubscribers as any).email, data.email))
+ );
if (existing) {
if (existing.status === 'unsubscribed') {
@@ -87,11 +88,9 @@ contactsRouter.post('/subscribe', zValidator('json', subscribeSchema), async (c)
contactsRouter.post('/unsubscribe', zValidator('json', z.object({ email: z.string().email() })), async (c) => {
const { email } = c.req.valid('json');
- const existing = await (db as any)
- .select()
- .from(emailSubscribers)
- .where(eq((emailSubscribers as any).email, email))
- .get();
+ const existing = await dbGet(
+ (db as any).select().from(emailSubscribers).where(eq((emailSubscribers as any).email, email))
+ );
if (!existing) {
return c.json({ error: 'Email not found' }, 404);
@@ -115,7 +114,7 @@ contactsRouter.get('/', requireAuth(['admin', 'organizer']), async (c) => {
query = query.where(eq((contacts as any).status, status));
}
- const result = await query.orderBy(desc((contacts as any).createdAt)).all();
+ const result = await dbAll(query.orderBy(desc((contacts as any).createdAt)));
return c.json({ contacts: result });
});
@@ -124,11 +123,12 @@ contactsRouter.get('/', requireAuth(['admin', 'organizer']), async (c) => {
contactsRouter.get('/:id', requireAuth(['admin', 'organizer']), async (c) => {
const id = c.req.param('id');
- const contact = await (db as any)
- .select()
- .from(contacts)
- .where(eq((contacts as any).id, id))
- .get();
+ const contact = await dbGet(
+ (db as any)
+ .select()
+ .from(contacts)
+ .where(eq((contacts as any).id, id))
+ );
if (!contact) {
return c.json({ error: 'Contact not found' }, 404);
@@ -142,11 +142,9 @@ contactsRouter.put('/:id', requireAuth(['admin', 'organizer']), zValidator('json
const id = c.req.param('id');
const data = c.req.valid('json');
- const existing = await (db as any)
- .select()
- .from(contacts)
- .where(eq((contacts as any).id, id))
- .get();
+ const existing = await dbGet(
+ (db as any).select().from(contacts).where(eq((contacts as any).id, id))
+ );
if (!existing) {
return c.json({ error: 'Contact not found' }, 404);
@@ -157,11 +155,9 @@ contactsRouter.put('/:id', requireAuth(['admin', 'organizer']), zValidator('json
.set({ status: data.status })
.where(eq((contacts as any).id, id));
- const updated = await (db as any)
- .select()
- .from(contacts)
- .where(eq((contacts as any).id, id))
- .get();
+ const updated = await dbGet(
+ (db as any).select().from(contacts).where(eq((contacts as any).id, id))
+ );
return c.json({ contact: updated });
});
@@ -185,7 +181,7 @@ contactsRouter.get('/subscribers/list', requireAuth(['admin', 'marketing']), asy
query = query.where(eq((emailSubscribers as any).status, status));
}
- const result = await query.orderBy(desc((emailSubscribers as any).createdAt)).all();
+ const result = await dbAll(query.orderBy(desc((emailSubscribers as any).createdAt)));
return c.json({ subscribers: result });
});
diff --git a/backend/src/routes/dashboard.ts b/backend/src/routes/dashboard.ts
index c3a4126..9895c5c 100644
--- a/backend/src/routes/dashboard.ts
+++ b/backend/src/routes/dashboard.ts
@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
-import { db, users, tickets, payments, events, invoices, User } from '../db/index.js';
+import { db, dbGet, dbAll, users, tickets, payments, events, invoices, User } from '../db/index.js';
import { eq, desc, and, gt, sql } from 'drizzle-orm';
import { requireAuth, getUserSessions, invalidateSession, invalidateAllUserSessions, hashPassword, validatePassword } from '../lib/auth.js';
import { generateId, getNow } from '../lib/utils.js';
@@ -70,11 +70,12 @@ dashboard.put('/profile', zValidator('json', updateProfileSchema), async (c) =>
})
.where(eq((users as any).id, user.id));
- const updatedUser = await (db as any)
- .select()
- .from(users)
- .where(eq((users as any).id, user.id))
- .get();
+ const updatedUser = await dbGet(
+ (db as any)
+ .select()
+ .from(users)
+ .where(eq((users as any).id, user.id))
+ );
return c.json({
profile: {
@@ -95,36 +96,40 @@ dashboard.put('/profile', zValidator('json', updateProfileSchema), async (c) =>
dashboard.get('/tickets', async (c) => {
const user = (c as any).get('user') as AuthUser;
- const userTickets = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).userId, user.id))
- .orderBy(desc((tickets as any).createdAt))
- .all();
+ const userTickets = await dbAll(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).userId, user.id))
+ .orderBy(desc((tickets as any).createdAt))
+ );
// Get event details for each ticket
const ticketsWithEvents = await Promise.all(
userTickets.map(async (ticket: any) => {
- const event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get();
+ const event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ );
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).ticketId, ticket.id))
- .get();
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).ticketId, ticket.id))
+ );
// Check for invoice
- let invoice = null;
+ let invoice: any = null;
if (payment && payment.status === 'paid') {
- invoice = await (db as any)
- .select()
- .from(invoices)
- .where(eq((invoices as any).paymentId, payment.id))
- .get();
+ invoice = await dbGet(
+ (db as any)
+ .select()
+ .from(invoices)
+ .where(eq((invoices as any).paymentId, payment.id))
+ );
}
return {
@@ -168,40 +173,44 @@ dashboard.get('/tickets/:id', async (c) => {
const user = (c as any).get('user') as AuthUser;
const ticketId = c.req.param('id');
- const ticket = await (db as any)
- .select()
- .from(tickets)
- .where(
- and(
- eq((tickets as any).id, ticketId),
- eq((tickets as any).userId, user.id)
+ const ticket = await dbGet(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(
+ and(
+ eq((tickets as any).id, ticketId),
+ eq((tickets as any).userId, user.id)
+ )
)
- )
- .get();
+ );
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
}
- const event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get();
+ const event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ );
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).ticketId, ticket.id))
- .get();
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).ticketId, ticket.id))
+ );
let invoice = null;
if (payment && payment.status === 'paid') {
- invoice = await (db as any)
- .select()
- .from(invoices)
- .where(eq((invoices as any).paymentId, payment.id))
- .get();
+ invoice = await dbGet(
+ (db as any)
+ .select()
+ .from(invoices)
+ .where(eq((invoices as any).paymentId, payment.id))
+ );
}
return c.json({
@@ -222,11 +231,12 @@ dashboard.get('/next-event', async (c) => {
const now = getNow();
// Get user's tickets for upcoming events
- const userTickets = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).userId, user.id))
- .all();
+ const userTickets = await dbAll(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).userId, user.id))
+ );
if (userTickets.length === 0) {
return c.json({ nextEvent: null });
@@ -240,11 +250,12 @@ dashboard.get('/next-event', async (c) => {
for (const ticket of userTickets) {
if (ticket.status === 'cancelled') continue;
- const event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get();
+ const event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ );
if (!event) continue;
@@ -253,11 +264,12 @@ dashboard.get('/next-event', async (c) => {
if (!nextEvent || new Date(event.startDatetime) < new Date(nextEvent.startDatetime)) {
nextEvent = event;
nextTicket = ticket;
- nextPayment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).ticketId, ticket.id))
- .get();
+ nextPayment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).ticketId, ticket.id))
+ );
}
}
}
@@ -282,11 +294,12 @@ dashboard.get('/payments', async (c) => {
const user = (c as any).get('user') as AuthUser;
// Get all user's tickets first
- const userTickets = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).userId, user.id))
- .all();
+ const userTickets = await dbAll(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).userId, user.id))
+ );
const ticketIds = userTickets.map((t: any) => t.id);
@@ -297,29 +310,32 @@ dashboard.get('/payments', async (c) => {
// Get all payments for user's tickets
const allPayments = [];
for (const ticketId of ticketIds) {
- const ticketPayments = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).ticketId, ticketId))
- .all();
+ const ticketPayments = await dbAll(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).ticketId, ticketId))
+ );
for (const payment of ticketPayments) {
const ticket = userTickets.find((t: any) => t.id === payment.ticketId);
const event = ticket
- ? await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get()
+ ? await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ )
: null;
- let invoice = null;
+ let invoice: any = null;
if (payment.status === 'paid') {
- invoice = await (db as any)
- .select()
- .from(invoices)
- .where(eq((invoices as any).paymentId, payment.id))
- .get();
+ invoice = await dbGet(
+ (db as any)
+ .select()
+ .from(invoices)
+ .where(eq((invoices as any).paymentId, payment.id))
+ );
}
allPayments.push({
@@ -355,36 +371,40 @@ dashboard.get('/payments', async (c) => {
dashboard.get('/invoices', async (c) => {
const user = (c as any).get('user') as AuthUser;
- const userInvoices = await (db as any)
- .select()
- .from(invoices)
- .where(eq((invoices as any).userId, user.id))
- .orderBy(desc((invoices as any).createdAt))
- .all();
+ const userInvoices = await dbAll(
+ (db as any)
+ .select()
+ .from(invoices)
+ .where(eq((invoices as any).userId, user.id))
+ .orderBy(desc((invoices as any).createdAt))
+ );
// Get payment and event details for each invoice
const invoicesWithDetails = await Promise.all(
userInvoices.map(async (invoice: any) => {
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).id, invoice.paymentId))
- .get();
-
- let event = null;
- if (payment) {
- const ticket = await (db as any)
+ const payment = await dbGet(
+ (db as any)
.select()
- .from(tickets)
- .where(eq((tickets as any).id, payment.ticketId))
- .get();
+ .from(payments)
+ .where(eq((payments as any).id, invoice.paymentId))
+ );
+
+ let event: any = null;
+ if (payment) {
+ const ticket = await dbGet(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).id, payment.ticketId))
+ );
if (ticket) {
- event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get();
+ event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ );
}
}
@@ -511,11 +531,12 @@ dashboard.get('/summary', async (c) => {
const membershipDays = Math.floor((now.getTime() - createdDate.getTime()) / (1000 * 60 * 60 * 24));
// Get ticket count
- const userTickets = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).userId, user.id))
- .all();
+ const userTickets = await dbAll(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).userId, user.id))
+ );
const totalTickets = userTickets.length;
const confirmedTickets = userTickets.filter((t: any) => t.status === 'confirmed' || t.status === 'checked_in').length;
@@ -524,11 +545,12 @@ dashboard.get('/summary', async (c) => {
for (const ticket of userTickets) {
if (ticket.status === 'cancelled') continue;
- const event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get();
+ const event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ );
if (event && new Date(event.startDatetime) > now) {
upcomingTickets.push({ ticket, event });
@@ -540,16 +562,17 @@ dashboard.get('/summary', async (c) => {
let pendingPayments = 0;
for (const ticketId of ticketIds) {
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(
- and(
- eq((payments as any).ticketId, ticketId),
- eq((payments as any).status, 'pending_approval')
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(
+ and(
+ eq((payments as any).ticketId, ticketId),
+ eq((payments as any).status, 'pending_approval')
+ )
)
- )
- .get();
+ );
if (payment) pendingPayments++;
}
diff --git a/backend/src/routes/emails.ts b/backend/src/routes/emails.ts
index 9506560..a8235f4 100644
--- a/backend/src/routes/emails.ts
+++ b/backend/src/routes/emails.ts
@@ -1,9 +1,8 @@
import { Hono } from 'hono';
-import { db, emailTemplates, emailLogs, events, tickets } from '../db/index.js';
+import { db, dbGet, dbAll, emailTemplates, emailLogs, events, tickets } from '../db/index.js';
import { eq, desc, and, sql } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
-import { getNow } from '../lib/utils.js';
-import { nanoid } from 'nanoid';
+import { getNow, generateId } from '../lib/utils.js';
import emailService from '../lib/email.js';
import { getTemplateVariables, defaultTemplates } from '../lib/emailTemplates.js';
@@ -13,11 +12,9 @@ const emailsRouter = new Hono();
// Get all email templates
emailsRouter.get('/templates', requireAuth(['admin', 'organizer']), async (c) => {
- const templates = await (db as any)
- .select()
- .from(emailTemplates)
- .orderBy(desc((emailTemplates as any).createdAt))
- .all();
+ const templates = await dbAll(
+ (db as any).select().from(emailTemplates).orderBy(desc((emailTemplates as any).createdAt))
+ );
// Parse variables JSON for each template
const parsedTemplates = templates.map((t: any) => ({
@@ -34,11 +31,12 @@ emailsRouter.get('/templates', requireAuth(['admin', 'organizer']), async (c) =>
emailsRouter.get('/templates/:id', requireAuth(['admin', 'organizer']), async (c) => {
const { id } = c.req.param();
- const template = await (db as any)
- .select()
- .from(emailTemplates)
- .where(eq((emailTemplates as any).id, id))
- .get();
+ const template = await dbGet(
+ (db as any)
+ .select()
+ .from(emailTemplates)
+ .where(eq((emailTemplates as any).id, id))
+ );
if (!template) {
return c.json({ error: 'Template not found' }, 404);
@@ -64,11 +62,9 @@ emailsRouter.post('/templates', requireAuth(['admin']), async (c) => {
}
// Check if slug already exists
- const existing = await (db as any)
- .select()
- .from(emailTemplates)
- .where(eq((emailTemplates as any).slug, slug))
- .get();
+ const existing = await dbGet(
+ (db as any).select().from(emailTemplates).where(eq((emailTemplates as any).slug, slug))
+ );
if (existing) {
return c.json({ error: 'Template with this slug already exists' }, 400);
@@ -76,7 +72,7 @@ emailsRouter.post('/templates', requireAuth(['admin']), async (c) => {
const now = getNow();
const template = {
- id: nanoid(),
+ id: generateId(),
name,
slug,
subject,
@@ -111,11 +107,12 @@ emailsRouter.put('/templates/:id', requireAuth(['admin']), async (c) => {
const { id } = c.req.param();
const body = await c.req.json();
- const existing = await (db as any)
- .select()
- .from(emailTemplates)
- .where(eq((emailTemplates as any).id, id))
- .get();
+ const existing = await dbGet(
+ (db as any)
+ .select()
+ .from(emailTemplates)
+ .where(eq((emailTemplates as any).id, id))
+ );
if (!existing) {
return c.json({ error: 'Template not found' }, 404);
@@ -148,11 +145,12 @@ emailsRouter.put('/templates/:id', requireAuth(['admin']), async (c) => {
.set(updateData)
.where(eq((emailTemplates as any).id, id));
- const updated = await (db as any)
- .select()
- .from(emailTemplates)
- .where(eq((emailTemplates as any).id, id))
- .get();
+ const updated = await dbGet(
+ (db as any)
+ .select()
+ .from(emailTemplates)
+ .where(eq((emailTemplates as any).id, id))
+ );
return c.json({
template: {
@@ -169,11 +167,9 @@ emailsRouter.put('/templates/:id', requireAuth(['admin']), async (c) => {
emailsRouter.delete('/templates/:id', requireAuth(['admin']), async (c) => {
const { id } = c.req.param();
- const template = await (db as any)
- .select()
- .from(emailTemplates)
- .where(eq((emailTemplates as any).id, id))
- .get();
+ const template = await dbGet(
+ (db as any).select().from(emailTemplates).where(eq((emailTemplates as any).id, id))
+ );
if (!template) {
return c.json({ error: 'Template not found' }, 404);
@@ -306,11 +302,12 @@ emailsRouter.get('/logs', requireAuth(['admin', 'organizer']), async (c) => {
query = query.where(and(...conditions));
}
- const logs = await query
- .orderBy(desc((emailLogs as any).createdAt))
- .limit(limit)
- .offset(offset)
- .all();
+ const logs = await dbAll(
+ query
+ .orderBy(desc((emailLogs as any).createdAt))
+ .limit(limit)
+ .offset(offset)
+ );
// Get total count
let countQuery = (db as any)
@@ -321,7 +318,7 @@ emailsRouter.get('/logs', requireAuth(['admin', 'organizer']), async (c) => {
countQuery = countQuery.where(and(...conditions));
}
- const totalResult = await countQuery.get();
+ const totalResult = await dbGet(countQuery);
const total = totalResult?.count || 0;
return c.json({
@@ -339,11 +336,9 @@ emailsRouter.get('/logs', requireAuth(['admin', 'organizer']), async (c) => {
emailsRouter.get('/logs/:id', requireAuth(['admin', 'organizer']), async (c) => {
const { id } = c.req.param();
- const log = await (db as any)
- .select()
- .from(emailLogs)
- .where(eq((emailLogs as any).id, id))
- .get();
+ const log = await dbGet(
+ (db as any).select().from(emailLogs).where(eq((emailLogs as any).id, id))
+ );
if (!log) {
return c.json({ error: 'Email log not found' }, 404);
@@ -362,22 +357,22 @@ emailsRouter.get('/stats', requireAuth(['admin', 'organizer']), async (c) => {
? (db as any).select({ count: sql`count(*)` }).from(emailLogs).where(baseCondition)
: (db as any).select({ count: sql`count(*)` }).from(emailLogs);
- const total = (await totalQuery.get())?.count || 0;
+ const total = (await dbGet(totalQuery))?.count || 0;
const sentCondition = baseCondition
? and(baseCondition, eq((emailLogs as any).status, 'sent'))
: eq((emailLogs as any).status, 'sent');
- const sent = (await (db as any).select({ count: sql`count(*)` }).from(emailLogs).where(sentCondition).get())?.count || 0;
+ const sent = (await dbGet((db as any).select({ count: sql`count(*)` }).from(emailLogs).where(sentCondition)))?.count || 0;
const failedCondition = baseCondition
? and(baseCondition, eq((emailLogs as any).status, 'failed'))
: eq((emailLogs as any).status, 'failed');
- const failed = (await (db as any).select({ count: sql`count(*)` }).from(emailLogs).where(failedCondition).get())?.count || 0;
+ const failed = (await dbGet((db as any).select({ count: sql`count(*)` }).from(emailLogs).where(failedCondition)))?.count || 0;
const pendingCondition = baseCondition
? and(baseCondition, eq((emailLogs as any).status, 'pending'))
: eq((emailLogs as any).status, 'pending');
- const pending = (await (db as any).select({ count: sql`count(*)` }).from(emailLogs).where(pendingCondition).get())?.count || 0;
+ const pending = (await dbGet((db as any).select({ count: sql`count(*)` }).from(emailLogs).where(pendingCondition)))?.count || 0;
return c.json({
stats: {
diff --git a/backend/src/routes/events.ts b/backend/src/routes/events.ts
index bba2275..02191b1 100644
--- a/backend/src/routes/events.ts
+++ b/backend/src/routes/events.ts
@@ -1,10 +1,10 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
-import { db, events, tickets, payments, eventPaymentOverrides, emailLogs, invoices } from '../db/index.js';
+import { db, dbGet, dbAll, events, tickets, payments, eventPaymentOverrides, emailLogs, invoices } from '../db/index.js';
import { eq, desc, and, gte, sql } from 'drizzle-orm';
import { requireAuth, getAuthUser } from '../lib/auth.js';
-import { generateId, getNow } from '../lib/utils.js';
+import { generateId, getNow, convertBooleansForDb, toDbDate } from '../lib/utils.js';
interface UserContext {
id: string;
@@ -15,6 +15,21 @@ interface UserContext {
const eventsRouter = new Hono<{ Variables: { user: UserContext } }>();
+// Helper to normalize event data for API response
+// PostgreSQL decimal returns strings, booleans are stored as integers
+function normalizeEvent(event: any) {
+ if (!event) return event;
+ return {
+ ...event,
+ // Convert price from string/decimal to clean number
+ price: typeof event.price === 'string' ? parseFloat(event.price) : Number(event.price),
+ // Convert capacity from string to number if needed
+ capacity: typeof event.capacity === 'string' ? parseInt(event.capacity, 10) : Number(event.capacity),
+ // Convert boolean integers to actual booleans for frontend
+ externalBookingEnabled: Boolean(event.externalBookingEnabled),
+ };
+}
+
// Custom validation error handler
const validationHook = (result: any, c: any) => {
if (!result.success) {
@@ -23,6 +38,27 @@ const validationHook = (result: any, c: any) => {
}
};
+// Helper to parse price from string (handles both "45000" and "41,44" formats)
+const parsePrice = (val: unknown): number => {
+ if (typeof val === 'number') return val;
+ if (typeof val === 'string') {
+ // Replace comma with dot for decimal parsing (European format)
+ const normalized = val.replace(',', '.');
+ const parsed = parseFloat(normalized);
+ return isNaN(parsed) ? 0 : parsed;
+ }
+ return 0;
+};
+
+// Helper to normalize boolean (handles true/false and 0/1)
+const normalizeBoolean = (val: unknown): boolean => {
+ if (typeof val === 'boolean') return val;
+ if (typeof val === 'number') return val !== 0;
+ if (val === 'true') return true;
+ if (val === 'false') return false;
+ return false;
+};
+
const baseEventSchema = z.object({
title: z.string().min(1),
titleEs: z.string().optional().nullable(),
@@ -34,14 +70,15 @@ const baseEventSchema = z.object({
endDatetime: z.string().optional().nullable(),
location: z.string().min(1),
locationUrl: z.string().url().optional().nullable().or(z.literal('')),
- price: z.number().min(0).default(0),
+ // Accept price as number or string (handles "45000" and "41,44" formats)
+ price: z.union([z.number(), z.string()]).transform(parsePrice).pipe(z.number().min(0)).default(0),
currency: z.string().default('PYG'),
- capacity: z.number().min(1).default(50),
+ capacity: z.union([z.number(), z.string()]).transform((val) => typeof val === 'string' ? parseInt(val, 10) || 50 : val).pipe(z.number().min(1)).default(50),
status: z.enum(['draft', 'published', 'cancelled', 'completed', 'archived']).default('draft'),
// Accept relative paths (/uploads/...) or full URLs
bannerUrl: z.string().optional().nullable().or(z.literal('')),
- // External booking support
- externalBookingEnabled: z.boolean().default(false),
+ // External booking support - accept boolean or number (0/1 from DB)
+ externalBookingEnabled: z.union([z.boolean(), z.number()]).transform(normalizeBoolean).default(false),
externalBookingUrl: z.string().url().optional().nullable().or(z.literal('')),
});
@@ -94,26 +131,28 @@ eventsRouter.get('/', async (c) => {
);
}
- const result = await query.orderBy(desc((events as any).startDatetime)).all();
+ const result = await dbAll(query.orderBy(desc((events as any).startDatetime)));
// Get ticket counts for each event
const eventsWithCounts = await Promise.all(
result.map(async (event: any) => {
- const ticketCount = await (db as any)
- .select({ count: sql`count(*)` })
- .from(tickets)
- .where(
- and(
- eq((tickets as any).eventId, event.id),
- eq((tickets as any).status, 'confirmed')
+ const ticketCount = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(tickets)
+ .where(
+ and(
+ eq((tickets as any).eventId, event.id),
+ eq((tickets as any).status, 'confirmed')
+ )
)
- )
- .get();
+ );
+ const normalized = normalizeEvent(event);
return {
- ...event,
+ ...normalized,
bookedCount: ticketCount?.count || 0,
- availableSeats: event.capacity - (ticketCount?.count || 0),
+ availableSeats: normalized.capacity - (ticketCount?.count || 0),
};
})
);
@@ -125,29 +164,33 @@ eventsRouter.get('/', async (c) => {
eventsRouter.get('/:id', async (c) => {
const id = c.req.param('id');
- const event = await (db as any).select().from(events).where(eq((events as any).id, id)).get();
+ const event = await dbGet(
+ (db as any).select().from(events).where(eq((events as any).id, id))
+ );
if (!event) {
return c.json({ error: 'Event not found' }, 404);
}
// Get ticket count
- const ticketCount = await (db as any)
- .select({ count: sql`count(*)` })
- .from(tickets)
- .where(
- and(
- eq((tickets as any).eventId, id),
- eq((tickets as any).status, 'confirmed')
+ const ticketCount = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(tickets)
+ .where(
+ and(
+ eq((tickets as any).eventId, id),
+ eq((tickets as any).status, 'confirmed')
+ )
)
- )
- .get();
+ );
+ const normalized = normalizeEvent(event);
return c.json({
event: {
- ...event,
+ ...normalized,
bookedCount: ticketCount?.count || 0,
- availableSeats: event.capacity - (ticketCount?.count || 0),
+ availableSeats: normalized.capacity - (ticketCount?.count || 0),
},
});
});
@@ -156,39 +199,42 @@ eventsRouter.get('/:id', async (c) => {
eventsRouter.get('/next/upcoming', async (c) => {
const now = getNow();
- const event = await (db as any)
- .select()
- .from(events)
- .where(
- and(
- eq((events as any).status, 'published'),
- gte((events as any).startDatetime, now)
+ const event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(
+ and(
+ eq((events as any).status, 'published'),
+ gte((events as any).startDatetime, now)
+ )
)
- )
- .orderBy((events as any).startDatetime)
- .limit(1)
- .get();
+ .orderBy((events as any).startDatetime)
+ .limit(1)
+ );
if (!event) {
return c.json({ event: null });
}
- const ticketCount = await (db as any)
- .select({ count: sql`count(*)` })
- .from(tickets)
- .where(
- and(
- eq((tickets as any).eventId, event.id),
- eq((tickets as any).status, 'confirmed')
+ const ticketCount = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(tickets)
+ .where(
+ and(
+ eq((tickets as any).eventId, event.id),
+ eq((tickets as any).status, 'confirmed')
+ )
)
- )
- .get();
+ );
+ const normalized = normalizeEvent(event);
return c.json({
event: {
- ...event,
+ ...normalized,
bookedCount: ticketCount?.count || 0,
- availableSeats: event.capacity - (ticketCount?.count || 0),
+ availableSeats: normalized.capacity - (ticketCount?.count || 0),
},
});
});
@@ -200,16 +246,22 @@ eventsRouter.post('/', requireAuth(['admin', 'organizer']), zValidator('json', c
const now = getNow();
const id = generateId();
+ // Convert data for database compatibility
+ const dbData = convertBooleansForDb(data);
+
const newEvent = {
id,
- ...data,
+ ...dbData,
+ startDatetime: toDbDate(data.startDatetime),
+ endDatetime: data.endDatetime ? toDbDate(data.endDatetime) : null,
createdAt: now,
updatedAt: now,
};
await (db as any).insert(events).values(newEvent);
- return c.json({ event: newEvent }, 201);
+ // Return normalized event data
+ return c.json({ event: normalizeEvent(newEvent) }, 201);
});
// Update event (admin/organizer only)
@@ -217,46 +269,64 @@ eventsRouter.put('/:id', requireAuth(['admin', 'organizer']), zValidator('json',
const id = c.req.param('id');
const data = c.req.valid('json');
- const existing = await (db as any).select().from(events).where(eq((events as any).id, id)).get();
+ const existing = await dbGet(
+ (db as any).select().from(events).where(eq((events as any).id, id))
+ );
if (!existing) {
return c.json({ error: 'Event not found' }, 404);
}
const now = getNow();
+ // Convert data for database compatibility
+ const updateData: Record = { ...convertBooleansForDb(data), updatedAt: now };
+ // Convert datetime fields if present
+ if (data.startDatetime) {
+ updateData.startDatetime = toDbDate(data.startDatetime);
+ }
+ if (data.endDatetime !== undefined) {
+ updateData.endDatetime = data.endDatetime ? toDbDate(data.endDatetime) : null;
+ }
+
await (db as any)
.update(events)
- .set({ ...data, updatedAt: now })
+ .set(updateData)
.where(eq((events as any).id, id));
- const updated = await (db as any).select().from(events).where(eq((events as any).id, id)).get();
+ const updated = await dbGet(
+ (db as any).select().from(events).where(eq((events as any).id, id))
+ );
- return c.json({ event: updated });
+ return c.json({ event: normalizeEvent(updated) });
});
// Delete event (admin only)
eventsRouter.delete('/:id', requireAuth(['admin']), async (c) => {
const id = c.req.param('id');
- const existing = await (db as any).select().from(events).where(eq((events as any).id, id)).get();
+ const existing = await dbGet(
+ (db as any).select().from(events).where(eq((events as any).id, id))
+ );
if (!existing) {
return c.json({ error: 'Event not found' }, 404);
}
// Get all tickets for this event
- const eventTickets = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).eventId, id))
- .all();
+ const eventTickets = await dbAll(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).eventId, id))
+ );
// Delete invoices and payments for all tickets of this event
for (const ticket of eventTickets) {
// Get payments for this ticket
- const ticketPayments = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).ticketId, ticket.id))
- .all();
+ const ticketPayments = await dbAll(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).ticketId, ticket.id))
+ );
// Delete invoices for each payment
for (const payment of ticketPayments) {
@@ -289,11 +359,12 @@ eventsRouter.delete('/:id', requireAuth(['admin']), async (c) => {
eventsRouter.get('/:id/attendees', requireAuth(['admin', 'organizer', 'staff']), async (c) => {
const id = c.req.param('id');
- const attendees = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).eventId, id))
- .all();
+ const attendees = await dbAll(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).eventId, id))
+ );
return c.json({ attendees });
});
@@ -302,7 +373,9 @@ eventsRouter.get('/:id/attendees', requireAuth(['admin', 'organizer', 'staff']),
eventsRouter.post('/:id/duplicate', requireAuth(['admin', 'organizer']), async (c) => {
const id = c.req.param('id');
- const existing = await (db as any).select().from(events).where(eq((events as any).id, id)).get();
+ const existing = await dbGet(
+ (db as any).select().from(events).where(eq((events as any).id, id))
+ );
if (!existing) {
return c.json({ error: 'Event not found' }, 404);
}
@@ -319,7 +392,7 @@ eventsRouter.post('/:id/duplicate', requireAuth(['admin', 'organizer']), async (
descriptionEs: existing.descriptionEs,
shortDescription: existing.shortDescription,
shortDescriptionEs: existing.shortDescriptionEs,
- startDatetime: existing.startDatetime,
+ startDatetime: existing.startDatetime, // Already in DB format from existing record
endDatetime: existing.endDatetime,
location: existing.location,
locationUrl: existing.locationUrl,
@@ -328,7 +401,7 @@ eventsRouter.post('/:id/duplicate', requireAuth(['admin', 'organizer']), async (
capacity: existing.capacity,
status: 'draft',
bannerUrl: existing.bannerUrl,
- externalBookingEnabled: existing.externalBookingEnabled || false,
+ externalBookingEnabled: existing.externalBookingEnabled ?? 0, // Already in DB format (0/1)
externalBookingUrl: existing.externalBookingUrl,
createdAt: now,
updatedAt: now,
@@ -336,7 +409,7 @@ eventsRouter.post('/:id/duplicate', requireAuth(['admin', 'organizer']), async (
await (db as any).insert(events).values(duplicatedEvent);
- return c.json({ event: duplicatedEvent, message: 'Event duplicated successfully' }, 201);
+ return c.json({ event: normalizeEvent(duplicatedEvent), message: 'Event duplicated successfully' }, 201);
});
export default eventsRouter;
diff --git a/backend/src/routes/legal-pages.ts b/backend/src/routes/legal-pages.ts
new file mode 100644
index 0000000..6e5f0a0
--- /dev/null
+++ b/backend/src/routes/legal-pages.ts
@@ -0,0 +1,393 @@
+import { Hono } from 'hono';
+import { db, dbGet, dbAll, legalPages } from '../db/index.js';
+import { eq, desc } from 'drizzle-orm';
+import { requireAuth } from '../lib/auth.js';
+import { getNow, generateId } from '../lib/utils.js';
+import fs from 'fs';
+import path from 'path';
+
+const legalPagesRouter = new Hono();
+
+// Helper: Convert plain text to simple markdown
+// Preserves paragraphs and line breaks, nothing fancy
+function textToMarkdown(text: string): string {
+ if (!text) return '';
+
+ // Split into paragraphs (double newlines)
+ const paragraphs = text.split(/\n\s*\n/);
+
+ // Process each paragraph
+ const processed = paragraphs.map(para => {
+ // Replace single newlines with double spaces + newline for markdown line breaks
+ return para.trim().replace(/\n/g, ' \n');
+ });
+
+ // Join paragraphs with double newlines
+ return processed.join('\n\n');
+}
+
+// Helper: Convert markdown to plain text for editing
+function markdownToText(markdown: string): string {
+ if (!markdown) return '';
+
+ let text = markdown;
+
+ // Remove markdown heading markers (# ## ###)
+ text = text.replace(/^#{1,6}\s+/gm, '');
+
+ // Remove horizontal rules
+ text = text.replace(/^---+$/gm, '');
+
+ // Remove bold/italic markers
+ text = text.replace(/\*\*([^*]+)\*\*/g, '$1');
+ text = text.replace(/\*([^*]+)\*/g, '$1');
+ text = text.replace(/__([^_]+)__/g, '$1');
+ text = text.replace(/_([^_]+)_/g, '$1');
+
+ // Remove list markers (preserve text)
+ text = text.replace(/^\s*[\*\-\+]\s+/gm, '');
+ text = text.replace(/^\s*\d+\.\s+/gm, '');
+
+ // Remove link formatting [text](url) -> text
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
+
+ // Remove double-space line breaks
+ text = text.replace(/ \n/g, '\n');
+
+ // Normalize multiple newlines
+ text = text.replace(/\n{3,}/g, '\n\n');
+
+ return text.trim();
+}
+
+// Helper: Extract title from markdown content
+function extractTitleFromMarkdown(content: string): string {
+ if (!content) return 'Untitled';
+
+ const match = content.match(/^#\s+(.+?)(?:\s*[–-]\s*.+)?$/m);
+ if (match) {
+ return match[1].trim();
+ }
+ // Fallback to first line
+ const firstLine = content.split('\n')[0].replace(/^#+\s*/, '').trim();
+ return firstLine || 'Untitled';
+}
+
+// Helper: Get legal directory path
+function getLegalDir(): string {
+ // When running from backend, legal folder is in frontend
+ const possiblePaths = [
+ path.join(process.cwd(), '../frontend/legal'),
+ path.join(process.cwd(), 'frontend/legal'),
+ path.join(process.cwd(), 'legal'),
+ ];
+
+ for (const p of possiblePaths) {
+ if (fs.existsSync(p)) {
+ return p;
+ }
+ }
+
+ return possiblePaths[0]; // Default
+}
+
+// Helper: Convert filename to slug
+function fileNameToSlug(fileName: string): string {
+ return fileName.replace('.md', '').replace(/_/g, '-');
+}
+
+// Title map for localization
+const titleMap: Record = {
+ 'privacy-policy': { en: 'Privacy Policy', es: 'Política de Privacidad' },
+ 'terms-policy': { en: 'Terms & Conditions', es: 'Términos y Condiciones' },
+ 'refund-cancelation-policy': { en: 'Refund & Cancellation Policy', es: 'Política de Reembolso y Cancelación' },
+};
+
+// Helper: Get localized content with fallback
+// If requested locale content is missing, fallback to the other locale
+function getLocalizedContent(page: any, locale: string = 'en'): { title: string; contentMarkdown: string } {
+ const isSpanish = locale === 'es';
+
+ // Title: prefer requested locale, fallback to other
+ let title: string;
+ if (isSpanish) {
+ title = page.titleEs || page.title;
+ } else {
+ title = page.title || page.titleEs;
+ }
+
+ // Content: prefer requested locale, fallback to other
+ let contentMarkdown: string;
+ if (isSpanish) {
+ contentMarkdown = page.contentMarkdownEs || page.contentMarkdown;
+ } else {
+ contentMarkdown = page.contentMarkdown || page.contentMarkdownEs;
+ }
+
+ return { title, contentMarkdown };
+}
+
+// ==================== Public Routes ====================
+
+// Get all legal pages (public, for footer/navigation)
+legalPagesRouter.get('/', async (c) => {
+ const locale = c.req.query('locale') || 'en';
+
+ const pages = await dbAll(
+ (db as any)
+ .select({
+ id: (legalPages as any).id,
+ slug: (legalPages as any).slug,
+ title: (legalPages as any).title,
+ titleEs: (legalPages as any).titleEs,
+ updatedAt: (legalPages as any).updatedAt,
+ })
+ .from(legalPages)
+ .orderBy((legalPages as any).slug)
+ );
+
+ // Return pages with localized title
+ const localizedPages = pages.map((page: any) => ({
+ id: page.id,
+ slug: page.slug,
+ title: locale === 'es' ? (page.titleEs || page.title) : (page.title || page.titleEs),
+ updatedAt: page.updatedAt,
+ }));
+
+ return c.json({ pages: localizedPages });
+});
+
+// Get single legal page (public, for rendering)
+legalPagesRouter.get('/:slug', async (c) => {
+ const { slug } = c.req.param();
+ const locale = c.req.query('locale') || 'en';
+
+ // First try to get from database
+ const page = await dbGet(
+ (db as any).select().from(legalPages).where(eq((legalPages as any).slug, slug))
+ );
+
+ if (page) {
+ // Get localized content with fallback
+ const { title, contentMarkdown } = getLocalizedContent(page, locale);
+
+ return c.json({
+ page: {
+ id: page.id,
+ slug: page.slug,
+ title,
+ contentMarkdown,
+ updatedAt: page.updatedAt,
+ source: 'database',
+ }
+ });
+ }
+
+ // Fallback to filesystem
+ const legalDir = getLegalDir();
+ const fileName = slug.replace(/-/g, '_') + '.md';
+ const filePath = path.join(legalDir, fileName);
+
+ if (fs.existsSync(filePath)) {
+ const content = fs.readFileSync(filePath, 'utf-8');
+ const titles = titleMap[slug];
+ const title = locale === 'es'
+ ? (titles?.es || titles?.en || slug)
+ : (titles?.en || titles?.es || slug);
+
+ return c.json({
+ page: {
+ slug,
+ title,
+ contentMarkdown: content,
+ source: 'filesystem',
+ }
+ });
+ }
+
+ return c.json({ error: 'Legal page not found' }, 404);
+});
+
+// ==================== Admin Routes ====================
+
+// Get all legal pages for admin (with full content)
+legalPagesRouter.get('/admin/list', requireAuth(['admin']), async (c) => {
+ const pages = await dbAll(
+ (db as any)
+ .select()
+ .from(legalPages)
+ .orderBy((legalPages as any).slug)
+ );
+
+ // Add flags to indicate which languages have content
+ const pagesWithFlags = pages.map((page: any) => ({
+ ...page,
+ hasEnglish: Boolean(page.contentText),
+ hasSpanish: Boolean(page.contentTextEs),
+ }));
+
+ return c.json({ pages: pagesWithFlags });
+});
+
+// Get single legal page for editing (admin)
+legalPagesRouter.get('/admin/:slug', requireAuth(['admin']), async (c) => {
+ const { slug } = c.req.param();
+
+ const page = await dbGet(
+ (db as any).select().from(legalPages).where(eq((legalPages as any).slug, slug))
+ );
+
+ if (!page) {
+ return c.json({ error: 'Legal page not found' }, 404);
+ }
+
+ return c.json({
+ page: {
+ ...page,
+ hasEnglish: Boolean(page.contentText),
+ hasSpanish: Boolean(page.contentTextEs),
+ }
+ });
+});
+
+// Update legal page (admin only)
+// Note: No creation or deletion - only updates are allowed
+// Accepts markdown content from rich text editor
+legalPagesRouter.put('/admin/:slug', requireAuth(['admin']), async (c) => {
+ const { slug } = c.req.param();
+ const user = (c as any).get('user');
+ const body = await c.req.json();
+ // Accept both contentText (legacy plain text) and contentMarkdown (from rich text editor)
+ const { contentText, contentTextEs, contentMarkdown, contentMarkdownEs, title, titleEs } = body;
+
+ // Determine content - prefer markdown if provided, fall back to contentText
+ const enContent = contentMarkdown !== undefined ? contentMarkdown : contentText;
+ const esContent = contentMarkdownEs !== undefined ? contentMarkdownEs : contentTextEs;
+
+ // At least one content field is required
+ if (!enContent && !esContent) {
+ return c.json({ error: 'At least one language content is required' }, 400);
+ }
+
+ const existing = await dbGet(
+ (db as any)
+ .select()
+ .from(legalPages)
+ .where(eq((legalPages as any).slug, slug))
+ );
+
+ if (!existing) {
+ return c.json({ error: 'Legal page not found' }, 404);
+ }
+
+ const updateData: any = {
+ updatedAt: getNow(),
+ updatedBy: user?.id || null,
+ };
+
+ // Update English content if provided
+ if (enContent !== undefined) {
+ // Store markdown directly (from rich text editor)
+ updateData.contentMarkdown = enContent;
+ // Derive plain text from markdown
+ updateData.contentText = markdownToText(enContent);
+ }
+
+ // Update Spanish content if provided
+ if (esContent !== undefined) {
+ updateData.contentMarkdownEs = esContent || null;
+ updateData.contentTextEs = esContent ? markdownToText(esContent) : null;
+ }
+
+ // Allow updating titles
+ if (title !== undefined) {
+ updateData.title = title;
+ }
+ if (titleEs !== undefined) {
+ updateData.titleEs = titleEs || null;
+ }
+
+ await (db as any)
+ .update(legalPages)
+ .set(updateData)
+ .where(eq((legalPages as any).slug, slug));
+
+ const updated = await dbGet(
+ (db as any).select().from(legalPages).where(eq((legalPages as any).slug, slug))
+ );
+
+ return c.json({
+ page: {
+ ...updated,
+ hasEnglish: Boolean(updated.contentText),
+ hasSpanish: Boolean(updated.contentTextEs),
+ },
+ message: 'Legal page updated successfully'
+ });
+});
+
+// Seed legal pages from filesystem (admin only)
+// This imports markdown files and converts them to plain text for editing
+// Files are imported as English content - Spanish can be added manually
+legalPagesRouter.post('/admin/seed', requireAuth(['admin']), async (c) => {
+ const user = (c as any).get('user');
+
+ // Check if table already has data
+ const existingPages = await dbAll(
+ (db as any)
+ .select({ id: (legalPages as any).id })
+ .from(legalPages)
+ .limit(1)
+ );
+
+ if (existingPages.length > 0) {
+ return c.json({
+ message: 'Legal pages already seeded. Use update to modify pages.',
+ seeded: 0
+ });
+ }
+
+ const legalDir = getLegalDir();
+
+ if (!fs.existsSync(legalDir)) {
+ return c.json({ error: `Legal directory not found: ${legalDir}` }, 400);
+ }
+
+ const files = fs.readdirSync(legalDir).filter(f => f.endsWith('.md'));
+ const seededPages: string[] = [];
+ const now = getNow();
+
+ for (const file of files) {
+ const filePath = path.join(legalDir, file);
+ const contentMarkdown = fs.readFileSync(filePath, 'utf-8');
+ const slug = fileNameToSlug(file);
+ const contentText = markdownToText(contentMarkdown);
+ const titles = titleMap[slug];
+ const title = titles?.en || extractTitleFromMarkdown(contentMarkdown);
+ const titleEs = titles?.es || null;
+
+ await (db as any).insert(legalPages).values({
+ id: generateId(),
+ slug,
+ title,
+ titleEs,
+ contentText, // English plain text
+ contentTextEs: null, // Spanish to be added manually
+ contentMarkdown, // English markdown
+ contentMarkdownEs: null, // Spanish to be generated when contentTextEs is set
+ updatedAt: now,
+ updatedBy: user?.id || null,
+ createdAt: now,
+ });
+
+ seededPages.push(slug);
+ }
+
+ return c.json({
+ message: `Successfully seeded ${seededPages.length} legal pages (English content imported, Spanish can be added via editor)`,
+ seeded: seededPages.length,
+ pages: seededPages,
+ });
+});
+
+export default legalPagesRouter;
diff --git a/backend/src/routes/lnbits.ts b/backend/src/routes/lnbits.ts
index 1282724..4226e97 100644
--- a/backend/src/routes/lnbits.ts
+++ b/backend/src/routes/lnbits.ts
@@ -1,6 +1,6 @@
import { Hono } from 'hono';
import { streamSSE } from 'hono/streaming';
-import { db, tickets, payments } from '../db/index.js';
+import { db, dbGet, tickets, payments } from '../db/index.js';
import { eq } from 'drizzle-orm';
import { getNow } from '../lib/utils.js';
import { verifyWebhookPayment, getPaymentStatus } from '../lib/lnbits.js';
@@ -157,11 +157,9 @@ async function handlePaymentComplete(ticketId: string, paymentHash: string) {
const now = getNow();
// Check if already confirmed to avoid duplicate updates
- const existingTicket = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).id, ticketId))
- .get();
+ const existingTicket = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, ticketId))
+ );
if (existingTicket?.status === 'confirmed') {
console.log(`Ticket ${ticketId} already confirmed, skipping update`);
@@ -188,11 +186,12 @@ async function handlePaymentComplete(ticketId: string, paymentHash: string) {
console.log(`Ticket ${ticketId} confirmed via Lightning payment (hash: ${paymentHash})`);
// Get payment for sending receipt
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).ticketId, ticketId))
- .get();
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).ticketId, ticketId))
+ );
// Send confirmation emails asynchronously
Promise.all([
@@ -211,11 +210,9 @@ lnbitsRouter.get('/stream/:ticketId', async (c) => {
const ticketId = c.req.param('ticketId');
// Verify ticket exists
- const ticket = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).id, ticketId))
- .get();
+ const ticket = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, ticketId))
+ );
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -227,11 +224,9 @@ lnbitsRouter.get('/stream/:ticketId', async (c) => {
}
// Get payment to start background checker
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).ticketId, ticketId))
- .get();
+ const payment = await dbGet(
+ (db as any).select().from(payments).where(eq((payments as any).ticketId, ticketId))
+ );
// Start background checker if not already running
if (payment?.reference && !activeCheckers.has(ticketId)) {
@@ -290,21 +285,23 @@ lnbitsRouter.get('/stream/:ticketId', async (c) => {
lnbitsRouter.get('/status/:ticketId', async (c) => {
const ticketId = c.req.param('ticketId');
- const ticket = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).id, ticketId))
- .get();
+ const ticket = await dbGet(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).id, ticketId))
+ );
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
}
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).ticketId, ticketId))
- .get();
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).ticketId, ticketId))
+ );
return c.json({
ticketStatus: ticket.status,
diff --git a/backend/src/routes/media.ts b/backend/src/routes/media.ts
index 96aa995..b0b75b6 100644
--- a/backend/src/routes/media.ts
+++ b/backend/src/routes/media.ts
@@ -1,5 +1,5 @@
import { Hono } from 'hono';
-import { db, media } from '../db/index.js';
+import { db, dbGet, dbAll, media } from '../db/index.js';
import { eq } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { generateId, getNow } from '../lib/utils.js';
@@ -85,11 +85,9 @@ mediaRouter.post('/upload', requireAuth(['admin', 'organizer']), async (c) => {
mediaRouter.get('/:id', async (c) => {
const id = c.req.param('id');
- const mediaRecord = await (db as any)
- .select()
- .from(media)
- .where(eq((media as any).id, id))
- .get();
+ const mediaRecord = await dbGet(
+ (db as any).select().from(media).where(eq((media as any).id, id))
+ );
if (!mediaRecord) {
return c.json({ error: 'Media not found' }, 404);
@@ -102,11 +100,9 @@ mediaRouter.get('/:id', async (c) => {
mediaRouter.delete('/:id', requireAuth(['admin', 'organizer']), async (c) => {
const id = c.req.param('id');
- const mediaRecord = await (db as any)
- .select()
- .from(media)
- .where(eq((media as any).id, id))
- .get();
+ const mediaRecord = await dbGet(
+ (db as any).select().from(media).where(eq((media as any).id, id))
+ );
if (!mediaRecord) {
return c.json({ error: 'Media not found' }, 404);
@@ -142,7 +138,7 @@ mediaRouter.get('/', requireAuth(['admin', 'organizer']), async (c) => {
query = query.where(eq((media as any).relatedId, relatedId));
}
- const result = await query.all();
+ const result = await dbAll(query);
return c.json({ media: result });
});
diff --git a/backend/src/routes/payment-options.ts b/backend/src/routes/payment-options.ts
index 369ecd2..c034aed 100644
--- a/backend/src/routes/payment-options.ts
+++ b/backend/src/routes/payment-options.ts
@@ -1,10 +1,10 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
-import { db, paymentOptions, eventPaymentOverrides, events } from '../db/index.js';
+import { db, dbGet, paymentOptions, eventPaymentOverrides, events } from '../db/index.js';
import { eq } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
-import { generateId, getNow } from '../lib/utils.js';
+import { generateId, getNow, convertBooleansForDb } from '../lib/utils.js';
const paymentOptionsRouter = new Hono();
@@ -52,10 +52,9 @@ const updateEventOverridesSchema = z.object({
// Get global payment options
paymentOptionsRouter.get('/', requireAuth(['admin']), async (c) => {
- const options = await (db as any)
- .select()
- .from(paymentOptions)
- .get();
+ const options = await dbGet(
+ (db as any).select().from(paymentOptions)
+ );
// If no options exist yet, return defaults
if (!options) {
@@ -92,17 +91,21 @@ paymentOptionsRouter.put('/', requireAuth(['admin']), zValidator('json', updateP
const now = getNow();
// Check if options exist
- const existing = await (db as any)
- .select()
- .from(paymentOptions)
- .get();
+ const existing = await dbGet(
+ (db as any)
+ .select()
+ .from(paymentOptions)
+ );
+ // Convert boolean fields for database compatibility
+ const dbData = convertBooleansForDb(data);
+
if (existing) {
// Update existing
await (db as any)
.update(paymentOptions)
.set({
- ...data,
+ ...dbData,
updatedAt: now,
updatedBy: user.id,
})
@@ -112,16 +115,17 @@ paymentOptionsRouter.put('/', requireAuth(['admin']), zValidator('json', updateP
const id = generateId();
await (db as any).insert(paymentOptions).values({
id,
- ...data,
+ ...dbData,
updatedAt: now,
updatedBy: user.id,
});
}
- const updated = await (db as any)
- .select()
- .from(paymentOptions)
- .get();
+ const updated = await dbGet(
+ (db as any)
+ .select()
+ .from(paymentOptions)
+ );
return c.json({ paymentOptions: updated, message: 'Payment options updated successfully' });
});
@@ -131,28 +135,31 @@ paymentOptionsRouter.get('/event/:eventId', async (c) => {
const eventId = c.req.param('eventId');
// Get the event first to verify it exists
- const event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, eventId))
- .get();
+ const event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, eventId))
+ );
if (!event) {
return c.json({ error: 'Event not found' }, 404);
}
// Get global options
- const globalOptions = await (db as any)
- .select()
- .from(paymentOptions)
- .get();
+ const globalOptions = await dbGet(
+ (db as any)
+ .select()
+ .from(paymentOptions)
+ );
// Get event overrides
- const overrides = await (db as any)
- .select()
- .from(eventPaymentOverrides)
- .where(eq((eventPaymentOverrides as any).eventId, eventId))
- .get();
+ const overrides = await dbGet(
+ (db as any)
+ .select()
+ .from(eventPaymentOverrides)
+ .where(eq((eventPaymentOverrides as any).eventId, eventId))
+ );
// Merge global with overrides (override takes precedence if not null)
const defaults = {
@@ -206,11 +213,9 @@ paymentOptionsRouter.get('/event/:eventId', async (c) => {
paymentOptionsRouter.get('/event/:eventId/overrides', requireAuth(['admin', 'organizer']), async (c) => {
const eventId = c.req.param('eventId');
- const overrides = await (db as any)
- .select()
- .from(eventPaymentOverrides)
- .where(eq((eventPaymentOverrides as any).eventId, eventId))
- .get();
+ const overrides = await dbGet(
+ (db as any).select().from(eventPaymentOverrides).where(eq((eventPaymentOverrides as any).eventId, eventId))
+ );
return c.json({ overrides: overrides || null });
});
@@ -222,28 +227,27 @@ paymentOptionsRouter.put('/event/:eventId/overrides', requireAuth(['admin', 'org
const now = getNow();
// Verify event exists
- const event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, eventId))
- .get();
+ const event = await dbGet(
+ (db as any).select().from(events).where(eq((events as any).id, eventId))
+ );
if (!event) {
return c.json({ error: 'Event not found' }, 404);
}
// Check if overrides exist
- const existing = await (db as any)
- .select()
- .from(eventPaymentOverrides)
- .where(eq((eventPaymentOverrides as any).eventId, eventId))
- .get();
+ const existing = await dbGet(
+ (db as any).select().from(eventPaymentOverrides).where(eq((eventPaymentOverrides as any).eventId, eventId))
+ );
+ // Convert boolean fields for database compatibility
+ const dbData = convertBooleansForDb(data);
+
if (existing) {
await (db as any)
.update(eventPaymentOverrides)
.set({
- ...data,
+ ...dbData,
updatedAt: now,
})
.where(eq((eventPaymentOverrides as any).id, existing.id));
@@ -252,17 +256,18 @@ paymentOptionsRouter.put('/event/:eventId/overrides', requireAuth(['admin', 'org
await (db as any).insert(eventPaymentOverrides).values({
id,
eventId,
- ...data,
+ ...dbData,
createdAt: now,
updatedAt: now,
});
}
- const updated = await (db as any)
- .select()
- .from(eventPaymentOverrides)
- .where(eq((eventPaymentOverrides as any).eventId, eventId))
- .get();
+ const updated = await dbGet(
+ (db as any)
+ .select()
+ .from(eventPaymentOverrides)
+ .where(eq((eventPaymentOverrides as any).eventId, eventId))
+ );
return c.json({ overrides: updated, message: 'Event payment overrides updated successfully' });
});
diff --git a/backend/src/routes/payments.ts b/backend/src/routes/payments.ts
index 683bcab..f35e3a9 100644
--- a/backend/src/routes/payments.ts
+++ b/backend/src/routes/payments.ts
@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
-import { db, payments, tickets, events } from '../db/index.js';
+import { db, dbGet, dbAll, payments, tickets, events } from '../db/index.js';
import { eq, desc, and, or, sql } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { getNow } from '../lib/utils.js';
@@ -30,11 +30,12 @@ paymentsRouter.get('/', requireAuth(['admin']), async (c) => {
const pendingApproval = c.req.query('pendingApproval');
// Get all payments with their associated tickets
- let allPayments = await (db as any)
- .select()
- .from(payments)
- .orderBy(desc((payments as any).createdAt))
- .all();
+ let allPayments = await dbAll(
+ (db as any)
+ .select()
+ .from(payments)
+ .orderBy(desc((payments as any).createdAt))
+ );
// Filter by status
if (status) {
@@ -54,19 +55,21 @@ paymentsRouter.get('/', requireAuth(['admin']), async (c) => {
// Enrich with ticket and event data
const enrichedPayments = await Promise.all(
allPayments.map(async (payment: any) => {
- const ticket = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).id, payment.ticketId))
- .get();
-
- let event = null;
- if (ticket) {
- event = await (db as any)
+ const ticket = await dbGet(
+ (db as any)
.select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get();
+ .from(tickets)
+ .where(eq((tickets as any).id, payment.ticketId))
+ );
+
+ let event: any = null;
+ if (ticket) {
+ event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ );
}
return {
@@ -93,29 +96,32 @@ paymentsRouter.get('/', requireAuth(['admin']), async (c) => {
// Get payments pending approval (admin dashboard view)
paymentsRouter.get('/pending-approval', requireAuth(['admin', 'organizer']), async (c) => {
- const pendingPayments = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).status, 'pending_approval'))
- .orderBy(desc((payments as any).userMarkedPaidAt))
- .all();
+ const pendingPayments = await dbAll(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).status, 'pending_approval'))
+ .orderBy(desc((payments as any).userMarkedPaidAt))
+ );
// Enrich with ticket and event data
const enrichedPayments = await Promise.all(
pendingPayments.map(async (payment: any) => {
- const ticket = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).id, payment.ticketId))
- .get();
-
- let event = null;
- if (ticket) {
- event = await (db as any)
+ const ticket = await dbGet(
+ (db as any)
.select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get();
+ .from(tickets)
+ .where(eq((tickets as any).id, payment.ticketId))
+ );
+
+ let event: any = null;
+ if (ticket) {
+ event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ );
}
return {
@@ -144,22 +150,24 @@ paymentsRouter.get('/pending-approval', requireAuth(['admin', 'organizer']), asy
paymentsRouter.get('/:id', requireAuth(['admin', 'organizer']), async (c) => {
const id = c.req.param('id');
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).id, id))
- .get();
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).id, id))
+ );
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
}
// Get associated ticket
- const ticket = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).id, payment.ticketId))
- .get();
+ const ticket = await dbGet(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).id, payment.ticketId))
+ );
return c.json({ payment: { ...payment, ticket } });
});
@@ -170,11 +178,12 @@ paymentsRouter.put('/:id', requireAuth(['admin', 'organizer']), zValidator('json
const data = c.req.valid('json');
const user = (c as any).get('user');
- const existing = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).id, id))
- .get();
+ const existing = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).id, id))
+ );
if (!existing) {
return c.json({ error: 'Payment not found' }, 404);
@@ -211,11 +220,12 @@ paymentsRouter.put('/:id', requireAuth(['admin', 'organizer']), zValidator('json
});
}
- const updated = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).id, id))
- .get();
+ const updated = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).id, id))
+ );
return c.json({ payment: updated });
});
@@ -226,11 +236,12 @@ paymentsRouter.post('/:id/approve', requireAuth(['admin', 'organizer']), zValida
const { adminNote } = c.req.valid('json');
const user = (c as any).get('user');
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).id, id))
- .get();
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).id, id))
+ );
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
@@ -269,11 +280,12 @@ paymentsRouter.post('/:id/approve', requireAuth(['admin', 'organizer']), zValida
console.error('[Email] Failed to send confirmation emails:', err);
});
- const updated = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).id, id))
- .get();
+ const updated = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).id, id))
+ );
return c.json({ payment: updated, message: 'Payment approved successfully' });
});
@@ -284,11 +296,12 @@ paymentsRouter.post('/:id/reject', requireAuth(['admin', 'organizer']), zValidat
const { adminNote } = c.req.valid('json');
const user = (c as any).get('user');
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).id, id))
- .get();
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).id, id))
+ );
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
@@ -327,11 +340,12 @@ paymentsRouter.post('/:id/reject', requireAuth(['admin', 'organizer']), zValidat
});
}
- const updated = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).id, id))
- .get();
+ const updated = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).id, id))
+ );
return c.json({ payment: updated, message: 'Payment rejected and booking cancelled' });
});
@@ -342,11 +356,12 @@ paymentsRouter.post('/:id/note', requireAuth(['admin', 'organizer']), async (c)
const body = await c.req.json();
const { adminNote } = body;
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).id, id))
- .get();
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).id, id))
+ );
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
@@ -362,11 +377,12 @@ paymentsRouter.post('/:id/note', requireAuth(['admin', 'organizer']), async (c)
})
.where(eq((payments as any).id, id));
- const updated = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).id, id))
- .get();
+ const updated = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).id, id))
+ );
return c.json({ payment: updated, message: 'Note updated' });
});
@@ -375,11 +391,12 @@ paymentsRouter.post('/:id/note', requireAuth(['admin', 'organizer']), async (c)
paymentsRouter.post('/:id/refund', requireAuth(['admin']), async (c) => {
const id = c.req.param('id');
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).id, id))
- .get();
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).id, id))
+ );
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
@@ -426,7 +443,7 @@ paymentsRouter.post('/webhook', async (c) => {
// Get payment statistics (admin)
paymentsRouter.get('/stats/overview', requireAuth(['admin']), async (c) => {
- const allPayments = await (db as any).select().from(payments).all();
+ const allPayments = await dbAll((db as any).select().from(payments));
const stats = {
total: allPayments.length,
diff --git a/backend/src/routes/site-settings.ts b/backend/src/routes/site-settings.ts
index 274dc1c..bc63fc7 100644
--- a/backend/src/routes/site-settings.ts
+++ b/backend/src/routes/site-settings.ts
@@ -1,10 +1,10 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
-import { db, siteSettings } from '../db/index.js';
+import { db, dbGet, siteSettings } from '../db/index.js';
import { eq } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
-import { generateId, getNow } from '../lib/utils.js';
+import { generateId, getNow, toDbBool } from '../lib/utils.js';
interface UserContext {
id: string;
@@ -34,7 +34,9 @@ const updateSiteSettingsSchema = z.object({
// Get site settings (public - needed for frontend timezone)
siteSettingsRouter.get('/', async (c) => {
- const settings = await (db as any).select().from(siteSettings).limit(1).get();
+ const settings = await dbGet(
+ (db as any).select().from(siteSettings).limit(1)
+ );
if (!settings) {
// Return default settings if none exist
@@ -95,7 +97,9 @@ siteSettingsRouter.put('/', requireAuth(['admin']), zValidator('json', updateSit
const now = getNow();
// Check if settings exist
- const existing = await (db as any).select().from(siteSettings).limit(1).get();
+ const existing = await dbGet(
+ (db as any).select().from(siteSettings).limit(1)
+ );
if (!existing) {
// Create new settings record
@@ -112,7 +116,7 @@ siteSettingsRouter.put('/', requireAuth(['admin']), zValidator('json', updateSit
instagramUrl: data.instagramUrl || null,
twitterUrl: data.twitterUrl || null,
linkedinUrl: data.linkedinUrl || null,
- maintenanceMode: data.maintenanceMode || false,
+ maintenanceMode: toDbBool(data.maintenanceMode || false),
maintenanceMessage: data.maintenanceMessage || null,
maintenanceMessageEs: data.maintenanceMessageEs || null,
updatedAt: now,
@@ -125,18 +129,24 @@ siteSettingsRouter.put('/', requireAuth(['admin']), zValidator('json', updateSit
}
// Update existing settings
- const updateData = {
+ const updateData: Record = {
...data,
updatedAt: now,
updatedBy: user.id,
};
+ // Convert maintenanceMode boolean to appropriate format for database
+ if (typeof data.maintenanceMode === 'boolean') {
+ updateData.maintenanceMode = toDbBool(data.maintenanceMode);
+ }
await (db as any)
.update(siteSettings)
.set(updateData)
.where(eq((siteSettings as any).id, existing.id));
- const updated = await (db as any).select().from(siteSettings).where(eq((siteSettings as any).id, existing.id)).get();
+ const updated = await dbGet(
+ (db as any).select().from(siteSettings).where(eq((siteSettings as any).id, existing.id))
+ );
return c.json({ settings: updated, message: 'Settings updated successfully' });
});
diff --git a/backend/src/routes/tickets.ts b/backend/src/routes/tickets.ts
index 9eb41a7..ce7c0ff 100644
--- a/backend/src/routes/tickets.ts
+++ b/backend/src/routes/tickets.ts
@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
-import { db, tickets, events, users, payments, paymentOptions } from '../db/index.js';
+import { db, dbGet, dbAll, tickets, events, users, payments, paymentOptions } from '../db/index.js';
import { eq, and, sql } from 'drizzle-orm';
import { requireAuth, getAuthUser } from '../lib/auth.js';
import { generateId, generateTicketCode, getNow } from '../lib/utils.js';
@@ -47,7 +47,9 @@ ticketsRouter.post('/', zValidator('json', createTicketSchema), async (c) => {
const data = c.req.valid('json');
// Get event
- const event = await (db as any).select().from(events).where(eq((events as any).id, data.eventId)).get();
+ const event = await dbGet(
+ (db as any).select().from(events).where(eq((events as any).id, data.eventId))
+ );
if (!event) {
return c.json({ error: 'Event not found' }, 404);
}
@@ -57,23 +59,26 @@ ticketsRouter.post('/', zValidator('json', createTicketSchema), async (c) => {
}
// Check capacity
- const ticketCount = await (db as any)
- .select({ count: sql`count(*)` })
- .from(tickets)
- .where(
- and(
- eq((tickets as any).eventId, data.eventId),
- eq((tickets as any).status, 'confirmed')
+ const ticketCount = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(tickets)
+ .where(
+ and(
+ eq((tickets as any).eventId, data.eventId),
+ eq((tickets as any).status, 'confirmed')
+ )
)
- )
- .get();
+ );
if ((ticketCount?.count || 0) >= event.capacity) {
return c.json({ error: 'Event is sold out' }, 400);
}
// Find or create user
- let user = await (db as any).select().from(users).where(eq((users as any).email, data.email)).get();
+ let user = await dbGet(
+ (db as any).select().from(users).where(eq((users as any).email, data.email))
+ );
const now = getNow();
@@ -98,24 +103,26 @@ ticketsRouter.post('/', zValidator('json', createTicketSchema), async (c) => {
}
// Check for duplicate booking (unless allowDuplicateBookings is enabled)
- const globalOptions = await (db as any)
- .select()
- .from(paymentOptions)
- .get();
+ const globalOptions = await dbGet(
+ (db as any)
+ .select()
+ .from(paymentOptions)
+ );
const allowDuplicateBookings = globalOptions?.allowDuplicateBookings ?? false;
if (!allowDuplicateBookings) {
- const existingTicket = await (db as any)
- .select()
- .from(tickets)
- .where(
- and(
- eq((tickets as any).userId, user.id),
- eq((tickets as any).eventId, data.eventId)
+ const existingTicket = await dbGet(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(
+ and(
+ eq((tickets as any).userId, user.id),
+ eq((tickets as any).eventId, data.eventId)
+ )
)
- )
- .get();
+ );
if (existingTicket && existingTicket.status !== 'cancelled') {
return c.json({ error: 'You have already booked this event' }, 400);
@@ -251,9 +258,11 @@ ticketsRouter.post('/', zValidator('json', createTicketSchema), async (c) => {
// Download ticket as PDF
ticketsRouter.get('/:id/pdf', async (c) => {
const id = c.req.param('id');
- const user = await getAuthUser(c);
+ const user: any = await getAuthUser(c);
- const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
+ const ticket = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, id))
+ );
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -278,7 +287,9 @@ ticketsRouter.get('/:id/pdf', async (c) => {
}
// Get event
- const event = await (db as any).select().from(events).where(eq((events as any).id, ticket.eventId)).get();
+ const event = await dbGet(
+ (db as any).select().from(events).where(eq((events as any).id, ticket.eventId))
+ );
if (!event) {
return c.json({ error: 'Event not found' }, 404);
@@ -316,17 +327,23 @@ ticketsRouter.get('/:id/pdf', async (c) => {
ticketsRouter.get('/:id', async (c) => {
const id = c.req.param('id');
- const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
+ const ticket = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, id))
+ );
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
}
// Get associated event
- const event = await (db as any).select().from(events).where(eq((events as any).id, ticket.eventId)).get();
+ const event = await dbGet(
+ (db as any).select().from(events).where(eq((events as any).id, ticket.eventId))
+ );
// Get payment
- const payment = await (db as any).select().from(payments).where(eq((payments as any).ticketId, id)).get();
+ const payment = await dbGet(
+ (db as any).select().from(payments).where(eq((payments as any).ticketId, id))
+ );
return c.json({
ticket: {
@@ -342,7 +359,9 @@ ticketsRouter.put('/:id', requireAuth(['admin', 'organizer', 'staff']), zValidat
const id = c.req.param('id');
const data = c.req.valid('json');
- const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
+ const ticket = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, id))
+ );
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -361,7 +380,9 @@ ticketsRouter.put('/:id', requireAuth(['admin', 'organizer', 'staff']), zValidat
await (db as any).update(tickets).set(updates).where(eq((tickets as any).id, id));
}
- const updated = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
+ const updated = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, id))
+ );
return c.json({ ticket: updated });
});
@@ -376,19 +397,21 @@ ticketsRouter.post('/validate', requireAuth(['admin', 'organizer', 'staff']), as
}
// Try to find ticket by QR code or ID
- let ticket = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).qrCode, code))
- .get();
+ let ticket = await dbGet(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).qrCode, code))
+ );
// If not found by QR, try by ID
if (!ticket) {
- ticket = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).id, code))
- .get();
+ ticket = await dbGet(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).id, code))
+ );
}
if (!ticket) {
@@ -409,11 +432,12 @@ ticketsRouter.post('/validate', requireAuth(['admin', 'organizer', 'staff']), as
}
// Get event details
- const event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get();
+ const event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ );
// Determine validity status
let validityStatus = 'invalid';
@@ -433,11 +457,12 @@ ticketsRouter.post('/validate', requireAuth(['admin', 'organizer', 'staff']), as
// Get admin who checked in (if applicable)
let checkedInBy = null;
if (ticket.checkedInByAdminId) {
- const admin = await (db as any)
- .select()
- .from(users)
- .where(eq((users as any).id, ticket.checkedInByAdminId))
- .get();
+ const admin = await dbGet(
+ (db as any)
+ .select()
+ .from(users)
+ .where(eq((users as any).id, ticket.checkedInByAdminId))
+ );
checkedInBy = admin ? admin.name : null;
}
@@ -469,7 +494,9 @@ ticketsRouter.post('/:id/checkin', requireAuth(['admin', 'organizer', 'staff']),
const id = c.req.param('id');
const adminUser = (c as any).get('user');
- const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
+ const ticket = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, id))
+ );
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -494,10 +521,14 @@ ticketsRouter.post('/:id/checkin', requireAuth(['admin', 'organizer', 'staff']),
})
.where(eq((tickets as any).id, id));
- const updated = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
+ const updated = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, id))
+ );
// Get event for response
- const event = await (db as any).select().from(events).where(eq((events as any).id, ticket.eventId)).get();
+ const event = await dbGet(
+ (db as any).select().from(events).where(eq((events as any).id, ticket.eventId))
+ );
return c.json({
ticket: {
@@ -517,7 +548,9 @@ ticketsRouter.post('/:id/mark-paid', requireAuth(['admin', 'organizer', 'staff']
const id = c.req.param('id');
const user = (c as any).get('user');
- const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
+ const ticket = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, id))
+ );
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -551,11 +584,12 @@ ticketsRouter.post('/:id/mark-paid', requireAuth(['admin', 'organizer', 'staff']
.where(eq((payments as any).ticketId, id));
// Get payment for sending receipt
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).ticketId, id))
- .get();
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).ticketId, id))
+ );
// Send confirmation emails asynchronously (don't block the response)
Promise.all([
@@ -565,7 +599,9 @@ ticketsRouter.post('/:id/mark-paid', requireAuth(['admin', 'organizer', 'staff']
console.error('[Email] Failed to send confirmation emails:', err);
});
- const updated = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
+ const updated = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, id))
+ );
return c.json({ ticket: updated, message: 'Payment marked as received' });
});
@@ -575,18 +611,21 @@ ticketsRouter.post('/:id/mark-paid', requireAuth(['admin', 'organizer', 'staff']
ticketsRouter.post('/:id/mark-payment-sent', async (c) => {
const id = c.req.param('id');
- const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
+ const ticket = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, id))
+ );
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
}
// Get the payment
- const payment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).ticketId, id))
- .get();
+ const payment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).ticketId, id))
+ );
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
@@ -632,11 +671,12 @@ ticketsRouter.post('/:id/mark-payment-sent', async (c) => {
.where(eq((payments as any).id, payment.id));
// Get updated payment
- const updatedPayment = await (db as any)
- .select()
- .from(payments)
- .where(eq((payments as any).id, payment.id))
- .get();
+ const updatedPayment = await dbGet(
+ (db as any)
+ .select()
+ .from(payments)
+ .where(eq((payments as any).id, payment.id))
+ );
// TODO: Send notification to admin about pending payment approval
@@ -649,9 +689,11 @@ ticketsRouter.post('/:id/mark-payment-sent', async (c) => {
// Cancel ticket
ticketsRouter.post('/:id/cancel', async (c) => {
const id = c.req.param('id');
- const user = await getAuthUser(c);
+ const user: any = await getAuthUser(c);
- const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
+ const ticket = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, id))
+ );
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -675,7 +717,9 @@ ticketsRouter.post('/:id/cancel', async (c) => {
ticketsRouter.post('/:id/remove-checkin', requireAuth(['admin', 'organizer', 'staff']), async (c) => {
const id = c.req.param('id');
- const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
+ const ticket = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, id))
+ );
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -690,7 +734,9 @@ ticketsRouter.post('/:id/remove-checkin', requireAuth(['admin', 'organizer', 'st
.set({ status: 'confirmed', checkinAt: null })
.where(eq((tickets as any).id, id));
- const updated = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
+ const updated = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, id))
+ );
return c.json({ ticket: updated, message: 'Check-in removed successfully' });
});
@@ -700,7 +746,9 @@ ticketsRouter.post('/:id/note', requireAuth(['admin', 'organizer', 'staff']), zV
const id = c.req.param('id');
const { note } = c.req.valid('json');
- const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
+ const ticket = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, id))
+ );
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -711,7 +759,9 @@ ticketsRouter.post('/:id/note', requireAuth(['admin', 'organizer', 'staff']), zV
.set({ adminNote: note || null })
.where(eq((tickets as any).id, id));
- const updated = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
+ const updated = await dbGet(
+ (db as any).select().from(tickets).where(eq((tickets as any).id, id))
+ );
return c.json({ ticket: updated, message: 'Note updated successfully' });
});
@@ -721,22 +771,25 @@ ticketsRouter.post('/admin/create', requireAuth(['admin', 'organizer', 'staff'])
const data = c.req.valid('json');
// Get event
- const event = await (db as any).select().from(events).where(eq((events as any).id, data.eventId)).get();
+ const event = await dbGet(
+ (db as any).select().from(events).where(eq((events as any).id, data.eventId))
+ );
if (!event) {
return c.json({ error: 'Event not found' }, 404);
}
// Check capacity
- const ticketCount = await (db as any)
- .select({ count: sql`count(*)` })
- .from(tickets)
- .where(
- and(
- eq((tickets as any).eventId, data.eventId),
- sql`${(tickets as any).status} IN ('confirmed', 'checked_in')`
+ const ticketCount = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(tickets)
+ .where(
+ and(
+ eq((tickets as any).eventId, data.eventId),
+ sql`${(tickets as any).status} IN ('confirmed', 'checked_in')`
+ )
)
- )
- .get();
+ );
if ((ticketCount?.count || 0) >= event.capacity) {
return c.json({ error: 'Event is at capacity' }, 400);
@@ -750,7 +803,9 @@ ticketsRouter.post('/admin/create', requireAuth(['admin', 'organizer', 'staff'])
: `door-${generateId()}@doorentry.local`;
// Find or create user
- let user = await (db as any).select().from(users).where(eq((users as any).email, attendeeEmail)).get();
+ let user = await dbGet(
+ (db as any).select().from(users).where(eq((users as any).email, attendeeEmail))
+ );
const adminFullName = data.lastName && data.lastName.trim()
? `${data.firstName} ${data.lastName}`.trim()
@@ -774,16 +829,17 @@ ticketsRouter.post('/admin/create', requireAuth(['admin', 'organizer', 'staff'])
// Check for existing active ticket for this user and event (only if real email provided)
if (data.email && data.email.trim() && !data.email.includes('@doorentry.local')) {
- const existingTicket = await (db as any)
- .select()
- .from(tickets)
- .where(
- and(
- eq((tickets as any).userId, user.id),
- eq((tickets as any).eventId, data.eventId)
+ const existingTicket = await dbGet(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(
+ and(
+ eq((tickets as any).userId, user.id),
+ eq((tickets as any).eventId, data.eventId)
+ )
)
- )
- .get();
+ );
if (existingTicket && existingTicket.status !== 'cancelled') {
return c.json({ error: 'This person already has a ticket for this event' }, 400);
@@ -869,7 +925,7 @@ ticketsRouter.get('/', requireAuth(['admin', 'organizer']), async (c) => {
query = query.where(and(...conditions));
}
- const result = await query.all();
+ const result = await dbAll(query);
return c.json({ tickets: result });
});
diff --git a/backend/src/routes/users.ts b/backend/src/routes/users.ts
index e2d90fb..7a6c64b 100644
--- a/backend/src/routes/users.ts
+++ b/backend/src/routes/users.ts
@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
-import { db, users, tickets, events, payments } from '../db/index.js';
+import { db, dbGet, dbAll, users, tickets, events, payments } from '../db/index.js';
import { eq, desc, sql } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { getNow } from '../lib/utils.js';
@@ -40,7 +40,7 @@ usersRouter.get('/', requireAuth(['admin']), async (c) => {
query = query.where(eq((users as any).role, role));
}
- const result = await query.orderBy(desc((users as any).createdAt)).all();
+ const result = await dbAll(query.orderBy(desc((users as any).createdAt)));
return c.json({ users: result });
});
@@ -55,19 +55,20 @@ usersRouter.get('/:id', requireAuth(['admin', 'organizer', 'staff', 'marketing',
return c.json({ error: 'Forbidden' }, 403);
}
- const user = await (db as any)
- .select({
- id: (users as any).id,
- email: (users as any).email,
- name: (users as any).name,
- phone: (users as any).phone,
- role: (users as any).role,
- languagePreference: (users as any).languagePreference,
- createdAt: (users as any).createdAt,
- })
- .from(users)
- .where(eq((users as any).id, id))
- .get();
+ const user = await dbGet(
+ (db as any)
+ .select({
+ id: (users as any).id,
+ email: (users as any).email,
+ name: (users as any).name,
+ phone: (users as any).phone,
+ role: (users as any).role,
+ languagePreference: (users as any).languagePreference,
+ createdAt: (users as any).createdAt,
+ })
+ .from(users)
+ .where(eq((users as any).id, id))
+ );
if (!user) {
return c.json({ error: 'User not found' }, 404);
@@ -92,7 +93,9 @@ usersRouter.put('/:id', requireAuth(['admin', 'organizer', 'staff', 'marketing',
delete data.role;
}
- const existing = await (db as any).select().from(users).where(eq((users as any).id, id)).get();
+ const existing = await dbGet(
+ (db as any).select().from(users).where(eq((users as any).id, id))
+ );
if (!existing) {
return c.json({ error: 'User not found' }, 404);
}
@@ -102,18 +105,19 @@ usersRouter.put('/:id', requireAuth(['admin', 'organizer', 'staff', 'marketing',
.set({ ...data, updatedAt: getNow() })
.where(eq((users as any).id, id));
- const updated = await (db as any)
- .select({
- id: (users as any).id,
- email: (users as any).email,
- name: (users as any).name,
- phone: (users as any).phone,
- role: (users as any).role,
- languagePreference: (users as any).languagePreference,
- })
- .from(users)
- .where(eq((users as any).id, id))
- .get();
+ const updated = await dbGet(
+ (db as any)
+ .select({
+ id: (users as any).id,
+ email: (users as any).email,
+ name: (users as any).name,
+ phone: (users as any).phone,
+ role: (users as any).role,
+ languagePreference: (users as any).languagePreference,
+ })
+ .from(users)
+ .where(eq((users as any).id, id))
+ );
return c.json({ user: updated });
});
@@ -128,21 +132,23 @@ usersRouter.get('/:id/history', requireAuth(['admin', 'organizer', 'staff', 'mar
return c.json({ error: 'Forbidden' }, 403);
}
- const userTickets = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).userId, id))
- .orderBy(desc((tickets as any).createdAt))
- .all();
+ const userTickets = await dbAll(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).userId, id))
+ .orderBy(desc((tickets as any).createdAt))
+ );
// Get event details for each ticket
const history = await Promise.all(
userTickets.map(async (ticket: any) => {
- const event = await (db as any)
- .select()
- .from(events)
- .where(eq((events as any).id, ticket.eventId))
- .get();
+ const event = await dbGet(
+ (db as any)
+ .select()
+ .from(events)
+ .where(eq((events as any).id, ticket.eventId))
+ );
return {
...ticket,
@@ -164,7 +170,9 @@ usersRouter.delete('/:id', requireAuth(['admin']), async (c) => {
return c.json({ error: 'Cannot delete your own account' }, 400);
}
- const existing = await (db as any).select().from(users).where(eq((users as any).id, id)).get();
+ const existing = await dbGet(
+ (db as any).select().from(users).where(eq((users as any).id, id))
+ );
if (!existing) {
return c.json({ error: 'User not found' }, 404);
}
@@ -176,11 +184,12 @@ usersRouter.delete('/:id', requireAuth(['admin']), async (c) => {
try {
// Get all tickets for this user
- const userTickets = await (db as any)
- .select()
- .from(tickets)
- .where(eq((tickets as any).userId, id))
- .all();
+ const userTickets = await dbAll(
+ (db as any)
+ .select()
+ .from(tickets)
+ .where(eq((tickets as any).userId, id))
+ );
// Delete payments associated with user's tickets
for (const ticket of userTickets) {
@@ -202,16 +211,18 @@ usersRouter.delete('/:id', requireAuth(['admin']), async (c) => {
// Get user statistics (admin)
usersRouter.get('/stats/overview', requireAuth(['admin']), async (c) => {
- const totalUsers = await (db as any)
- .select({ count: sql`count(*)` })
- .from(users)
- .get();
+ const totalUsers = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(users)
+ );
- const adminCount = await (db as any)
- .select({ count: sql`count(*)` })
- .from(users)
- .where(eq((users as any).role, 'admin'))
- .get();
+ const adminCount = await dbGet(
+ (db as any)
+ .select({ count: sql`count(*)` })
+ .from(users)
+ .where(eq((users as any).role, 'admin'))
+ );
return c.json({
stats: {
diff --git a/frontend/package.json b/frontend/package.json
index 9e942c9..16fb5ed 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,6 +10,10 @@
},
"dependencies": {
"@heroicons/react": "^2.1.4",
+ "@tiptap/extension-placeholder": "^3.18.0",
+ "@tiptap/pm": "^3.18.0",
+ "@tiptap/react": "^3.18.0",
+ "@tiptap/starter-kit": "^3.18.0",
"clsx": "^2.1.1",
"html5-qrcode": "^2.3.8",
"next": "^14.2.4",
diff --git a/frontend/public/images/carrousel/2026-01-29 13.09.59.jpg b/frontend/public/images/carrousel/2026-01-29 13.09.59.jpg
deleted file mode 100644
index a986426..0000000
Binary files a/frontend/public/images/carrousel/2026-01-29 13.09.59.jpg and /dev/null differ
diff --git a/frontend/public/images/carrousel/2026-01-29 13.10.12.jpg b/frontend/public/images/carrousel/2026-01-29 13.10.12.jpg
deleted file mode 100644
index 5ef68d4..0000000
Binary files a/frontend/public/images/carrousel/2026-01-29 13.10.12.jpg and /dev/null differ
diff --git a/frontend/public/images/carrousel/2026-01-29 13.10.16.jpg b/frontend/public/images/carrousel/2026-01-29 13.10.16.jpg
deleted file mode 100644
index 7dbb84f..0000000
Binary files a/frontend/public/images/carrousel/2026-01-29 13.10.16.jpg and /dev/null differ
diff --git a/frontend/public/images/carrousel/2026-01-29 13.10.20.jpg b/frontend/public/images/carrousel/2026-01-29 13.10.20.jpg
deleted file mode 100644
index 7e0563e..0000000
Binary files a/frontend/public/images/carrousel/2026-01-29 13.10.20.jpg and /dev/null differ
diff --git a/frontend/public/images/carrousel/2026-02-01 02.53.00.jpg b/frontend/public/images/carrousel/2026-02-01 02.53.00.jpg
deleted file mode 100644
index e0c8e7d..0000000
Binary files a/frontend/public/images/carrousel/2026-02-01 02.53.00.jpg and /dev/null differ
diff --git a/frontend/public/images/carrousel/2026-02-01 02.53.30.jpg b/frontend/public/images/carrousel/2026-02-01 02.53.30.jpg
deleted file mode 100644
index 5eda3f7..0000000
Binary files a/frontend/public/images/carrousel/2026-02-01 02.53.30.jpg and /dev/null differ
diff --git a/frontend/public/images/carrousel/2026-02-01 02.53.36.jpg b/frontend/public/images/carrousel/2026-02-01 02.53.36.jpg
deleted file mode 100644
index eca6c29..0000000
Binary files a/frontend/public/images/carrousel/2026-02-01 02.53.36.jpg and /dev/null differ
diff --git a/frontend/public/images/carrousel/2026-02-01 02.53.41.jpg b/frontend/public/images/carrousel/2026-02-01 02.53.41.jpg
deleted file mode 100644
index 6645467..0000000
Binary files a/frontend/public/images/carrousel/2026-02-01 02.53.41.jpg and /dev/null differ
diff --git a/frontend/public/images/carrousel/2026-02-01 02.53.45.jpg b/frontend/public/images/carrousel/2026-02-01 02.53.45.jpg
deleted file mode 100644
index 573dcc6..0000000
Binary files a/frontend/public/images/carrousel/2026-02-01 02.53.45.jpg and /dev/null differ
diff --git a/frontend/public/images/carrousel/2026-02-01 02.53.48.jpg b/frontend/public/images/carrousel/2026-02-01 02.53.48.jpg
deleted file mode 100644
index bd555df..0000000
Binary files a/frontend/public/images/carrousel/2026-02-01 02.53.48.jpg and /dev/null differ
diff --git a/frontend/public/images/carrousel/2026-02-02 00.33.56.jpg b/frontend/public/images/carrousel/2026-02-02 00.33.56.jpg
new file mode 100644
index 0000000..94d4c43
Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-02 00.33.56.jpg differ
diff --git a/frontend/public/images/carrousel/2026-02-02 00.34.12.jpg b/frontend/public/images/carrousel/2026-02-02 00.34.12.jpg
new file mode 100644
index 0000000..f2ae9ba
Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-02 00.34.12.jpg differ
diff --git a/frontend/public/images/carrousel/2026-02-02 00.34.15.jpg b/frontend/public/images/carrousel/2026-02-02 00.34.15.jpg
new file mode 100644
index 0000000..7df1aff
Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-02 00.34.15.jpg differ
diff --git a/frontend/public/images/carrousel/2026-02-02 00.34.18.jpg b/frontend/public/images/carrousel/2026-02-02 00.34.18.jpg
new file mode 100644
index 0000000..a57b697
Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-02 00.34.18.jpg differ
diff --git a/frontend/public/images/carrousel/2026-02-02 00.34.21.jpg b/frontend/public/images/carrousel/2026-02-02 00.34.21.jpg
new file mode 100644
index 0000000..567f703
Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-02 00.34.21.jpg differ
diff --git a/frontend/public/images/carrousel/2026-02-02 00.34.23.jpg b/frontend/public/images/carrousel/2026-02-02 00.34.23.jpg
new file mode 100644
index 0000000..58433c2
Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-02 00.34.23.jpg differ
diff --git a/frontend/public/images/carrousel/2026-02-02 00.34.26.jpg b/frontend/public/images/carrousel/2026-02-02 00.34.26.jpg
new file mode 100644
index 0000000..34383a0
Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-02 00.34.26.jpg differ
diff --git a/frontend/public/images/carrousel/2026-02-02 00.34.28.jpg b/frontend/public/images/carrousel/2026-02-02 00.34.28.jpg
new file mode 100644
index 0000000..1117acd
Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-02 00.34.28.jpg differ
diff --git a/frontend/public/images/carrousel/2026-02-02 00.34.31.jpg b/frontend/public/images/carrousel/2026-02-02 00.34.31.jpg
new file mode 100644
index 0000000..66c721f
Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-02 00.34.31.jpg differ
diff --git a/frontend/public/images/carrousel/2026-02-02 00.34.33.jpg b/frontend/public/images/carrousel/2026-02-02 00.34.33.jpg
new file mode 100644
index 0000000..aa29cc9
Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-02 00.34.33.jpg differ
diff --git a/frontend/public/images/carrousel/2026-02-02 00.34.38.jpg b/frontend/public/images/carrousel/2026-02-02 00.34.38.jpg
new file mode 100644
index 0000000..7f3ced3
Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-02 00.34.38.jpg differ
diff --git a/frontend/public/images/carrousel/2026-02-02 00.34.40.jpg b/frontend/public/images/carrousel/2026-02-02 00.34.40.jpg
new file mode 100644
index 0000000..f5f43ef
Binary files /dev/null and b/frontend/public/images/carrousel/2026-02-02 00.34.40.jpg differ
diff --git a/frontend/src/app/(public)/book/[eventId]/page.tsx b/frontend/src/app/(public)/book/[eventId]/page.tsx
index e66d20a..dbe19ae 100644
--- a/frontend/src/app/(public)/book/[eventId]/page.tsx
+++ b/frontend/src/app/(public)/book/[eventId]/page.tsx
@@ -6,6 +6,7 @@ import Link from 'next/link';
import { useLanguage } from '@/context/LanguageContext';
import { useAuth } from '@/context/AuthContext';
import { eventsApi, ticketsApi, paymentOptionsApi, Event, PaymentOptionsConfig } from '@/lib/api';
+import { formatPrice } from '@/lib/utils';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
@@ -620,7 +621,7 @@ export default function BookingPage() {
{locale === 'es' ? 'Monto a pagar' : 'Amount to pay'}
- {event?.price?.toLocaleString()} {event?.currency}
+ {event?.price !== undefined ? formatPrice(event.price, event.currency) : ''}
@@ -924,7 +925,7 @@ export default function BookingPage() {
{event.price === 0
? t('events.details.free')
- : `${event.price.toLocaleString()} ${event.currency}`}
+ : formatPrice(event.price, event.currency)}
diff --git a/frontend/src/app/(public)/booking/[ticketId]/page.tsx b/frontend/src/app/(public)/booking/[ticketId]/page.tsx
index b25bfcd..3e5b291 100644
--- a/frontend/src/app/(public)/booking/[ticketId]/page.tsx
+++ b/frontend/src/app/(public)/booking/[ticketId]/page.tsx
@@ -5,6 +5,7 @@ import { useParams, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { useLanguage } from '@/context/LanguageContext';
import { ticketsApi, paymentOptionsApi, Ticket, PaymentOptionsConfig } from '@/lib/api';
+import { formatPrice } from '@/lib/utils';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import {
@@ -341,7 +342,7 @@ export default function BookingPaymentPage() {
- {ticket.event.price?.toLocaleString()} {ticket.event.currency}
+ {ticket.event.price !== undefined ? formatPrice(ticket.event.price, ticket.event.currency) : ''}
@@ -374,7 +375,7 @@ export default function BookingPaymentPage() {
{locale === 'es' ? 'Monto a pagar' : 'Amount to pay'}
- {ticket.event?.price?.toLocaleString()} {ticket.event?.currency}
+ {ticket.event?.price !== undefined ? formatPrice(ticket.event.price, ticket.event.currency) : ''}
diff --git a/frontend/src/app/(public)/components/NextEventSection.tsx b/frontend/src/app/(public)/components/NextEventSection.tsx
index c24989c..d306795 100644
--- a/frontend/src/app/(public)/components/NextEventSection.tsx
+++ b/frontend/src/app/(public)/components/NextEventSection.tsx
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
import Link from 'next/link';
import { useLanguage } from '@/context/LanguageContext';
import { eventsApi, Event } from '@/lib/api';
+import { formatPrice } from '@/lib/utils';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import { CalendarIcon, MapPinIcon } from '@heroicons/react/24/outline';
@@ -91,7 +92,7 @@ export default function NextEventSection() {
{nextEvent.price === 0
? t('events.details.free')
- : `${nextEvent.price.toLocaleString()} ${nextEvent.currency}`}
+ : formatPrice(nextEvent.price, nextEvent.currency)}
{!nextEvent.externalBookingEnabled && (
diff --git a/frontend/src/app/(public)/events/[id]/EventDetailClient.tsx b/frontend/src/app/(public)/events/[id]/EventDetailClient.tsx
index 9a6e7b5..55d8a7c 100644
--- a/frontend/src/app/(public)/events/[id]/EventDetailClient.tsx
+++ b/frontend/src/app/(public)/events/[id]/EventDetailClient.tsx
@@ -5,6 +5,7 @@ import Link from 'next/link';
import Image from 'next/image';
import { useLanguage } from '@/context/LanguageContext';
import { eventsApi, Event } from '@/lib/api';
+import { formatPrice } from '@/lib/utils';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import ShareButtons from '@/components/ShareButtons';
@@ -186,7 +187,7 @@ export default function EventDetailClient({ eventId, initialEvent }: EventDetail
{event.price === 0
? t('events.details.free')
- : `${event.price.toLocaleString()} ${event.currency}`}
+ : formatPrice(event.price, event.currency)}
diff --git a/frontend/src/app/(public)/events/page.tsx b/frontend/src/app/(public)/events/page.tsx
index 4f7910e..d70d3ba 100644
--- a/frontend/src/app/(public)/events/page.tsx
+++ b/frontend/src/app/(public)/events/page.tsx
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
import Link from 'next/link';
import { useLanguage } from '@/context/LanguageContext';
import { eventsApi, Event } from '@/lib/api';
+import { formatPrice } from '@/lib/utils';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import { CalendarIcon, MapPinIcon, UserGroupIcon } from '@heroicons/react/24/outline';
@@ -149,7 +150,7 @@ export default function EventsPage() {
{event.price === 0
? t('events.details.free')
- : `${event.price.toLocaleString()} ${event.currency}`}
+ : formatPrice(event.price, event.currency)}