v0.1.0:25–40 — tier model, edit forms, force-delete, license counts, migration 0009 (and hotfix); KEYSAT_INTEGRATION.md merged with downstream-LLM revisions
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
-- Tiered pricing UX (v0.1.0:27).
|
||||
--
|
||||
-- Two changes, both additive:
|
||||
--
|
||||
-- 1. Mark policies as buyer-visible. Operators may have policies they don't
|
||||
-- want to render on the public /buy/<slug> page (e.g. "Comp / press
|
||||
-- giveaway", "Internal team seat"). Defaults to public=1 so existing
|
||||
-- policies keep their current behaviour.
|
||||
--
|
||||
-- 2. Remember which policy the buyer chose at purchase time. Today,
|
||||
-- `issue_license_for_invoice` picks the "default" policy (or first
|
||||
-- active) for the product. With multi-tier pricing, the buyer's
|
||||
-- explicit choice needs to round-trip from /buy → BTCPay invoice →
|
||||
-- settlement webhook → license issuance. Storing it on the invoice is
|
||||
-- the simplest place — it sticks even if the policy is later
|
||||
-- deactivated, and the FK keeps integrity. NULL means "fall back to
|
||||
-- the product's default policy" for backwards compatibility with
|
||||
-- pre-:27 invoices.
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
ALTER TABLE policies ADD COLUMN public INTEGER NOT NULL DEFAULT 1;
|
||||
ALTER TABLE invoices ADD COLUMN policy_id TEXT REFERENCES policies(id);
|
||||
|
||||
-- Helps the public buy-page endpoint enumerate visible tiers cheaply.
|
||||
CREATE INDEX IF NOT EXISTS idx_policies_public ON policies(public);
|
||||
@@ -0,0 +1,26 @@
|
||||
-- Web UI password + session-based authentication.
|
||||
--
|
||||
-- Until v0.1.0:28 the only credential was the admin API key, which the
|
||||
-- SPA stored in localStorage every login. This migration sets up the
|
||||
-- alternate path: the operator sets a password (argon2id-hashed in the
|
||||
-- settings table under key 'web_ui_password_hash'); successful login
|
||||
-- issues a session token stored as an HttpOnly cookie. The API key
|
||||
-- continues to work for automation; admin endpoints accept either
|
||||
-- credential.
|
||||
--
|
||||
-- A future migration may add per-user accounts. For v0.1 there's a
|
||||
-- single admin password — the StartOS service is single-tenant by
|
||||
-- design and an operator's StartOS already gates physical access.
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
token TEXT PRIMARY KEY, -- random 32-byte URL-safe base64
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL, -- ISO-8601 UTC
|
||||
last_seen_at TEXT NOT NULL,
|
||||
ip TEXT,
|
||||
user_agent TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
||||
@@ -0,0 +1,54 @@
|
||||
-- Allow `kind = 'set_price'` on discount_codes (added at the daemon
|
||||
-- level in v0.1.0:26 but the migration that created the CHECK constraint
|
||||
-- in 0004 didn't include it, so existing instances reject the new kind
|
||||
-- with "CHECK constraint failed").
|
||||
--
|
||||
-- SQLite doesn't support ALTER TABLE ... DROP CONSTRAINT, so we rebuild
|
||||
-- the table: copy → drop old → rename. sqlx-migrate already wraps each
|
||||
-- .sql file in a transaction, so we DON'T do BEGIN/COMMIT here (nested
|
||||
-- transactions are not supported in SQLite).
|
||||
--
|
||||
-- `PRAGMA defer_foreign_keys = 1` is the transaction-local equivalent
|
||||
-- of `foreign_keys = OFF`: it postpones FK constraint checks until
|
||||
-- COMMIT time. This lets us drop the old discount_codes table without
|
||||
-- the immediate FK check from discount_redemptions.code_id failing.
|
||||
-- The IDs are preserved across the rebuild, so when the FK check runs
|
||||
-- at COMMIT, every referencing row still resolves cleanly.
|
||||
|
||||
PRAGMA defer_foreign_keys = 1;
|
||||
|
||||
CREATE TABLE discount_codes_new (
|
||||
id TEXT PRIMARY KEY,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
kind TEXT NOT NULL, -- 'percent' | 'fixed_sats' | 'set_price' | 'free_license'
|
||||
amount INTEGER NOT NULL,
|
||||
max_uses INTEGER,
|
||||
used_count INTEGER NOT NULL DEFAULT 0,
|
||||
expires_at TEXT,
|
||||
applies_to_product_id TEXT,
|
||||
applies_to_policy_id TEXT,
|
||||
referrer_label TEXT,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (applies_to_product_id) REFERENCES products(id),
|
||||
FOREIGN KEY (applies_to_policy_id) REFERENCES policies(id),
|
||||
CHECK (kind IN ('percent', 'fixed_sats', 'set_price', 'free_license')),
|
||||
CHECK (amount >= 0),
|
||||
CHECK (used_count >= 0)
|
||||
);
|
||||
|
||||
INSERT INTO discount_codes_new
|
||||
SELECT id, code, kind, amount, max_uses, used_count, expires_at,
|
||||
applies_to_product_id, applies_to_policy_id, referrer_label,
|
||||
description, active, created_at, updated_at
|
||||
FROM discount_codes;
|
||||
|
||||
DROP TABLE discount_codes;
|
||||
ALTER TABLE discount_codes_new RENAME TO discount_codes;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_discount_codes_active ON discount_codes(active);
|
||||
CREATE INDEX IF NOT EXISTS idx_discount_codes_product ON discount_codes(applies_to_product_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_discount_codes_policy ON discount_codes(applies_to_policy_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_discount_codes_expires ON discount_codes(expires_at);
|
||||
Reference in New Issue
Block a user