-- gitea-saas-core billing schema (SQLite). -- -- Idempotent by construction: every object is created with IF NOT EXISTS, -- so executing this file any number of times is safe. -- -- Conventions: -- * All amounts are INTEGER minor units (cents). No floats, ever. -- * Currency defaults to USD. -- * Timestamps are ISO-8601 UTC text written by the application layer; -- the DEFAULT expressions are fallbacks for manual/one-off inserts only. -- * ledger.kind is constrained to {credit, debit, hold, release, refund}. -- * ledger.idempotency_key is globally UNIQUE: every mutation is replay-safe. CREATE TABLE IF NOT EXISTS clients ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, tier TEXT NOT NULL DEFAULT 'free', status TEXT NOT NULL DEFAULT 'active', meta_json TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ); CREATE TABLE IF NOT EXISTS ledger ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER NOT NULL REFERENCES clients(id), kind TEXT NOT NULL CHECK (kind IN ('credit', 'debit', 'hold', 'release', 'refund')), amount_minor INTEGER NOT NULL CHECK (amount_minor >= 0), currency TEXT NOT NULL DEFAULT 'USD', idempotency_key TEXT NOT NULL UNIQUE, ref_type TEXT, ref_id TEXT, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ); CREATE TABLE IF NOT EXISTS holds ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER NOT NULL REFERENCES clients(id), amount_minor INTEGER NOT NULL CHECK (amount_minor >= 0), ref_type TEXT, ref_id TEXT, expires_at TEXT NOT NULL, released_at TEXT ); -- Balance / listing lookups. CREATE INDEX IF NOT EXISTS idx_ledger_client_created ON ledger (client_id, created_at, id); CREATE INDEX IF NOT EXISTS idx_ledger_client_kind ON ledger (client_id, kind); CREATE INDEX IF NOT EXISTS idx_holds_client ON holds (client_id); CREATE INDEX IF NOT EXISTS idx_holds_stale ON holds (client_id) WHERE released_at IS NULL;