Fresha-style booking: shops sign up, add services/therapists/hours, and get a public booking page they can drop into their Google Business Profile so customers book from Maps. Deposits via Stripe stop no-shows. Stack: Hono + Cloudflare Workers + D1 + Stripe, server-rendered HTML. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
126 lines
4.3 KiB
SQL
126 lines
4.3 KiB
SQL
-- Kneadly — booking platform for massage shops
|
|
-- Multi-tenant: one owner account owns one shop (schema allows more later).
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT PRIMARY KEY,
|
|
email TEXT UNIQUE NOT NULL,
|
|
name TEXT NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL,
|
|
expires_at INTEGER NOT NULL,
|
|
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
-- The massage business / shop
|
|
CREATE TABLE IF NOT EXISTS shops (
|
|
id TEXT PRIMARY KEY,
|
|
owner_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
slug TEXT UNIQUE NOT NULL,
|
|
tagline TEXT,
|
|
about TEXT,
|
|
phone TEXT,
|
|
email TEXT,
|
|
address TEXT,
|
|
suburb TEXT,
|
|
state TEXT,
|
|
postcode TEXT,
|
|
timezone TEXT NOT NULL DEFAULT 'Australia/Sydney',
|
|
emoji TEXT NOT NULL DEFAULT '💆',
|
|
accent TEXT NOT NULL DEFAULT '#0f766e',
|
|
currency TEXT NOT NULL DEFAULT 'aud',
|
|
deposit_pct INTEGER NOT NULL DEFAULT 20, -- % of service price taken as deposit
|
|
cancellation_hours INTEGER NOT NULL DEFAULT 24,
|
|
is_published INTEGER NOT NULL DEFAULT 1,
|
|
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_shops_owner ON shops(owner_id);
|
|
|
|
-- Services offered (e.g. "60min Deep Tissue")
|
|
CREATE TABLE IF NOT EXISTS services (
|
|
id TEXT PRIMARY KEY,
|
|
shop_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
duration_minutes INTEGER NOT NULL DEFAULT 60,
|
|
price_cents INTEGER NOT NULL DEFAULT 0,
|
|
category TEXT NOT NULL DEFAULT 'Massage',
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_services_shop ON services(shop_id, is_active);
|
|
|
|
-- Therapists / massage staff
|
|
CREATE TABLE IF NOT EXISTS staff (
|
|
id TEXT PRIMARY KEY,
|
|
shop_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
title TEXT DEFAULT 'Massage Therapist',
|
|
bio TEXT,
|
|
emoji TEXT NOT NULL DEFAULT '🧑⚕️',
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_staff_shop ON staff(shop_id, is_active);
|
|
|
|
-- Which staff can perform which services (absence of rows for a service = all staff)
|
|
CREATE TABLE IF NOT EXISTS staff_services (
|
|
staff_id TEXT NOT NULL,
|
|
service_id TEXT NOT NULL,
|
|
PRIMARY KEY (staff_id, service_id),
|
|
FOREIGN KEY (staff_id) REFERENCES staff(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
|
|
);
|
|
|
|
-- Weekly working hours, per staff member
|
|
CREATE TABLE IF NOT EXISTS availability (
|
|
id TEXT PRIMARY KEY,
|
|
staff_id TEXT NOT NULL,
|
|
day_of_week INTEGER NOT NULL, -- 0 = Sunday .. 6 = Saturday
|
|
start_time TEXT NOT NULL, -- 'HH:MM'
|
|
end_time TEXT NOT NULL,
|
|
UNIQUE(staff_id, day_of_week),
|
|
FOREIGN KEY (staff_id) REFERENCES staff(id) ON DELETE CASCADE
|
|
);
|
|
|
|
-- Customer appointments
|
|
CREATE TABLE IF NOT EXISTS bookings (
|
|
id TEXT PRIMARY KEY,
|
|
shop_id TEXT NOT NULL,
|
|
service_id TEXT NOT NULL,
|
|
staff_id TEXT NOT NULL,
|
|
customer_name TEXT NOT NULL,
|
|
customer_email TEXT NOT NULL,
|
|
customer_phone TEXT,
|
|
start_time INTEGER NOT NULL, -- unix seconds (UTC)
|
|
end_time INTEGER NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending_payment', -- pending_payment | confirmed | completed | cancelled | no_show
|
|
price_cents INTEGER NOT NULL DEFAULT 0,
|
|
deposit_cents INTEGER NOT NULL DEFAULT 0,
|
|
service_name TEXT,
|
|
staff_name TEXT,
|
|
notes TEXT,
|
|
stripe_session_id TEXT,
|
|
stripe_payment_intent_id TEXT,
|
|
stripe_charge_id TEXT,
|
|
refunded_at INTEGER,
|
|
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (service_id) REFERENCES services(id),
|
|
FOREIGN KEY (staff_id) REFERENCES staff(id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_bookings_shop ON bookings(shop_id, start_time);
|
|
CREATE INDEX IF NOT EXISTS idx_bookings_staff ON bookings(staff_id, start_time);
|
|
CREATE INDEX IF NOT EXISTS idx_bookings_stripe ON bookings(stripe_session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|