Files
kaffeekasse-saas/database/schema.sqlite.sql
T
2026-06-17 16:45:14 +02:00

278 lines
11 KiB
SQL

CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
full_name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
platform_role TEXT NOT NULL DEFAULT 'user' CHECK(platform_role IN ('platform_admin', 'user')),
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS password_reset_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
email TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
requested_ip_hash TEXT,
requested_user_agent_hash TEXT,
expires_at TEXT NOT NULL,
consumed_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_password_reset_user_created ON password_reset_tokens (user_id, created_at);
CREATE INDEX IF NOT EXISTS idx_password_reset_expires ON password_reset_tokens (expires_at);
CREATE TABLE IF NOT EXISTS rate_limits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
action_key TEXT NOT NULL,
bucket_key TEXT NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
window_start TEXT NOT NULL,
last_attempt_at TEXT NOT NULL,
blocked_until TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (action_key, bucket_key)
);
CREATE INDEX IF NOT EXISTS idx_rate_limit_blocked_until ON rate_limits (blocked_until);
CREATE INDEX IF NOT EXISTS idx_rate_limit_last_attempt ON rate_limits (last_attempt_at);
CREATE TABLE IF NOT EXISTS tenants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
plan TEXT NOT NULL DEFAULT 'starter' CHECK(plan IN ('starter', 'team', 'business')),
status TEXT NOT NULL DEFAULT 'trial' CHECK(status IN ('trial', 'active', 'suspended')),
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS tenant_domains (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
host TEXT NOT NULL UNIQUE,
is_primary INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS tenant_memberships (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role TEXT NOT NULL DEFAULT 'member' CHECK(role IN ('owner', 'admin', 'member', 'device')),
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'invited', 'disabled')),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (tenant_id, user_id),
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS tenant_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL UNIQUE,
currency_code TEXT NOT NULL DEFAULT 'EUR',
default_product_price_cents INTEGER NOT NULL DEFAULT 120,
allow_self_service INTEGER NOT NULL DEFAULT 1,
allow_paper_lists INTEGER NOT NULL DEFAULT 1,
allow_rfid INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS members (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
user_id INTEGER,
display_name TEXT NOT NULL,
email TEXT,
access_pin TEXT,
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived')),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (tenant_id, email),
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
name TEXT NOT NULL,
sku TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS product_prices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
price_cents INTEGER NOT NULL,
valid_from TEXT NOT NULL,
valid_until TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS capture_sources (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
code TEXT NOT NULL,
name TEXT NOT NULL,
channel TEXT NOT NULL CHECK(channel IN ('paper', 'digital', 'admin', 'rfid')),
is_system INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (tenant_id, code),
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS paper_sheets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
title TEXT NOT NULL,
period_label TEXT,
period_start TEXT,
period_end TEXT,
status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft', 'posted')),
notes TEXT,
created_by_user_id INTEGER,
posted_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE,
FOREIGN KEY (created_by_user_id) REFERENCES users (id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS paper_sheet_lines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
paper_sheet_id INTEGER NOT NULL,
tenant_id INTEGER NOT NULL,
member_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity REAL NOT NULL DEFAULT 1.00,
effective_at TEXT,
note TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (paper_sheet_id) REFERENCES paper_sheets (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE,
FOREIGN KEY (member_id) REFERENCES members (id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS consumption_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
member_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
source_id INTEGER NOT NULL,
paper_sheet_line_id INTEGER,
quantity REAL NOT NULL DEFAULT 1.00,
unit_price_cents INTEGER NOT NULL,
total_cents INTEGER NOT NULL,
effective_at TEXT NOT NULL,
recorded_at TEXT NOT NULL,
recorded_by_user_id INTEGER,
source_reference TEXT,
notes TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE,
FOREIGN KEY (member_id) REFERENCES members (id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
FOREIGN KEY (source_id) REFERENCES capture_sources (id) ON DELETE CASCADE,
FOREIGN KEY (paper_sheet_line_id) REFERENCES paper_sheet_lines (id) ON DELETE SET NULL,
FOREIGN KEY (recorded_by_user_id) REFERENCES users (id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS ledger_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
member_id INTEGER,
source_id INTEGER,
type TEXT NOT NULL CHECK(type IN ('consumption', 'payment', 'correction', 'credit', 'opening_balance')),
reference_type TEXT,
reference_id INTEGER,
description TEXT NOT NULL,
occurred_at TEXT NOT NULL,
created_by_user_id INTEGER,
reversal_of_entry_id INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE,
FOREIGN KEY (member_id) REFERENCES members (id) ON DELETE SET NULL,
FOREIGN KEY (source_id) REFERENCES capture_sources (id) ON DELETE SET NULL,
FOREIGN KEY (created_by_user_id) REFERENCES users (id) ON DELETE SET NULL,
FOREIGN KEY (reversal_of_entry_id) REFERENCES ledger_entries (id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS ledger_lines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ledger_entry_id INTEGER NOT NULL,
tenant_id INTEGER NOT NULL,
member_id INTEGER,
account_code TEXT NOT NULL CHECK(account_code IN ('member_balance', 'cashbox', 'coffee_revenue')),
amount_cents INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (ledger_entry_id) REFERENCES ledger_entries (id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE,
FOREIGN KEY (member_id) REFERENCES members (id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS rfid_devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
name TEXT NOT NULL,
location_label TEXT,
device_token TEXT NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT 'planned' CHECK(status IN ('planned', 'active', 'disabled')),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS rfid_tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
member_id INTEGER,
uid_hash TEXT NOT NULL,
label TEXT,
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'lost', 'retired')),
last_seen_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (tenant_id, uid_hash),
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE,
FOREIGN KEY (member_id) REFERENCES members (id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS rfid_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
device_id INTEGER,
external_event_id TEXT,
uid_hash TEXT NOT NULL,
payload_json TEXT,
status TEXT NOT NULL DEFAULT 'received' CHECK(status IN ('received', 'linked', 'ignored', 'error')),
received_at TEXT NOT NULL,
processed_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (tenant_id, external_event_id),
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES rfid_devices (id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS audit_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER,
actor_user_id INTEGER,
action TEXT NOT NULL,
target_type TEXT NOT NULL,
target_id INTEGER,
result TEXT NOT NULL CHECK(result IN ('success', 'failure')),
request_id TEXT NOT NULL,
ip_hash TEXT,
user_agent_hash TEXT,
meta_json TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE SET NULL,
FOREIGN KEY (actor_user_id) REFERENCES users (id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_audit_tenant_created ON audit_events (tenant_id, created_at);