chore: seminarhof customer project

This commit is contained in:
e2e
2026-07-10 23:07:47 +02:00
commit e3a13a8504
188 changed files with 20779 additions and 0 deletions

50
db/annotations.ts Normal file
View File

@@ -0,0 +1,50 @@
import type { DbClassificationMap } from '../packages/functions/.pikku/db/classification-map.gen.d.ts'
export const classifications = {
"account": {},
"audit": {},
"bathroom": {},
"booking": {
"booking_id": { security: 'public' },
"event_name": { security: 'public' },
"start_date": { security: 'public' },
"end_date": { security: 'public' },
"status": { security: 'public' },
"half_house": { security: 'public' },
"online_ad": { security: 'public' },
"cover_image_url": { security: 'public' },
"description": { security: 'public' },
"organiser_website": { security: 'public' },
"expected_persons": { security: 'public' },
"event_outline": { security: 'public' },
},
"booking_extra": {
"booking_id": { security: 'public' },
},
"booking_session": {},
"client": {
"name": { security: 'public' },
"website": { security: 'public' },
},
"client_member": {},
"enquiry": {
"half_house": { security: 'public' },
},
"enquiry_date_option": {},
"invoice": {
"booking_id": { security: 'public' },
},
"participant": {
"booking_id": { security: 'public' },
},
"participant_allergy": {},
"room": {},
"room_assignment": {
"booking_id": { security: 'public' },
},
"seminar_room": {},
"session": {},
"user": {},
"venue": {},
"verification": {},
} satisfies DbClassificationMap

635
db/sqlite-seed.sql Normal file
View File

