first commit

This commit is contained in:
Tomas Dvorak
2026-04-10 12:01:36 +02:00
commit 035ac8ddb5
61 changed files with 6600 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
-- +goose Up
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
CREATE TABLE IF NOT EXISTS tenants (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
slug text NOT NULL UNIQUE,
name text NOT NULL,
preset text NOT NULL,
locale text NOT NULL DEFAULT 'cs',
timezone text NOT NULL DEFAULT 'Europe/Prague',
plan_code text NOT NULL DEFAULT 'starter',
subscription_status text NOT NULL DEFAULT 'trialing',
stripe_customer_id text,
stripe_subscription_id text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS users (
id uuid PRIMARY KEY,
email text NOT NULL,
display_name text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS tenant_users (
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (tenant_id, user_id)
);
CREATE TABLE IF NOT EXISTS locations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name text NOT NULL,
timezone text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS staff (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
location_id uuid REFERENCES locations(id) ON DELETE SET NULL,
display_name text NOT NULL,
active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS services (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name text NOT NULL,
duration_minutes integer NOT NULL,
buffer_before_minutes integer NOT NULL DEFAULT 0,
buffer_after_minutes integer NOT NULL DEFAULT 0,
price_cents integer NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS class_templates (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
title text NOT NULL,
duration_minutes integer NOT NULL,
capacity integer NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS class_sessions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
template_id uuid NOT NULL REFERENCES class_templates(id) ON DELETE CASCADE,
location_id uuid REFERENCES locations(id) ON DELETE SET NULL,
starts_at timestamptz NOT NULL,
ends_at timestamptz NOT NULL,
capacity integer NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS availability_rules (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
staff_id uuid REFERENCES staff(id) ON DELETE CASCADE,
day_of_week integer NOT NULL,
starts_local time NOT NULL,
ends_local time NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS availability_exceptions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
staff_id uuid REFERENCES staff(id) ON DELETE CASCADE,
starts_at timestamptz NOT NULL,
ends_at timestamptz NOT NULL,
kind text NOT NULL,
reason text,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS bookings (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
service_id uuid REFERENCES services(id) ON DELETE SET NULL,
class_session_id uuid REFERENCES class_sessions(id) ON DELETE SET NULL,
staff_id uuid REFERENCES staff(id) ON DELETE SET NULL,
location_id uuid REFERENCES locations(id) ON DELETE SET NULL,
booking_mode text NOT NULL,
customer_name text NOT NULL,
customer_email text NOT NULL,
starts_at timestamptz NOT NULL,
ends_at timestamptz NOT NULL,
status text NOT NULL,
reference text NOT NULL UNIQUE,
notes text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS waitlist_entries (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
class_session_id uuid NOT NULL REFERENCES class_sessions(id) ON DELETE CASCADE,
customer_name text NOT NULL,
customer_email text NOT NULL,
position integer NOT NULL,
status text NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS reminder_jobs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
booking_id uuid REFERENCES bookings(id) ON DELETE CASCADE,
channel text NOT NULL,
scheduled_for timestamptz NOT NULL,
dispatched_at timestamptz,
status text NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS subscription_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
stripe_event_id text NOT NULL UNIQUE,
event_type text NOT NULL,
payload jsonb NOT NULL,
processed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_bookings_tenant_time ON bookings (tenant_id, starts_at DESC);
CREATE INDEX IF NOT EXISTS idx_sessions_tenant_time ON class_sessions (tenant_id, starts_at DESC);
CREATE INDEX IF NOT EXISTS idx_reminder_jobs_pending ON reminder_jobs (scheduled_for) WHERE status = 'pending';
-- +goose Down
DROP TABLE IF EXISTS subscription_events;
DROP TABLE IF EXISTS reminder_jobs;
DROP TABLE IF EXISTS waitlist_entries;
DROP TABLE IF EXISTS bookings;
DROP TABLE IF EXISTS availability_exceptions;
DROP TABLE IF EXISTS availability_rules;
DROP TABLE IF EXISTS class_sessions;
DROP TABLE IF EXISTS class_templates;
DROP TABLE IF EXISTS services;
DROP TABLE IF EXISTS staff;
DROP TABLE IF EXISTS locations;
DROP TABLE IF EXISTS tenant_users;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS tenants;
+109
View File
@@ -0,0 +1,109 @@
-- +goose Up
INSERT INTO tenants (
id, slug, name, preset, locale, timezone, plan_code, subscription_status
) VALUES (
'5d6b3551-0a3e-4b86-bdf0-e9df20a47148',
'studio-atelier',
'Studio Atelier',
'studio',
'cs',
'Europe/Prague',
'growth',
'active'
)
ON CONFLICT (id) DO NOTHING;
INSERT INTO users (id, email, display_name)
VALUES (
'11111111-1111-1111-1111-111111111111',
'owner@bookra.dev',
'Bookra Demo Owner'
)
ON CONFLICT (id) DO NOTHING;
INSERT INTO tenant_users (tenant_id, user_id, role)
VALUES (
'5d6b3551-0a3e-4b86-bdf0-e9df20a47148',
'11111111-1111-1111-1111-111111111111',
'owner'
)
ON CONFLICT (tenant_id, user_id) DO NOTHING;
INSERT INTO locations (id, tenant_id, name, timezone)
VALUES (
'659f1cc0-a850-46d6-b3b8-cb15d55d8daf',
'5d6b3551-0a3e-4b86-bdf0-e9df20a47148',
'Prague Studio',
'Europe/Prague'
)
ON CONFLICT (id) DO NOTHING;
INSERT INTO staff (id, tenant_id, location_id, display_name)
VALUES (
'6936c444-c7d0-4a7d-b596-a9b72d2f4fc0',
'5d6b3551-0a3e-4b86-bdf0-e9df20a47148',
'659f1cc0-a850-46d6-b3b8-cb15d55d8daf',
'Lenka Atelier'
)
ON CONFLICT (id) DO NOTHING;
INSERT INTO services (id, tenant_id, name, duration_minutes, buffer_before_minutes, buffer_after_minutes, price_cents)
VALUES (
'd5d76a61-3d49-467c-8dd4-bf61ee754e39',
'5d6b3551-0a3e-4b86-bdf0-e9df20a47148',
'Signature treatment',
60,
0,
15,
120000
)
ON CONFLICT (id) DO NOTHING;
INSERT INTO class_templates (id, tenant_id, title, duration_minutes, capacity)
VALUES (
'd13fe5fd-727f-4d69-bfd8-47f1b92a2cf7',
'5d6b3551-0a3e-4b86-bdf0-e9df20a47148',
'Small group mobility class',
60,
4
)
ON CONFLICT (id) DO NOTHING;
INSERT INTO class_sessions (id, tenant_id, template_id, location_id, starts_at, ends_at, capacity)
VALUES (
'4bf74c12-44dd-45ca-86bb-b104f16f2435',
'5d6b3551-0a3e-4b86-bdf0-e9df20a47148',
'd13fe5fd-727f-4d69-bfd8-47f1b92a2cf7',
'659f1cc0-a850-46d6-b3b8-cb15d55d8daf',
now() + interval '2 days',
now() + interval '2 days' + interval '1 hour',
4
)
ON CONFLICT (id) DO NOTHING;
INSERT INTO availability_rules (tenant_id, staff_id, day_of_week, starts_local, ends_local)
SELECT
'5d6b3551-0a3e-4b86-bdf0-e9df20a47148',
'6936c444-c7d0-4a7d-b596-a9b72d2f4fc0',
weekday,
'09:00:00',
'17:00:00'
FROM (VALUES (1), (2), (3), (4), (5)) AS weekdays(weekday)
WHERE NOT EXISTS (
SELECT 1
FROM availability_rules ar
WHERE ar.tenant_id = '5d6b3551-0a3e-4b86-bdf0-e9df20a47148'
AND ar.staff_id = '6936c444-c7d0-4a7d-b596-a9b72d2f4fc0'
AND ar.day_of_week = weekdays.weekday
);
-- +goose Down
DELETE FROM availability_rules WHERE tenant_id = '5d6b3551-0a3e-4b86-bdf0-e9df20a47148';
DELETE FROM class_sessions WHERE id = '4bf74c12-44dd-45ca-86bb-b104f16f2435';
DELETE FROM class_templates WHERE id = 'd13fe5fd-727f-4d69-bfd8-47f1b92a2cf7';
DELETE FROM services WHERE id = 'd5d76a61-3d49-467c-8dd4-bf61ee754e39';
DELETE FROM staff WHERE id = '6936c444-c7d0-4a7d-b596-a9b72d2f4fc0';
DELETE FROM locations WHERE id = '659f1cc0-a850-46d6-b3b8-cb15d55d8daf';
DELETE FROM tenant_users WHERE tenant_id = '5d6b3551-0a3e-4b86-bdf0-e9df20a47148' AND user_id = '11111111-1111-1111-1111-111111111111';
DELETE FROM users WHERE id = '11111111-1111-1111-1111-111111111111';
DELETE FROM tenants WHERE id = '5d6b3551-0a3e-4b86-bdf0-e9df20a47148';
@@ -0,0 +1,23 @@
-- +goose Up
CREATE TABLE IF NOT EXISTS billing_snapshots (
tenant_id uuid PRIMARY KEY REFERENCES tenants(id) ON DELETE CASCADE,
stripe_customer_id text NOT NULL DEFAULT '',
stripe_subscription_id text NOT NULL DEFAULT '',
status text NOT NULL DEFAULT 'none',
plan_code text NOT NULL DEFAULT 'starter',
price_id text NOT NULL DEFAULT '',
cancel_at_period_end boolean NOT NULL DEFAULT false,
current_period_start timestamptz,
current_period_end timestamptz,
payment_method_brand text NOT NULL DEFAULT '',
payment_method_last4 text NOT NULL DEFAULT '',
last_synced_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_subscription_events_processed ON subscription_events (processed_at);
-- +goose Down
DROP INDEX IF EXISTS idx_subscription_events_processed;
DROP TABLE IF EXISTS billing_snapshots;
@@ -0,0 +1,20 @@
-- +goose Up
CREATE TABLE IF NOT EXISTS notification_delivery_logs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
reminder_job_id uuid REFERENCES reminder_jobs(id) ON DELETE SET NULL,
channel text NOT NULL,
provider text NOT NULL,
recipient text NOT NULL,
delivery_status text NOT NULL,
external_id text,
error_message text,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_notification_delivery_logs_tenant_time
ON notification_delivery_logs (tenant_id, created_at DESC);
-- +goose Down
DROP INDEX IF EXISTS idx_notification_delivery_logs_tenant_time;
DROP TABLE IF EXISTS notification_delivery_logs;
@@ -0,0 +1,18 @@
-- +goose Up
ALTER TABLE users
ADD COLUMN IF NOT EXISTS neon_subject text;
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_neon_subject
ON users (neon_subject)
WHERE neon_subject IS NOT NULL;
UPDATE users
SET neon_subject = 'demo-owner',
updated_at = now()
WHERE email = 'owner@bookra.dev'
AND neon_subject IS NULL;
-- +goose Down
DROP INDEX IF EXISTS idx_users_neon_subject;
ALTER TABLE users
DROP COLUMN IF EXISTS neon_subject;