@@ -0,0 +1,635 @@
-- Seed data for Seminarhof Drawehn dev environment.
-- Idempotent: existing rows with matching primary keys are left untouched.
-- ─── Venue ───────────────────────────────────────────────────────────
INSERT OR IGNORE INTO venue (venue_id, name, slug) VALUES
('v_drawehn', 'Seminarhof Drawehn', 'drawehn');
-- ─── Bathrooms ───────────────────────────────────────────────────────
INSERT OR IGNORE INTO bathroom (bathroom_id, venue_id, label) VALUES
('bath_1', 'v_drawehn', 'Bad Zi 1'),
('bath_2', 'v_drawehn', 'Bad Zi 2'),
('bath_3', 'v_drawehn', 'Bad Zi 3'),
('bath_4', 'v_drawehn', 'Bad Zi 4'),
('bath_5', 'v_drawehn', 'Bad Zi 5'),
('bath_6', 'v_drawehn', 'Bad Zi 6'),
('bath_7', 'v_drawehn', 'Bad Zi 7'),
('bath_8_9','v_drawehn', 'Bad Zi 8+9 (shared)'),
('bath_10', 'v_drawehn', 'Bad Zi 10'),
('bath_11', 'v_drawehn', 'Bad Zi 11'),
('bath_12', 'v_drawehn', 'Bad Zi 12'),
('bath_13', 'v_drawehn', 'Bad Zi 13'),
('bath_14', 'v_drawehn', 'Bad Zi 14'),
('bath_15', 'v_drawehn', 'Bad Zi 15'),
('bath_16', 'v_drawehn', 'Bad Zi 16'),
('bath_17', 'v_drawehn', 'Bad Zi 17'),
('bath_18', 'v_drawehn', 'Bad Zi 18'),
('bath_19_20','v_drawehn','Bad Zi 19+20 (shared)'),
('bath_21', 'v_drawehn', 'Bad Zi 21'),
('bath_22', 'v_drawehn', 'Bad Zi 22'),
('bath_23', 'v_drawehn', 'Bad Zi 23');
-- ─── Rooms ───────────────────────────────────────────────────────────
INSERT OR IGNORE INTO room
(room_id, venue_id, number, beds, room_type, building, floor, bathroom_id,
wheelchair, ground_floor, quiet, shared_bath, dogs_allowed, double_bed_140,
surcharge_eur_cents)
VALUES
('r_1', 'v_drawehn', 1, 1, 'EZ_only', 'gaestehaus', 'eg', 'bath_1', 1, 1, 0, 0, 0, 0, 0),
('r_2', 'v_drawehn', 2, 2, 'DZ', 'gaestehaus', 'eg', 'bath_2', 1, 1, 0, 0, 0, 0, 0),
('r_3', 'v_drawehn', 3, 2, 'DZ', 'gaestehaus', 'eg', 'bath_3', 0, 1, 0, 0, 0, 0, 0),
('r_4', 'v_drawehn', 4, 2, 'DZ', 'gaestehaus', 'eg', 'bath_4', 0, 1, 0, 0, 0, 0, 0),
('r_5', 'v_drawehn', 5, 3, 'MBZ', 'gaestehaus', 'eg', 'bath_5', 0, 1, 0, 0, 0, 0, 0),
('r_6', 'v_drawehn', 6, 1, 'EZ_only', 'gaestehaus', 'eg', 'bath_6', 0, 1, 0, 0, 0, 0, 0),
('r_7', 'v_drawehn', 7, 3, 'MBZ', 'gaestehaus', 'eg', 'bath_7', 0, 1, 1, 0, 0, 0, 0),
('r_8', 'v_drawehn', 8, 2, 'DZ', 'gaestehaus', 'eg', 'bath_8_9',0, 1, 1, 1, 0, 0, 0),
('r_9', 'v_drawehn', 9, 2, 'DZ', 'gaestehaus', 'eg', 'bath_8_9',0, 1, 1, 1, 0, 0, 0),
('r_10', 'v_drawehn', 10, 2, 'DZ', 'gaestehaus', 'eg', 'bath_10', 0, 1, 1, 0, 1, 0, 2000),
('r_11', 'v_drawehn', 11, 2, 'DZ', 'gaestehaus', 'eg', 'bath_11', 0, 1, 1, 0, 0, 0, 0),
('r_12', 'v_drawehn', 12, 2, 'DZ', 'gaestehaus', 'eg', 'bath_12', 0, 1, 1, 0, 0, 0, 0),
('r_13', 'v_drawehn', 13, 1, 'EZ_only', 'gaestehaus', 'og', 'bath_13', 0, 0, 0, 0, 0, 0, 0),
('r_14', 'v_drawehn', 14, 1, 'EZ_only', 'gaestehaus', 'og', 'bath_14', 0, 0, 0, 0, 0, 1, 0),
('r_15', 'v_drawehn', 15, 2, 'DZ', 'gaestehaus', 'og', 'bath_15', 0, 0, 0, 0, 0, 0, 0),
('r_16', 'v_drawehn', 16, 2, 'DZ', 'gaestehaus', 'og', 'bath_16', 0, 0, 0, 0, 0, 0, 0),
('r_17', 'v_drawehn', 17, 2, 'DZ', 'gaestehaus', 'og', 'bath_17', 0, 0, 0, 0, 0, 0, 0),
('r_18', 'v_drawehn', 18, 2, 'DZ', 'gaestehaus', 'og', 'bath_18', 0, 0, 0, 0, 0, 0, 0),
('r_19', 'v_drawehn', 19, 2, 'DZ', 'haupthaus', 'eg', 'bath_19_20',0, 1, 0, 1, 0, 0, 0),
('r_20', 'v_drawehn', 20, 3, 'DZ', 'haupthaus', 'eg', 'bath_19_20',0, 1, 0, 1, 0, 1, 0),
('r_21', 'v_drawehn', 21, 2, 'DZ', 'haupthaus', 'og', 'bath_21', 0, 0, 0, 0, 0, 0, 0),
('r_22', 'v_drawehn', 22, 1, 'EZ_only', 'haupthaus', 'og', 'bath_22', 0, 0, 0, 0, 0, 1, 0),
('r_23', 'v_drawehn', 23, 2, 'DZ', 'haupthaus', 'og', 'bath_23', 0, 0, 0, 0, 0, 0, 0);
-- ─── Seminar rooms ───────────────────────────────────────────────────
INSERT OR IGNORE INTO seminar_room (seminar_room_id, venue_id, name) VALUES
('sr_mandala', 'v_drawehn', 'Mandala'),
('sr_yogaraum', 'v_drawehn', 'Yogaraum');
-- ─── Demo client ─────────────────────────────────────────────────────
-- Users are created via better-auth sign-up (not seeded here).
-- Demo credentials (sign up via /api/auth/sign-up/email):
-- sarah@seminarhof.example → owner1234
-- christina@seminarhof.example → admin1234
-- demo@yoga-retreat.example → demo1234
INSERT OR IGNORE INTO client
(client_id, venue_id, name, is_stammgruppe, contact_email, website)
VALUES
('client_yoga', 'v_drawehn', 'Yoga Retreat e.V.', 1, 'demo@yoga-retreat.example', 'https://yoga-retreat.example');
-- ─── Demo bookings ──────────────────────────────────────────────────
INSERT OR IGNORE INTO booking
(booking_id, venue_id, client_id, event_name, start_date, end_date,
status, half_house, online_ad, expected_persons, description,
meal_time_breakfast, meal_time_lunch, meal_time_dinner, payment_method,
agb_year, agb_accepted_at, privacy_accepted_at)
VALUES
('b_yoga_2026_summer',
'v_drawehn', 'client_yoga',
'Summer Yoga Retreat 2026',
'2026-07-12', '2026-07-19',
'confirmed', 0, 1, 28,
'A week of yoga, breath work, and quiet meals in Lower Saxony.',
'08:00', '13:00', '19:00', 'transfer',
2026, '2026-02-14T10:30:00Z', '2026-02-14T10:30:00Z'),
('b_yoga_2026_winter',
'v_drawehn', 'client_yoga',
'Winter Stille 2026',
'2026-12-27', '2027-01-02',
'confirmed', 0, 0, 18,
'Year-end silent retreat.',
'08:30', '13:00', '18:30', 'transfer',
2026, '2026-04-01T09:00:00Z', '2026-04-01T09:00:00Z'),
('b_lc_enquiry',
'v_drawehn', 'client_yoga',
'Lifecycle Test Retreat',
'2026-12-20', '2026-12-27',
'enquiry', 0, 0, 10,
'Lifecycle e2e test booking.',
'08:00', '13:00', '19:00', 'transfer',
NULL, NULL, NULL),
('b_lc_reserved',
'v_drawehn', 'client_yoga',
'Lifecycle Test Retreat (Reserved)',
'2026-12-20', '2026-12-27',
'reserved', 0, 0, 10,
'Reserved - contract not yet sent.',
'08:00', '13:00', '19:00', 'transfer',
NULL, NULL, NULL),
('b_lc_contract_sent',
'v_drawehn', 'client_yoga',
'Lifecycle Test Retreat (Contract Sent)',
'2026-12-20', '2026-12-27',
'reserved', 0, 0, 10,
'Reserved - contract sent 8 days ago.',
'08:00', '13:00', '19:00', 'transfer',
NULL, NULL, NULL),
('b_lc_confirmed',
'v_drawehn', 'client_yoga',
'Lifecycle Test Retreat (Confirmed)',
'2026-05-26', '2026-06-02',
'confirmed', 0, 0, 10,
'Confirmed - ended yesterday, ready for auto-complete.',
'08:00', '13:00', '19:00', 'transfer',
NULL, NULL, NULL);
-- ─── Sample participants for the summer retreat ─────────────────────
INSERT OR IGNORE INTO participant (participant_id, booking_id, name, kind, dietary_tag) VALUES
('p_s_01', 'b_yoga_2026_summer', 'Anja Weber', 'overnight', 'vegan'),
('p_s_02', 'b_yoga_2026_summer', 'Bernd Krause', 'overnight', 'vegetarian'),
('p_s_03', 'b_yoga_2026_summer', 'Clara Hoffmann', 'overnight', 'vegan'),
('p_s_04', 'b_yoga_2026_summer', 'Dieter Lange', 'overnight', 'omnivore'),
('p_s_05', 'b_yoga_2026_summer', 'Eva Richter', 'overnight', 'gluten_free'),
('p_s_06', 'b_yoga_2026_summer', 'Frank Müller', 'overnight', NULL),
('p_s_07', 'b_yoga_2026_summer', 'Greta Schulz', 'overnight', 'vegan'),
('p_s_08', 'b_yoga_2026_summer', 'Hans Becker', 'day_guest', 'vegetarian');
INSERT OR IGNORE INTO participant_allergy (participant_id, allergen, severity) VALUES
('p_s_01', 'nuts', 'standard'),
('p_s_05', 'gluten_strict', 'separate_prep');
-- ─── Sample invoices ────────────────────────────────────────────────
INSERT OR IGNORE INTO invoice
(invoice_id, booking_id, kind, invoice_number, issued_on, due_on, paid_on, amount_cents, status)
VALUES
('inv_s_dep', 'b_yoga_2026_summer', 'deposit', '2026-001', '2026-02-15', '2026-03-01', '2026-02-22', 30000, 'paid'),
('inv_w_dep', 'b_yoga_2026_winter', 'deposit', '2026-002', '2026-04-02', '2026-04-16', '2026-04-10', 30000, 'paid'),
('inv_lc_dep', 'b_lc_contract_sent', 'deposit', 'LC-DEP-001', '2026-05-26', '2026-06-09', NULL, 30000, 'outstanding');
-- ─── Additional clients ────────────────────────────────────────────
INSERT OR IGNORE INTO client
(client_id, venue_id, name, is_stammgruppe, contact_email, contact_phone, billing_address, website)
VALUES
('client_breath', 'v_drawehn', 'Breath & Body Collective', 1, 'hi@breath-body.example', '+49 30 22221111', 'Hauptstr. 22, 10115 Berlin', 'https://breath-body.example'),
('client_silent', 'v_drawehn', 'Silent Path Sangha', 0, 'team@silent-path.example','+49 40 33334444', 'Mönckebergstr. 7, 20095 Hamburg', 'https://silent-path.example'),
('client_dance', 'v_drawehn', '5Rhythms Nord', 0, 'kontakt@5r-nord.example', '+49 511 5556677', 'Lister Meile 14, 30161 Hannover','https://5r-nord.example'),
('client_writers', 'v_drawehn', 'Schreibwerkstatt Lüneburger Heide', 0, 'mail@schreiben-heide.example', '+49 5841 991122', 'Lange Str. 4, 29410 Salzwedel', NULL),
('client_qigong', 'v_drawehn', 'Qigong Verein Hamburg e.V.', 1, 'verein@qigong-hh.example','+49 40 88991122', 'Eppendorfer Weg 12, 20259 Hamburg', NULL),
('client_workation','v_drawehn', 'Remote First GmbH', 0, 'ops@remotefirst.example', '+49 30 99887766', 'Linienstr. 3, 10119 Berlin', 'https://remotefirst.example'),
('client_choir', 'v_drawehn', 'Heide Vocal Ensemble', 0, 'info@heide-vocal.example',NULL, NULL, NULL);
-- ─── Additional users (clients linked to orgs) ─────────────────────
-- Inserted into user table (better-auth schema). Passwords must be set via /api/auth/sign-up/email.
INSERT OR IGNORE INTO "user" (id, email, name, locale, role, email_verified, created_at, updated_at) VALUES
('u_breath', 'lea@breath-body.example', 'Lea Breath', 'de', 'client', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('u_silent', 'tom@silent-path.example', 'Tom Silent', 'en', 'client', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('u_dance', 'jana@5r-nord.example', 'Jana Dance', 'de', 'client', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('u_writers', 'paula@schreiben-heide.example', 'Paula Schreiber','de', 'client', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('u_qigong', 'kai@qigong-hh.example', 'Kai Wu', 'de', 'client', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('u_workation','noah@remotefirst.example', 'Noah Remote', 'en', 'client', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('u_choir', 'sofia@heide-vocal.example', 'Sofia Vocal', 'de', 'client', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z');
INSERT OR IGNORE INTO client_member (client_id, user_id, role) VALUES
('client_breath', 'u_breath', 'organiser'),
('client_silent', 'u_silent', 'organiser'),
('client_dance', 'u_dance', 'organiser'),
('client_writers', 'u_writers', 'organiser'),
('client_qigong', 'u_qigong', 'organiser'),
('client_workation','u_workation','organiser'),
('client_choir', 'u_choir', 'organiser');
-- ─── Additional bookings spanning the year ─────────────────────────
INSERT OR IGNORE INTO booking
(booking_id, venue_id, client_id, event_name, start_date, end_date,
status, half_house, online_ad, expected_persons, description,
meal_time_breakfast, meal_time_lunch, meal_time_dinner, payment_method,
agb_year, agb_accepted_at, privacy_accepted_at, organiser_website, cover_image_url, notes)
VALUES
-- past, ended
('b_breath_spring',
'v_drawehn', 'client_breath',
'Spring Breath Intensive',
'2026-03-15', '2026-03-22',
'ended', 0, 1, 24,
'Pranayama and somatic breath work in the early bloom of spring.',
'08:00','13:00','19:00','transfer',
2026, '2025-11-12T09:00:00Z','2025-11-12T09:00:00Z',
'https://breath-body.example', NULL, NULL),
('b_qigong_easter',
'v_drawehn', 'client_qigong',
'Qigong Osterklausur',
'2026-04-03','2026-04-09',
'ended', 1, 0, 14,
'Half-house qigong retreat over the Easter break.',
'07:30','12:30','18:30','transfer',
2026, '2025-12-01T10:00:00Z','2025-12-01T10:00:00Z',
NULL, NULL, 'Stammgruppe — repeat client'),
-- in-flight, current
('b_writers_may',
'v_drawehn', 'client_writers',
'Schreibwoche Mai',
'2026-05-08','2026-05-14',
'confirmed', 0, 1, 16,
'A writing week with morning prompts and afternoon silence.',
'08:30','13:00','19:00','transfer',
2026, '2026-01-20T11:00:00Z','2026-01-20T11:00:00Z',
NULL, NULL, NULL),
-- upcoming, deposit paid
('b_dance_june',
'v_drawehn', 'client_dance',
'5Rhythms Solstice',
'2026-06-19','2026-06-22',
'confirmed', 1, 1, 22,
'Long-weekend movement intensive at the start of summer.',
'08:30','13:30','19:30','transfer',
2026, '2026-02-02T14:00:00Z','2026-02-02T14:00:00Z',
'https://5r-nord.example', NULL, NULL),
-- shares the solstice weekend with 5Rhythms (both half-house) → the
-- availability calendar renders Jun 2022 as a diagonal split.
('b_writers_solstice',
'v_drawehn', 'client_writers',
'Solstice Schreibzirkel',
'2026-06-20','2026-06-22',
'confirmed', 1, 1, 10,
'A small writing circle taking the other half of the house over the solstice.',
'08:30','13:00','19:00','transfer',
2026, '2026-02-10T10:00:00Z','2026-02-10T10:00:00Z',
NULL, NULL, 'Half-house — shares the weekend with 5Rhythms Solstice'),
-- upcoming, reserved
('b_workation_aug',
'v_drawehn', 'client_workation',
'Remote First Workation',
'2026-08-10','2026-08-17',
'reserved', 0, 0, 12,
'Engineering team offsite — co-working with daily morning movement.',
'08:00','13:00','19:00','transfer',
2026, '2026-03-12T16:00:00Z','2026-03-12T16:00:00Z',
'https://remotefirst.example', NULL, 'Needs strong wifi and 2nd seminar room'),
('b_silent_sept',
'v_drawehn', 'client_silent',
'Silent Autumn Sangha',
'2026-09-21','2026-09-28',
'confirmed', 0, 1, 26,
'Seven days of noble silence, sitting and walking meditation.',
'07:30','12:30','18:30','transfer',
2026, '2026-04-04T09:30:00Z','2026-04-04T09:30:00Z',
'https://silent-path.example', NULL, NULL),
('b_choir_october',
'v_drawehn', 'client_choir',
'Vocal Ensemble Probenwoche',
'2026-10-12','2026-10-18',
'confirmed', 0, 1, 20,
'Choir rehearsal week preparing the winter programme.',
'08:30','13:00','19:30','transfer',
2026, '2026-04-15T10:00:00Z','2026-04-15T10:00:00Z',
NULL, NULL, NULL),
-- new enquiry, awaiting admin approve/decline
('b_qigong_nov',
'v_drawehn', 'client_qigong',
'Qigong Spätherbst',
'2026-11-08','2026-11-13',
'enquiry', 1, 0, 12,
NULL,
'08:00','13:00','18:30','transfer',
2026, NULL, NULL,
NULL, NULL, NULL),
-- reserved, contract sent + signed, deposit pending (day 8 of 14)
('b_breath_dec',
'v_drawehn', 'client_breath',
'Atemarbeit zum Jahreswechsel',
'2026-12-12','2026-12-19',
'reserved', 0, 1, 22,
'Year-end breath work for the dark half of the year.',
'08:30','13:00','19:00','transfer',
2026, '2026-05-01T09:00:00Z','2026-05-01T09:00:00Z',
'https://breath-body.example', NULL, NULL),
-- cancelled (was reserved, fell through)
('b_dance_cancelled',
'v_drawehn', 'client_dance',
'5Rhythms Sommerfest (cancelled)',
'2026-07-25','2026-07-28',
'cancelled', 1, 0, 18,
'Cancelled due to overlap with Solstice retreat.',
'08:00','13:00','19:00','transfer',
2026, '2026-02-10T09:00:00Z','2026-02-10T09:00:00Z',
NULL, NULL, NULL);
-- ─── Additional participants for the new bookings ──────────────────
INSERT OR IGNORE INTO participant (participant_id, booking_id, name, kind, dietary_tag) VALUES
-- breath spring (ended) — 12 named
('p_b_01','b_breath_spring','Lea Breath', 'overnight','vegan'),
('p_b_02','b_breath_spring','Mara Lehmann', 'overnight','vegetarian'),
('p_b_03','b_breath_spring','Niko Petrov', 'overnight','omnivore'),
('p_b_04','b_breath_spring','Olga Schmidt', 'overnight','vegan'),
('p_b_05','b_breath_spring','Pia Brandt', 'overnight','gluten_free'),
('p_b_06','b_breath_spring','Quentin Ross', 'overnight','vegetarian'),
('p_b_07','b_breath_spring','Rosa Jung', 'overnight','vegan'),
('p_b_08','b_breath_spring','Stefan Kühn', 'overnight','omnivore'),
('p_b_09','b_breath_spring','Tara Singh', 'overnight','lactose_free'),
('p_b_10','b_breath_spring','Ulla Berger', 'day_guest','vegetarian'),
('p_b_11','b_breath_spring','Vincent Aydin', 'day_guest','vegan'),
('p_b_12','b_breath_spring','Wenke Otto', 'overnight','vegetarian'),
-- writers may (confirmed, in-flight) — 9 named
('p_w_01','b_writers_may','Paula Schreiber','overnight','vegetarian'),
('p_w_02','b_writers_may','Annika Born', 'overnight','vegan'),
('p_w_03','b_writers_may','Bjarne Holm', 'overnight','omnivore'),
('p_w_04','b_writers_may','Conny Dittmer', 'overnight','vegan'),
('p_w_05','b_writers_may','Detlef Vogel', 'overnight','gluten_free'),
('p_w_06','b_writers_may','Erika Pohl', 'overnight','vegetarian'),
('p_w_07','b_writers_may','Falk Reuter', 'overnight','omnivore'),
('p_w_08','b_writers_may','Gesa Mahler', 'day_guest','pescatarian'),
('p_w_09','b_writers_may','Henrik Lass', 'overnight','vegan'),
-- dance june — 8 named
('p_d_01','b_dance_june','Jana Dance', 'overnight','vegan'),
('p_d_02','b_dance_june','Karim Yilmaz', 'overnight','omnivore'),
('p_d_03','b_dance_june','Lena Frey', 'overnight','vegetarian'),
('p_d_04','b_dance_june','Mira Hoch', 'overnight','vegan'),
('p_d_05','b_dance_june','Nils Roth', 'overnight','vegetarian'),
('p_d_06','b_dance_june','Olaf Heim', 'overnight','omnivore'),
('p_d_07','b_dance_june','Petra Wenz', 'overnight','vegan'),
('p_d_08','b_dance_june','Quirin Albers', 'overnight','gluten_free'),
-- silent sept — 10 named
('p_si_01','b_silent_sept','Tom Silent', 'overnight','vegan'),
('p_si_02','b_silent_sept','Ute Mendel', 'overnight','vegetarian'),
('p_si_03','b_silent_sept','Vera Klein', 'overnight','vegan'),
('p_si_04','b_silent_sept','Walter Frey', 'overnight','omnivore'),
('p_si_05','b_silent_sept','Xenia Adler', 'overnight','vegan'),
('p_si_06','b_silent_sept','Yannick Bauer', 'overnight','vegetarian'),
('p_si_07','b_silent_sept','Zoe Maier', 'overnight','gluten_free'),
('p_si_08','b_silent_sept','Ada Friese', 'overnight','vegan'),
('p_si_09','b_silent_sept','Bo Tillmann', 'overnight','vegetarian'),
('p_si_10','b_silent_sept','Carla Hess', 'overnight','vegan'),
-- workation aug — 10 named
('p_wa_01','b_workation_aug','Noah Remote', 'overnight','vegetarian'),
('p_wa_02','b_workation_aug','Aisha Khan', 'overnight','vegan'),
('p_wa_03','b_workation_aug','Bruno Sá', 'overnight','omnivore'),
('p_wa_04','b_workation_aug','Chen Wei', 'overnight','pescatarian'),
('p_wa_05','b_workation_aug','Dani Lopez', 'overnight','vegan'),
('p_wa_06','b_workation_aug','Elif Demir', 'overnight','vegetarian'),
('p_wa_07','b_workation_aug','Felix Andersen','overnight','omnivore'),
('p_wa_08','b_workation_aug','Gita Rao', 'overnight','vegan'),
('p_wa_09','b_workation_aug','Henri Dubois', 'overnight','gluten_free'),
('p_wa_10','b_workation_aug','Ivy Park', 'overnight','vegan'),
-- choir october — 8 named
('p_c_01','b_choir_october','Sofia Vocal', 'overnight','vegetarian'),
('p_c_02','b_choir_october','Toni Krug', 'overnight','vegan'),
('p_c_03','b_choir_october','Una Berg', 'overnight','omnivore'),
('p_c_04','b_choir_october','Vlad Olar', 'overnight','vegan'),
('p_c_05','b_choir_october','Wim de Jong', 'overnight','vegetarian'),
('p_c_06','b_choir_october','Yuna Park', 'overnight','vegan'),
('p_c_07','b_choir_october','Zara Mahmood', 'overnight','gluten_free'),
('p_c_08','b_choir_october','Ali Khalil', 'overnight','omnivore'),
-- qigong easter (ended, half-house) — 6 named
('p_qe_01','b_qigong_easter','Kai Wu', 'overnight','vegan'),
('p_qe_02','b_qigong_easter','Sun Park', 'overnight','vegetarian'),
('p_qe_03','b_qigong_easter','Marek Liu', 'overnight','vegan'),
('p_qe_04','b_qigong_easter','Hannah Lee', 'overnight','pescatarian'),
('p_qe_05','b_qigong_easter','Iris Wang', 'overnight','vegetarian'),
('p_qe_06','b_qigong_easter','Jonas Meier','overnight','omnivore');
-- ─── Allergies sprinkled across new participants ──────────────────
INSERT OR IGNORE INTO participant_allergy (participant_id, allergen, severity) VALUES
('p_b_05','gluten_strict','separate_prep'),
('p_b_09','lactose','standard'),
('p_w_05','gluten_strict','separate_prep'),
('p_d_08','gluten_strict','separate_prep'),
('p_si_07','gluten_strict','separate_prep'),
('p_wa_04','shellfish','standard'),
('p_wa_09','gluten_strict','separate_prep'),
('p_c_07','gluten_strict','separate_prep'),
('p_c_03','peanuts','severe');
-- ─── Invoices for new bookings ─────────────────────────────────────
INSERT OR IGNORE INTO invoice
(invoice_id, booking_id, kind, invoice_number, issued_on, due_on, paid_on, amount_cents, status)
VALUES
('inv_b_dep','b_breath_spring','deposit','2026-010','2025-12-01','2025-12-15','2025-12-08', 36000,'paid'),
('inv_b_fin','b_breath_spring','final', '2026-011','2026-03-23','2026-04-06','2026-04-02',144000,'paid'),
('inv_qe_dep','b_qigong_easter','deposit','2026-012','2025-12-10','2025-12-24','2025-12-20', 21000,'paid'),
('inv_qe_fin','b_qigong_easter','final', '2026-013','2026-04-10','2026-04-24','2026-04-22', 84000,'paid'),
('inv_w_dep_writers','b_writers_may','deposit','2026-014','2026-02-01','2026-02-15','2026-02-09', 24000,'paid'),
('inv_d_dep','b_dance_june','deposit','2026-015','2026-02-15','2026-03-01','2026-02-22', 33000,'paid'),
('inv_si_dep','b_silent_sept','deposit','2026-016','2026-04-15','2026-04-29',NULL, 39000,'outstanding'),
('inv_c_dep','b_choir_october','deposit','2026-017','2026-04-20','2026-05-04','2026-04-30', 30000,'paid'),
('inv_wa_dep','b_workation_aug','deposit','2026-018','2026-03-20','2026-04-03',NULL, 18000,'outstanding'),
('inv_bd_dep','b_breath_dec','deposit','2026-019','2026-05-13','2026-05-27',NULL, 33000,'outstanding');
-- ─── Lifecycle milestone state ────────────────────────────────────
-- Status-transition timestamps (migration 0007) and contract milestones.
-- confirmed bookings: reserved → contract → deposit → confirmed
UPDATE booking SET
reserved_at = '2026-02-15T10:00:00Z',
contract_sent_at = '2026-02-15T10:00:00Z',
contract_response = 'approved', contract_response_at = '2026-02-18T14:00:00Z',
confirmed_at = '2026-02-22T09:00:00Z'
WHERE booking_id = 'b_yoga_2026_summer';
UPDATE booking SET
reserved_at = '2026-04-02T10:00:00Z',
contract_sent_at = '2026-04-02T10:00:00Z',
contract_response = 'approved', contract_response_at = '2026-04-05T14:00:00Z',
confirmed_at = '2026-04-10T09:00:00Z'
WHERE booking_id = 'b_yoga_2026_winter';
UPDATE booking SET
reserved_at = '2025-11-13T10:00:00Z',
contract_sent_at = '2025-11-13T10:00:00Z',
contract_response = 'approved', contract_response_at = '2025-11-16T14:00:00Z',
confirmed_at = '2025-12-08T09:00:00Z',
ended_at = '2026-03-23T12:00:00Z'
WHERE booking_id = 'b_breath_spring';
UPDATE booking SET
reserved_at = '2025-12-02T10:00:00Z',
contract_sent_at = '2025-12-02T10:00:00Z',
contract_response = 'approved', contract_response_at = '2025-12-05T14:00:00Z',
confirmed_at = '2025-12-20T09:00:00Z',
ended_at = '2026-04-10T12:00:00Z'
WHERE booking_id = 'b_qigong_easter';
UPDATE booking SET
reserved_at = '2026-01-21T10:00:00Z',
contract_sent_at = '2026-01-21T10:00:00Z',
contract_response = 'approved', contract_response_at = '2026-01-24T14:00:00Z',
confirmed_at = '2026-02-09T09:00:00Z'
WHERE booking_id = 'b_writers_may';
UPDATE booking SET
reserved_at = '2026-02-03T10:00:00Z',
contract_sent_at = '2026-02-03T10:00:00Z',
contract_response = 'approved', contract_response_at = '2026-02-06T14:00:00Z',
confirmed_at = '2026-02-22T09:00:00Z'
WHERE booking_id = 'b_dance_june';
UPDATE booking SET
reserved_at = '2026-04-05T10:00:00Z',
contract_sent_at = '2026-04-05T10:00:00Z',
contract_response = 'approved', contract_response_at = '2026-04-08T14:00:00Z',
confirmed_at = '2026-04-15T09:00:00Z'
WHERE booking_id = 'b_silent_sept';
UPDATE booking SET
reserved_at = '2026-04-16T10:00:00Z',
contract_sent_at = '2026-04-16T10:00:00Z',
contract_response = 'approved', contract_response_at = '2026-04-19T14:00:00Z',
confirmed_at = '2026-04-30T09:00:00Z'
WHERE booking_id = 'b_choir_october';
UPDATE booking SET
reserved_at = '2026-06-03T10:00:00Z'
WHERE booking_id = 'b_lc_reserved';
UPDATE booking SET
reserved_at = '2026-06-03T10:00:00Z',
contract_sent_at = '2026-05-26'
WHERE booking_id = 'b_lc_contract_sent';
UPDATE booking SET
reserved_at = '2026-05-19T10:00:00Z',
contract_sent_at = '2026-05-19T10:00:00Z',
contract_response = 'approved',
contract_response_at = '2026-05-22T14:00:00Z',
confirmed_at = '2026-05-26T09:00:00Z',
ended_at = '2026-06-02T12:00:00Z'
WHERE booking_id = 'b_lc_confirmed';
-- reserved booking: contract sent + signed, deposit pending, day 8 of 14 (healthy)
UPDATE booking SET
reserved_at = '2026-05-02T10:00:00Z',
contract_sent_at = '2026-05-13T09:00:00Z',
contract_response = 'approved',
contract_response_at = '2026-05-15T14:00:00Z',
deposit_reminder_sent_at = '2026-05-20T06:00:00Z'
WHERE booking_id = 'b_breath_dec';
-- reserved booking: contract sent + signed, deposit overdue → flagged
UPDATE booking SET
reserved_at = '2026-03-13T10:00:00Z',
contract_sent_at = '2026-03-20T09:00:00Z',
contract_response = 'approved',
contract_response_at = '2026-03-24T11:00:00Z',
deposit_reminder_sent_at = '2026-03-27T06:00:00Z',
flagged_at = '2026-04-03T06:00:00Z',
flag_reason = 'deposit_overdue'
WHERE booking_id = 'b_workation_aug';
-- cancelled booking
UPDATE booking SET
reserved_at = '2026-02-11T10:00:00Z',
cancelled_at = '2026-02-25T09:00:00Z'
WHERE booking_id = 'b_dance_cancelled';
-- ─── Sample room assignments (so the room-map view is populated) ────
INSERT OR IGNORE INTO room_assignment (booking_id, participant_id, room_id) VALUES
('b_writers_may','p_w_01','r_1'),
('b_writers_may','p_w_02','r_2'),
('b_writers_may','p_w_03','r_3'),
('b_writers_may','p_w_04','r_4'),
('b_writers_may','p_w_05','r_5'),
('b_writers_may','p_w_06','r_6'),
('b_writers_may','p_w_07','r_7'),
('b_writers_may','p_w_09','r_15');
-- ─── Audit log — status transition history for seeded bookings ───────
-- Audit log — status transition history for seeded bookings. Stored as
-- pikku AuditEvents: the domain payload lives in the `data` JSON column,
-- which get-booking-audit-log reads back via json_extract.
INSERT OR IGNORE INTO audit (audit_id, occurred_at, type, source, actor_user_id, data) VALUES
-- b_yoga_2026_summer: enquiry → reserved → confirmed
('al_ys_1', '2026-02-15T10:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_yoga_2026_summer","action":"update","field":"status","before":"enquiry","after":"reserved","venueId":"v_drawehn"}'),
('al_ys_2', '2026-02-22T09:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_yoga_2026_summer","action":"update","field":"status","before":"reserved","after":"confirmed","venueId":"v_drawehn"}'),
-- b_yoga_2026_winter: enquiry → reserved → confirmed
('al_yw_1', '2026-04-02T10:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_yoga_2026_winter","action":"update","field":"status","before":"enquiry","after":"reserved","venueId":"v_drawehn"}'),
('al_yw_2', '2026-04-10T09:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_yoga_2026_winter","action":"update","field":"status","before":"reserved","after":"confirmed","venueId":"v_drawehn"}'),
-- b_breath_spring: enquiry → reserved → confirmed → ended
('al_bs_1', '2025-11-13T10:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_breath_spring","action":"update","field":"status","before":"enquiry","after":"reserved","venueId":"v_drawehn"}'),
('al_bs_2', '2025-12-08T09:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_breath_spring","action":"update","field":"status","before":"reserved","after":"confirmed","venueId":"v_drawehn"}'),
('al_bs_3', '2026-03-23T12:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_breath_spring","action":"update","field":"status","before":"confirmed","after":"ended","venueId":"v_drawehn"}'),
-- b_qigong_easter: enquiry → reserved → confirmed → ended
('al_qe_1', '2025-12-02T10:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_qigong_easter","action":"update","field":"status","before":"enquiry","after":"reserved","venueId":"v_drawehn"}'),
('al_qe_2', '2025-12-20T09:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_qigong_easter","action":"update","field":"status","before":"reserved","after":"confirmed","venueId":"v_drawehn"}'),
('al_qe_3', '2026-04-10T12:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_qigong_easter","action":"update","field":"status","before":"confirmed","after":"ended","venueId":"v_drawehn"}'),
-- b_writers_may: enquiry → reserved → confirmed
('al_wm_1', '2026-01-21T10:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_writers_may","action":"update","field":"status","before":"enquiry","after":"reserved","venueId":"v_drawehn"}'),
('al_wm_2', '2026-02-09T09:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_writers_may","action":"update","field":"status","before":"reserved","after":"confirmed","venueId":"v_drawehn"}'),
-- b_dance_june: enquiry → reserved → confirmed
('al_dj_1', '2026-02-03T10:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_dance_june","action":"update","field":"status","before":"enquiry","after":"reserved","venueId":"v_drawehn"}'),
('al_dj_2', '2026-02-22T09:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_dance_june","action":"update","field":"status","before":"reserved","after":"confirmed","venueId":"v_drawehn"}'),
-- b_workation_aug: enquiry → reserved
('al_wa_1', '2026-03-13T10:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_workation_aug","action":"update","field":"status","before":"enquiry","after":"reserved","venueId":"v_drawehn"}'),
-- b_silent_sept: enquiry → reserved → confirmed
('al_ss_1', '2026-04-05T10:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_silent_sept","action":"update","field":"status","before":"enquiry","after":"reserved","venueId":"v_drawehn"}'),
('al_ss_2', '2026-04-15T09:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_silent_sept","action":"update","field":"status","before":"reserved","after":"confirmed","venueId":"v_drawehn"}'),
-- lifecycle fixtures
('al_lcr_1', '2026-06-03T10:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_lc_reserved","action":"update","field":"status","before":"enquiry","after":"reserved","venueId":"v_drawehn"}'),
('al_lcc_1', '2026-06-03T10:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_lc_contract_sent","action":"update","field":"status","before":"enquiry","after":"reserved","venueId":"v_drawehn"}'),
('al_lcf_1', '2026-05-19T10:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_lc_confirmed","action":"update","field":"status","before":"enquiry","after":"reserved","venueId":"v_drawehn"}'),
('al_lcf_2', '2026-05-26T09:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_lc_confirmed","action":"update","field":"status","before":"reserved","after":"confirmed","venueId":"v_drawehn"}'),
('al_lcf_3', '2026-06-02T12:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_lc_confirmed","action":"update","field":"status","before":"confirmed","after":"ended","venueId":"v_drawehn"}'),
-- b_choir_october: enquiry → reserved → confirmed
('al_co_1', '2026-04-16T10:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_choir_october","action":"update","field":"status","before":"enquiry","after":"reserved","venueId":"v_drawehn"}'),
('al_co_2', '2026-04-30T09:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_choir_october","action":"update","field":"status","before":"reserved","after":"confirmed","venueId":"v_drawehn"}'),
-- b_breath_dec: enquiry → reserved
('al_bd_1', '2026-05-02T10:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_breath_dec","action":"update","field":"status","before":"enquiry","after":"reserved","venueId":"v_drawehn"}'),
-- b_dance_cancelled: enquiry → reserved → cancelled
('al_dc_1', '2026-02-11T10:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_dance_cancelled","action":"update","field":"status","before":"enquiry","after":"reserved","venueId":"v_drawehn"}'),
('al_dc_2', '2026-02-25T09:00:00Z', 'booking.update', 'explicit', NULL, '{"entity":"booking","entityId":"b_dance_cancelled","action":"update","field":"status","before":"reserved","after":"cancelled","venueId":"v_drawehn"}');
-- ─── Public enquiries — awaiting admin triage ──────────────────────
-- Dates live in enquiry_date_option (priority-ordered), not on the enquiry.
INSERT OR IGNORE INTO enquiry
(enquiry_id, venue_id, contact_name, contact_email, contact_phone, website,
event_name, event_outline, description,
expected_persons, half_house, notes, privacy_accepted_at, status,
waitlisted_at, approved_at, declined_at, created_at) VALUES
-- Fresh pending enquiry with a single date option
('enq_pottery', 'v_drawehn', 'Lena Hoffmann', 'lena.hoffmann@example.com', '+49 170 1112233', 'https://tonundton.example.com',
'Pottery & Clay Retreat', 'A hands-on weekend of wheel-throwing and glazing.', 'Small group ceramics retreat with daily guided sessions.',
12, 0, 'Would need kiln access if possible.', '2026-05-18T14:30:00Z', 'pending',
NULL, NULL, NULL, '2026-05-18T14:30:00Z'),
-- Pending enquiry, no dates yet (flexible) — zero date options
('enq_songwriting', 'v_drawehn', 'Marco Velten', 'marco@vamp-music.example.com', NULL, NULL,
'Songwriting Intensive', 'Collaborative songwriting and recording.', 'Looking for a quiet space, flexible on timing in autumn.',
8, 1, NULL, '2026-05-20T09:10:00Z', 'pending',
NULL, NULL, NULL, '2026-05-20T09:10:00Z'),
-- Waitlisted (dates clash with an existing booking) — two date options
('enq_breathwork', 'v_drawehn', 'Sophie Klar', 'sophie.klar@example.com', '+49 151 9988776', NULL,
'Breathwork Weekend', 'Guided breathwork and cold exposure.', 'Preferred dates may overlap another group.',
20, 0, 'Flexible by a week either side.', '2026-05-12T16:45:00Z', 'pending',
'2026-05-15T11:00:00Z', NULL, NULL, '2026-05-12T16:45:00Z');
INSERT OR IGNORE INTO enquiry_date_option
(option_id, enquiry_id, start_date, end_date, priority) VALUES
('edo_pottery_1', 'enq_pottery', '2026-09-18', '2026-09-20', 0),
('edo_breath_1', 'enq_breathwork', '2026-10-09', '2026-10-11', 0),
('edo_breath_2', 'enq_breathwork', '2026-10-16', '2026-10-18', 1);
-- ─── Demo auth accounts (better-auth credentials) ──────────────────────
-- Generated by .tmp-gen-seed-accounts.mjs. These let demo users log in
-- directly from the seed; passwords are documented below. The account.password
-- is a better-auth scrypt hash (salt:hash, self-contained → portable).
-- sarah@seminarhof.example → owner1234
-- christina@seminarhof.example → admin1234
-- demo@yoga-retreat.example → demo1234
-- <all client users> → client1234
INSERT OR IGNORE INTO "user" (id, email, name, locale, role, email_verified, created_at, updated_at) VALUES
('u_sarah', 'sarah@seminarhof.example', 'Sarah Drawehn', 'de', 'owner', 1, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('u_christina', 'christina@seminarhof.example', 'Christina Admin', 'de', 'admin', 1, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('u_demo', 'demo@yoga-retreat.example', 'Demo Organiser', 'en', 'client', 1, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z');
INSERT OR IGNORE INTO account (id, account_id, provider_id, user_id, password, created_at, updated_at) VALUES
('acct_u_sarah', 'u_sarah', 'credential', 'u_sarah', '3d22bc6eefcbad9f528ec3b3138a9520:c084a3eec4a456ed966e78e77b0f0e3b8cfc1531eb8d06379e8831fc9b97426feacde413767f4a765c7eb907bfe15c84b6ccba26bb3048adc711811f0e45af31', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('acct_u_christina', 'u_christina', 'credential', 'u_christina', '7d8cccdd9a221d22c77163cf5f386c0b:f0e8f680914ffab822d2ceaf172469c15352c5e5470397d149079c1c077915812beabc8a167f85b59e506f33e769bb1b1018a55820e7b66c13c35965a6ccde71', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('acct_u_demo', 'u_demo', 'credential', 'u_demo', '8048a18c1e5f5174f7d6741401d0e5da:6cceb3546aec90ad4e27c3fcea0da435eab4d85ecc3deb68f288acc01774b9fe85c1a2d514804d047e01df56435a5bff1affde91196b18883a43e0dea79dbceb', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('acct_u_breath', 'u_breath', 'credential', 'u_breath', '3042ba9c3b893b91f28cb45fccdd6766:7ddbe83af827f84e83dea9302388d007b96ab484025339eb70260849531d5897f4128bbf3a789bf6b49ddc1997659ccdcd9239e1e5e2dfe1a9f482913f3050c6', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('acct_u_silent', 'u_silent', 'credential', 'u_silent', 'ebbf8f286058ddf0023424706daed7cd:04286f7173acdc6c6e146eba2b47a21631a964cb7987f354c1a12958379abb74fc89f2c34f6ccb15aa557de969c0b88f88f6169eb380c66c6d75bedd37ee3d95', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('acct_u_dance', 'u_dance', 'credential', 'u_dance', 'aea4f8e5cd2abb13d4bd311762279a17:67f8a7435161108bd4561c60e8eac1c5ddccac31bdac3b66fb465dee0dd6787feec5a84ca3d2a8fb94370ccdfeac44d3d7417d7c2e1d5e0bc6582c7670b97fdd', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('acct_u_writers', 'u_writers', 'credential', 'u_writers', 'fdb4f899224c45faaad0eebcc119047a:99107e9232e7114a653bcf29702312ef1ceeb4277135fcc6ff0a69034076a8cb066d3a247478b7844446a5466d32b6402995dfff9efbfa6194e1a8369ed90bf0', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('acct_u_qigong', 'u_qigong', 'credential', 'u_qigong', '61b26a60ec6ffa7a8c06e21c160a05d3:0dbe469c60e36268152bb32629a14b3502ac88cf812191f92d886d9080e8807e8820dcb8c34999ff6dc9d5fbf109731dfcfc1e483dd9f4d5e2ed3cb0f23edc0a', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('acct_u_workation', 'u_workation', 'credential', 'u_workation', '6fca2b7798eb61b72f70935fdd8fcd78:8160b10d86917d3e0a36910cc930a47989ca998a5ed3af688873b2ee8dbd82ada7fde4f71578c0a141ae2525d5b1f766ca6616a38b411b841b7d31bd28369580', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
('acct_u_choir', 'u_choir', 'credential', 'u_choir', '129e67fd24ef2bb88126febc0bf9a951:3e0700ae7f90a1a623ccac1748fb2d928dd56b0a0b3bb8a84647eda17ac726690b89bdabb9e215d85fb5db2c496e93c51d3e784a6c1096b197080ac4a8507fa9', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z');

10
db/sqlite/0001-init.sql Normal file
View File

@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS kanban_card (
card_id TEXT PRIMARY KEY,
title TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'todo',
position INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_kanban_card_status_position
ON kanban_card (status, position);

View File

@@ -0,0 +1,218 @@
-- Drop kanban template scaffold.
DROP TABLE IF EXISTS kanban_card;
-- Seminarhof Drawehn core schema.
-- Multi-tenant from day one (venue_id on every domain row).
-- All money stored as integer cents (euro_cents) to avoid float drift.
--
-- Enum-valued columns carry a CHECK (col IN (...)) so the DB is the single
-- source of truth for each enum. The pikku CLI introspects these into
-- string-literal unions (db.types.ts + enums.gen.ts).
-- ─────────────────────────────────────────────────────────────────────
-- Tenancy
CREATE TABLE IF NOT EXISTS venue (
venue_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
locale_default TEXT NOT NULL DEFAULT 'de',
meal_window_breakfast TEXT NOT NULL DEFAULT '07:00-10:00',
meal_window_lunch TEXT NOT NULL DEFAULT '12:00-14:00',
meal_window_dinner TEXT NOT NULL DEFAULT '18:00-20:00',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- ─────────────────────────────────────────────────────────────────────
-- Identity
CREATE TABLE IF NOT EXISTS app_user (
user_id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
locale TEXT NOT NULL DEFAULT 'de',
role TEXT NOT NULL CHECK (role IN ('admin', 'client', 'owner')),
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS client (
client_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
name TEXT,
is_stammgruppe INTEGER NOT NULL DEFAULT 0,
billing_address TEXT,
contact_email TEXT,
contact_phone TEXT,
website TEXT,
notes TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS client_member (
client_id TEXT NOT NULL REFERENCES client(client_id),
user_id TEXT NOT NULL REFERENCES app_user(user_id),
role TEXT NOT NULL DEFAULT 'organiser' CHECK (role IN ('organiser', 'colleague')),
PRIMARY KEY (client_id, user_id)
);
-- ─────────────────────────────────────────────────────────────────────
-- Physical inventory
CREATE TABLE IF NOT EXISTS bathroom (
bathroom_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
label TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS room (
room_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
number INTEGER NOT NULL,
beds INTEGER NOT NULL,
room_type TEXT NOT NULL CHECK (room_type IN ('MBZ', 'DZ', 'EZ_only')),
building TEXT NOT NULL CHECK (building IN ('gaestehaus', 'haupthaus')),
floor TEXT NOT NULL CHECK (floor IN ('eg', 'og')),
bathroom_id TEXT REFERENCES bathroom(bathroom_id),
wheelchair INTEGER NOT NULL DEFAULT 0,
ground_floor INTEGER NOT NULL DEFAULT 0,
quiet INTEGER NOT NULL DEFAULT 0,
shared_bath INTEGER NOT NULL DEFAULT 0,
dogs_allowed INTEGER NOT NULL DEFAULT 0,
double_bed_140 INTEGER NOT NULL DEFAULT 0,
allergy_friendly INTEGER NOT NULL DEFAULT 0,
surcharge_eur_cents INTEGER NOT NULL DEFAULT 0,
UNIQUE (venue_id, number)
);
CREATE TABLE IF NOT EXISTS seminar_room (
seminar_room_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
name TEXT NOT NULL
);
-- ─────────────────────────────────────────────────────────────────────
-- Bookings
CREATE TABLE IF NOT EXISTS booking (
booking_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
client_id TEXT NOT NULL REFERENCES client(client_id),
event_name TEXT NOT NULL,
start_date TEXT,
end_date TEXT,
status TEXT NOT NULL CHECK (status IN ('enquiry', 'reserved', 'confirmed', 'ended', 'cancelled')),
half_house INTEGER NOT NULL DEFAULT 0,
online_ad INTEGER NOT NULL DEFAULT 0,
cover_image_url TEXT,
short_description TEXT,
organiser_website TEXT,
expected_persons INTEGER,
meal_time_breakfast TEXT,
meal_time_lunch TEXT,
meal_time_dinner TEXT,
daily_plan TEXT,
arrival_time TEXT,
departure_time TEXT,
payment_method TEXT NOT NULL DEFAULT 'transfer' CHECK (payment_method IN ('cash', 'transfer')),
agb_year INTEGER,
agb_accepted_at TEXT,
privacy_accepted_at TEXT,
notes TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_booking_venue_dates
ON booking (venue_id, start_date, end_date);
CREATE INDEX IF NOT EXISTS idx_booking_client
ON booking (client_id);
CREATE INDEX IF NOT EXISTS idx_booking_status
ON booking (venue_id, status);
CREATE TABLE IF NOT EXISTS booking_extra (
booking_extra_id TEXT PRIMARY KEY,
booking_id TEXT NOT NULL REFERENCES booking(booking_id),
kind TEXT NOT NULL CHECK (kind IN ('cake', 'second_seminar_room', 'early_arrival', 'bedlinen', 'massage_bench', 'sauna')),
qty INTEGER NOT NULL DEFAULT 1,
on_dates TEXT, -- JSON array of dates for cake-day picker
unit_price_cents INTEGER NOT NULL DEFAULT 0,
note TEXT
);
-- ─────────────────────────────────────────────────────────────────────
-- Participants
CREATE TABLE IF NOT EXISTS participant (
participant_id TEXT PRIMARY KEY,
booking_id TEXT NOT NULL REFERENCES booking(booking_id),
name TEXT NOT NULL,
email TEXT,
age INTEGER,
kind TEXT NOT NULL DEFAULT 'overnight' CHECK (kind IN ('overnight', 'day_guest')),
dietary_tag TEXT CHECK (dietary_tag IN ('omnivore', 'vegetarian', 'vegan', 'gluten_free', 'lactose_free', 'pescatarian', 'unknown')),
free_text TEXT,
self_form_token TEXT UNIQUE,
self_form_submitted_at TEXT,
notes TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_participant_booking
ON participant (booking_id);
CREATE TABLE IF NOT EXISTS participant_allergy (
participant_id TEXT NOT NULL REFERENCES participant(participant_id),
allergen TEXT NOT NULL CHECK (allergen IN ('nuts', 'soy', 'gluten_strict', 'dairy', 'egg', 'fish', 'celery', 'mustard', 'sesame', 'shellfish', 'other')),
severity TEXT NOT NULL DEFAULT 'standard' CHECK (severity IN ('standard', 'mild', 'moderate', 'severe', 'separate_prep')),
note TEXT,
PRIMARY KEY (participant_id, allergen)
);
CREATE TABLE IF NOT EXISTS room_assignment (
booking_id TEXT NOT NULL REFERENCES booking(booking_id),
participant_id TEXT NOT NULL REFERENCES participant(participant_id),
room_id TEXT NOT NULL REFERENCES room(room_id),
PRIMARY KEY (booking_id, participant_id)
);
CREATE INDEX IF NOT EXISTS idx_room_assignment_room
ON room_assignment (room_id);
-- ─────────────────────────────────────────────────────────────────────
-- Invoicing
CREATE TABLE IF NOT EXISTS invoice (
invoice_id TEXT PRIMARY KEY,
booking_id TEXT NOT NULL REFERENCES booking(booking_id),
kind TEXT NOT NULL CHECK (kind IN ('deposit', 'final')),
invoice_number TEXT NOT NULL UNIQUE,
issued_on TEXT NOT NULL,
due_on TEXT,
paid_on TEXT,
amount_cents INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'outstanding' CHECK (status IN ('outstanding', 'paid', 'cancelled')),
pdf_url TEXT,
datev_ref TEXT
);
CREATE INDEX IF NOT EXISTS idx_invoice_booking
ON invoice (booking_id);
-- ─────────────────────────────────────────────────────────────────────
-- Audit
CREATE TABLE IF NOT EXISTS audit_log (
audit_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
user_id TEXT REFERENCES app_user(user_id),
entity TEXT NOT NULL,
entity_id TEXT NOT NULL,
field TEXT,
before_value TEXT,
after_value TEXT,
action TEXT NOT NULL CHECK (action IN ('create', 'update', 'delete')),
at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_audit_entity
ON audit_log (entity, entity_id);

View File

@@ -0,0 +1,12 @@
-- Auth sessions: opaque cookie token → user_id mapping.
-- We avoid JWT to keep the dependency footprint small; revocation is just DELETE.
CREATE TABLE session (
session_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES app_user(user_id) ON DELETE CASCADE,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX session_user_idx ON session(user_id);
CREATE INDEX session_expires_idx ON session(expires_at);

View File

@@ -0,0 +1,4 @@
-- Password support for app_user. Format stored: pbkdf2:<iterations>:<salt_b64>:<hash_b64>
-- Existing rows have NULL password_hash and cannot log in until one is set.
ALTER TABLE app_user ADD COLUMN password_hash TEXT;

View File

@@ -0,0 +1,4 @@
-- Rename short_description -> description (the field is multi-sentence prose, not a
-- "short" blurb) and add a separate one-line event_outline captured on the enquiry form.
ALTER TABLE booking RENAME COLUMN short_description TO description;
ALTER TABLE booking ADD COLUMN event_outline TEXT;

View File

@@ -0,0 +1,17 @@
-- Booking lifecycle milestones.
--
-- The status enum is reshaped to a lean "bucket": enquiry | reserved |
-- confirmed | ended | cancelled. The contract + deposit progression that
-- runs inside `reserved` (over ~1 year) is tracked as the columns below plus the
-- deposit row on the invoice table — NOT as extra statuses. A daily cron reads
-- these columns; nothing here runs a long-lived workflow.
ALTER TABLE booking ADD COLUMN contract_sent_at TEXT; -- ISO; day-0 anchor + R1 idempotency guard
ALTER TABLE booking ADD COLUMN contract_response TEXT CHECK (contract_response IN ('approved', 'declined')); -- null = pending
ALTER TABLE booking ADD COLUMN contract_response_at TEXT; -- ISO
ALTER TABLE booking ADD COLUMN deposit_reminder_sent_at TEXT; -- ISO; R2 idempotency guard
ALTER TABLE booking ADD COLUMN flagged_at TEXT; -- ISO; set when admin review is needed
ALTER TABLE booking ADD COLUMN flag_reason TEXT CHECK (flag_reason IN ('deposit_overdue', 'short_window', 'contract_declined'));
-- Collapse the two retired statuses onto the new bucket (for any non-reset DB).
UPDATE booking SET status = 'reserved' WHERE status = 'form_received';
UPDATE booking SET status = 'confirmed' WHERE status = 'deposit_paid';

View File

@@ -0,0 +1,6 @@
-- Status transition timestamps. Set by applyBookingTransition when each
-- bucket is entered for the first time. No backfill — existing rows get NULL.
ALTER TABLE booking ADD COLUMN reserved_at TEXT;
ALTER TABLE booking ADD COLUMN confirmed_at TEXT;
ALTER TABLE booking ADD COLUMN ended_at TEXT;
ALTER TABLE booking ADD COLUMN cancelled_at TEXT;

View File

@@ -0,0 +1,64 @@
-- Public enquiries get their own table and lifecycle, separate from bookings.
-- A public submission lands here as `pending`. An admin then either:
-- approve → status=approved, a booking is created (copy) referencing this
-- enquiry via booking.enquiry_id (admin picks start/end + a client)
-- waitlist → waitlisted_at set (requires ≥1 date option); stays pending so it
-- can still be approved or declined later
-- declined → status=declined
--
-- `waitlisted_at` is a parallel timestamp on a pending row, NOT a status — a
-- waitlisted enquiry is still pending and can be approved or declined.
--
-- Dates live in `enquiry_date_option` (1-to-N, priority-ordered), NOT on the
-- enquiry row — a visitor can propose several date ranges in priority order
-- (E6.1). The enquiry has no single start/end of its own; reads that need a
-- "preferred" date pick the lowest-priority option.
CREATE TABLE IF NOT EXISTS enquiry (
enquiry_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
-- Contact details from the public form. No client row exists yet — one is
-- selected or created on approval.
contact_name TEXT,
contact_email TEXT NOT NULL,
contact_phone TEXT,
website TEXT,
-- Event details.
event_name TEXT NOT NULL,
event_outline TEXT,
description TEXT,
expected_persons INTEGER,
half_house INTEGER NOT NULL DEFAULT 0,
notes TEXT,
privacy_accepted_at TEXT,
-- Lifecycle.
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'declined')), -- 'waitlisted' is a derived UI state (pending + waitlisted_at), not a status
waitlisted_at TEXT,
approved_at TEXT,
declined_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_enquiry_venue_status
ON enquiry (venue_id, status);
-- Prioritized date options for an enquiry (E6.1). `priority` is 0-based; 0 is
-- the visitor's first choice. Optional: an enquiry may have zero options.
CREATE TABLE IF NOT EXISTS enquiry_date_option (
option_id TEXT PRIMARY KEY,
enquiry_id TEXT NOT NULL REFERENCES enquiry(enquiry_id) ON DELETE CASCADE,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
priority INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_enquiry_date_option_enquiry
ON enquiry_date_option (enquiry_id, priority);
-- A booking may originate from an approved enquiry. NULL for admin-created
-- bookings. UNIQUE so an enquiry maps to at most one booking (SQLite allows
-- many NULLs in a unique index).
ALTER TABLE booking ADD COLUMN enquiry_id TEXT REFERENCES enquiry(enquiry_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_booking_enquiry
ON booking (enquiry_id);

View File

@@ -0,0 +1,94 @@
-- Migrate from custom app_user/session to Better Auth.
-- Drop tables with FKs to app_user, drop app_user, recreate with better-auth schema.
-- Drop tables that reference app_user
DROP TABLE IF EXISTS client_member;
DROP TABLE IF EXISTS session;
DROP TABLE IF EXISTS audit_log;
DROP TABLE IF EXISTS app_user;
-- Better Auth core tables
CREATE TABLE IF NOT EXISTS "user" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL UNIQUE,
"email_verified" INTEGER NOT NULL,
"image" TEXT,
"created_at" TEXT NOT NULL,
"updated_at" TEXT NOT NULL,
"role" TEXT NOT NULL DEFAULT 'client' CHECK ("role" IN ('admin', 'client', 'owner')),
"locale" TEXT NOT NULL DEFAULT 'de'
);
CREATE TABLE IF NOT EXISTS "session" (
"id" TEXT NOT NULL PRIMARY KEY,
"expires_at" TEXT NOT NULL,
"token" TEXT NOT NULL UNIQUE,
"created_at" TEXT NOT NULL,
"updated_at" TEXT NOT NULL,
"ip_address" TEXT,
"user_agent" TEXT,
"user_id" TEXT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS "account" (
"id" TEXT NOT NULL PRIMARY KEY,
"account_id" TEXT NOT NULL,
"provider_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
"access_token" TEXT,
"refresh_token" TEXT,
"id_token" TEXT,
"access_token_expires_at" TEXT,
"refresh_token_expires_at" TEXT,
"scope" TEXT,
"password" TEXT,
"created_at" TEXT NOT NULL,
"updated_at" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "verification" (
"id" TEXT NOT NULL PRIMARY KEY,
"identifier" TEXT NOT NULL,
"value" TEXT NOT NULL,
"expires_at" TEXT NOT NULL,
"created_at" TEXT NOT NULL,
"updated_at" TEXT NOT NULL
);
-- Booking-client cookie sessions (kept separate from better-auth sessions)
CREATE TABLE IF NOT EXISTS booking_session (
session_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Recreate client_member referencing user(id)
CREATE TABLE IF NOT EXISTS client_member (
client_id TEXT NOT NULL REFERENCES client(client_id),
user_id TEXT NOT NULL REFERENCES "user" ("id"),
role TEXT NOT NULL DEFAULT 'organiser' CHECK (role IN ('organiser', 'colleague')),
PRIMARY KEY (client_id, user_id)
);
-- Recreate audit_log referencing user(id)
CREATE TABLE IF NOT EXISTS audit_log (
audit_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
user_id TEXT REFERENCES "user" ("id"),
entity TEXT NOT NULL,
entity_id TEXT NOT NULL,
field TEXT,
before_value TEXT,
after_value TEXT,
action TEXT NOT NULL CHECK (action IN ('create', 'update', 'delete')),
at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS session_user_id_idx ON "session" ("user_id");
CREATE INDEX IF NOT EXISTS account_user_id_idx ON "account" ("user_id");
CREATE INDEX IF NOT EXISTS verification_identifier_idx ON "verification" ("identifier");
CREATE INDEX IF NOT EXISTS booking_session_user_idx ON booking_session (user_id);
CREATE INDEX IF NOT EXISTS booking_session_expires_idx ON booking_session (expires_at);
CREATE INDEX IF NOT EXISTS audit_log_entity_idx ON audit_log (entity, entity_id);

View File

@@ -0,0 +1,28 @@
-- ============================================================
-- Fabric audit event sink — the table Pikku Fabric writes AuditEvent
-- records to. This is the framework-level event stream (distinct from any
-- domain-level row-change history). Columns mirror the AuditEvent shape
-- (camelCase → snake_case for the CamelCasePlugin kysely).
-- ============================================================
CREATE TABLE audit (
event_id TEXT PRIMARY KEY,
type TEXT NOT NULL,
source TEXT,
outcome TEXT,
occurred_at TEXT NOT NULL DEFAULT (datetime('now')),
function_id TEXT,
wire_type TEXT,
wire_id TEXT,
trace_id TEXT,
transaction_id TEXT,
query_id TEXT,
actor TEXT,
input TEXT,
metadata TEXT
);
CREATE INDEX idx_audit_type ON audit(type);
CREATE INDEX idx_audit_occurred ON audit(occurred_at);
CREATE INDEX idx_audit_function ON audit(function_id);
CREATE INDEX idx_audit_trace ON audit(trace_id);

View File

@@ -0,0 +1,32 @@
-- Tear out the hand-rolled `audit_log` domain-history table and realign the
-- framework `audit` sink to the canonical pikku AuditEvent shape (audit_id PK,
-- actor_user_id, data JSON). Domain row-change history now flows through the
-- pikku audit service — KyselyAuditService locally, a platform queue sink in
-- prod — persisted here and read back by get-booking-audit-log via metadata →
-- data. The old `audit` table had a drifted shape and was never written to.
DROP TABLE IF EXISTS audit_log;
DROP TABLE IF EXISTS audit;
CREATE TABLE audit (
audit_id TEXT NOT NULL PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
occurred_at TEXT NOT NULL DEFAULT (datetime('now')),
type TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'auto',
outcome TEXT,
function_id TEXT,
wire_type TEXT,
trace_id TEXT,
transaction_id TEXT,
query_id TEXT,
actor_user_id TEXT,
actor_org_id TEXT,
tables TEXT, -- JSON array of table names touched
changed_cols TEXT, -- JSON array of changed column names
event TEXT, -- custom event label
old TEXT, -- JSON: previous values
data TEXT -- JSON: new values / event payload
);
CREATE INDEX IF NOT EXISTS idx_audit_occurred_at ON audit (occurred_at);
CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit (actor_user_id) WHERE actor_user_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_audit_function ON audit (function_id) WHERE function_id IS NOT NULL;

View File

@@ -0,0 +1,11 @@
-- =============================================================================
-- BETTER AUTH ACTOR PLUGIN — user-flow actors
-- Adds the `actor` boolean the Better Auth `actor()` plugin declares so its
-- schema is covered by an explicit migration and the db drift check passes.
-- Actor rows are synthetic users signed in by pikkuUserFlow via
-- POST /api/auth/sign-in/actor with the server-held USER_FLOW_ACTOR_SECRET;
-- the flag rides into the pikku core session so audits/analytics can address
-- synthetic traffic. A non-actor user can never be signed in this way.
-- =============================================================================
ALTER TABLE "user" ADD COLUMN "actor" integer NOT NULL DEFAULT 0;

17
db/sqlite/0013-admin.sql Normal file
View File

@@ -0,0 +1,17 @@
-- =============================================================================
-- BETTER AUTH ADMIN PLUGIN — user administration
-- Adds the columns the Better Auth `admin()` plugin declares so its schema is
-- covered by an explicit migration and the db drift check passes. The plugin
-- exposes /api/auth/admin/* (listUsers, setRole, ban, impersonateUser, …);
-- `impersonated_by` marks a session created via "view as user" so it can be
-- reverted with admin.stopImpersonating. All fields are optional — a user with
-- `role` = 'admin' is an administrator; everyone else stays a normal user.
-- `role` already exists on the user table (0009-better-auth.sql), so only the
-- admin-specific columns are added here.
-- =============================================================================
ALTER TABLE "user" ADD COLUMN "banned" integer DEFAULT 0;
ALTER TABLE "user" ADD COLUMN "ban_reason" text;
ALTER TABLE "user" ADD COLUMN "ban_expires" date;
ALTER TABLE "session" ADD COLUMN "impersonated_by" text;

13
db/sqlite/0014-fabric.sql Normal file
View File

@@ -0,0 +1,13 @@
-- =============================================================================
-- BETTER AUTH FABRIC PLUGIN — Fabric operator admin sessions
-- Adds the `fabric` boolean the Better Auth `fabric()` plugin declares so its
-- schema is covered by an explicit migration and the db drift check passes.
-- Fabric rows are synthetic operator users minted by POST /api/auth/sign-in/fabric
-- after verifying a short-lived RS256 token the Fabric control plane signed
-- (checked against FABRIC_AUTH_PUBLIC_KEY). They are created with role 'admin' so
-- the console Users tab can list/impersonate real end-users WITHOUT the operator
-- being one of them; these rows are filtered out of any end-user listing. A real
-- user can never be signed in this way.
-- =============================================================================
ALTER TABLE "user" ADD COLUMN "fabric" integer NOT NULL DEFAULT 0;