chore: sync current workspace changes
Desktop CI Build / Build macOS ARM64 DMG (push) Has been cancelled
Desktop CI Build / Build Windows x64 (push) Has been cancelled
Desktop CI Build / Build macOS ARM64 DMG (push) Has been cancelled
Desktop CI Build / Build Windows x64 (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
CREATE INDEX IF NOT EXISTS background_tasks_task_type_created_at_desc_idx
|
||||
ON background_tasks (task_type, created_at DESC, id DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS background_tasks_created_at_desc_idx
|
||||
ON background_tasks (created_at DESC, id DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS background_tasks_task_type_status_created_at_idx
|
||||
ON background_tasks (task_type, status, created_at);
|
||||
@@ -0,0 +1,28 @@
|
||||
-- Speed up workspace asset gallery queries.
|
||||
-- The gallery only lists completed, non-deleted creative outputs; partial
|
||||
-- indexes keep the hot path small and allow ORDER BY created_at DESC, id DESC
|
||||
-- to avoid an explicit sort.
|
||||
|
||||
CREATE INDEX IF NOT EXISTS workspace_assets_completed_user_created_idx
|
||||
ON workspace_assets (user_id, created_at DESC, id DESC)
|
||||
WHERE deleted_at IS NULL AND status = 'completed';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS workspace_assets_completed_created_idx
|
||||
ON workspace_assets (created_at DESC, id DESC)
|
||||
WHERE deleted_at IS NULL AND status = 'completed';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS workspace_assets_completed_source_created_idx
|
||||
ON workspace_assets (source, created_at DESC, id DESC)
|
||||
WHERE deleted_at IS NULL AND status = 'completed';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS workspace_assets_completed_kind_created_idx
|
||||
ON workspace_assets (kind, created_at DESC, id DESC)
|
||||
WHERE deleted_at IS NULL AND status = 'completed';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS workspace_assets_completed_user_source_created_idx
|
||||
ON workspace_assets (user_id, source, created_at DESC, id DESC)
|
||||
WHERE deleted_at IS NULL AND status = 'completed';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS workspace_assets_completed_user_kind_created_idx
|
||||
ON workspace_assets (user_id, kind, created_at DESC, id DESC)
|
||||
WHERE deleted_at IS NULL AND status = 'completed';
|
||||
@@ -0,0 +1,55 @@
|
||||
-- Payment orders own fulfillment. Keep the charged amount separate from the
|
||||
-- API quota granted so non-USD providers cannot accidentally grant a nominally
|
||||
-- equal USD balance.
|
||||
ALTER TABLE payment_orders ADD COLUMN credit_amount REAL NOT NULL DEFAULT 0;
|
||||
ALTER TABLE payment_orders ADD COLUMN credit_unit TEXT NOT NULL DEFAULT 'USD API quota';
|
||||
|
||||
ALTER TABLE user_recharge_records ADD COLUMN currency TEXT;
|
||||
ALTER TABLE user_recharge_records ADD COLUMN credit_amount REAL;
|
||||
ALTER TABLE user_recharge_records ADD COLUMN credit_unit TEXT;
|
||||
|
||||
-- One row is inserted in the same transaction as the balance mutation. The
|
||||
-- primary key is the durable idempotency guard for duplicate payment callbacks.
|
||||
CREATE TABLE IF NOT EXISTS payment_order_fulfillments (
|
||||
payment_order_id INTEGER PRIMARY KEY NOT NULL,
|
||||
credited_key_id INTEGER,
|
||||
credit_amount REAL NOT NULL,
|
||||
credit_unit TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (payment_order_id) REFERENCES payment_orders(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (credited_key_id) REFERENCES downstream_api_keys(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS payment_order_fulfillments_key_idx
|
||||
ON payment_order_fulfillments (credited_key_id);
|
||||
|
||||
-- These tables were originally created lazily. Keep the runtime fallback for
|
||||
-- old installs, but ship their canonical schema through migrations.
|
||||
CREATE TABLE IF NOT EXISTS credit_ledger (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
track TEXT NOT NULL,
|
||||
entry_type TEXT NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
balance_after REAL NOT NULL,
|
||||
ref_type TEXT,
|
||||
ref_id TEXT,
|
||||
model TEXT,
|
||||
operator TEXT,
|
||||
note TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS credit_ledger_user_created_idx
|
||||
ON credit_ledger (user_id, created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS credit_ledger_track_type_idx
|
||||
ON credit_ledger (track, entry_type);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_credits (
|
||||
user_id INTEGER PRIMARY KEY NOT NULL,
|
||||
points_balance REAL NOT NULL DEFAULT 0,
|
||||
points_total_recharged REAL NOT NULL DEFAULT 0,
|
||||
updated_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
@@ -0,0 +1,93 @@
|
||||
-- The workspace gallery predicate includes several JSON branches. Index the
|
||||
-- full predicate so SQLite can satisfy both filtering and ordering without
|
||||
-- reparsing large metadata documents for every completed asset.
|
||||
|
||||
CREATE INDEX IF NOT EXISTS workspace_assets_gallery_created_idx
|
||||
ON workspace_assets (created_at DESC, id DESC)
|
||||
WHERE deleted_at IS NULL
|
||||
AND status = 'completed'
|
||||
AND (
|
||||
json_extract(metadata, '$.workspaceAsset.role') = 'output'
|
||||
OR json_extract(metadata, '$.workflowUi.type') = 'output'
|
||||
OR (
|
||||
source IN ('picture-agent', 'workflow-studio')
|
||||
AND json_type(metadata, '$.responseItem') IS NOT NULL
|
||||
)
|
||||
OR json_type(metadata, '$.moneyPrinter') IS NOT NULL
|
||||
OR json_type(metadata, '$.digitalHuman') IS NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS workspace_assets_gallery_user_created_idx
|
||||
ON workspace_assets (user_id, created_at DESC, id DESC)
|
||||
WHERE deleted_at IS NULL
|
||||
AND status = 'completed'
|
||||
AND (
|
||||
json_extract(metadata, '$.workspaceAsset.role') = 'output'
|
||||
OR json_extract(metadata, '$.workflowUi.type') = 'output'
|
||||
OR (
|
||||
source IN ('picture-agent', 'workflow-studio')
|
||||
AND json_type(metadata, '$.responseItem') IS NOT NULL
|
||||
)
|
||||
OR json_type(metadata, '$.moneyPrinter') IS NOT NULL
|
||||
OR json_type(metadata, '$.digitalHuman') IS NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS workspace_assets_gallery_source_created_idx
|
||||
ON workspace_assets (source, created_at DESC, id DESC)
|
||||
WHERE deleted_at IS NULL
|
||||
AND status = 'completed'
|
||||
AND (
|
||||
json_extract(metadata, '$.workspaceAsset.role') = 'output'
|
||||
OR json_extract(metadata, '$.workflowUi.type') = 'output'
|
||||
OR (
|
||||
source IN ('picture-agent', 'workflow-studio')
|
||||
AND json_type(metadata, '$.responseItem') IS NOT NULL
|
||||
)
|
||||
OR json_type(metadata, '$.moneyPrinter') IS NOT NULL
|
||||
OR json_type(metadata, '$.digitalHuman') IS NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS workspace_assets_gallery_kind_created_idx
|
||||
ON workspace_assets (kind, created_at DESC, id DESC)
|
||||
WHERE deleted_at IS NULL
|
||||
AND status = 'completed'
|
||||
AND (
|
||||
json_extract(metadata, '$.workspaceAsset.role') = 'output'
|
||||
OR json_extract(metadata, '$.workflowUi.type') = 'output'
|
||||
OR (
|
||||
source IN ('picture-agent', 'workflow-studio')
|
||||
AND json_type(metadata, '$.responseItem') IS NOT NULL
|
||||
)
|
||||
OR json_type(metadata, '$.moneyPrinter') IS NOT NULL
|
||||
OR json_type(metadata, '$.digitalHuman') IS NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS workspace_assets_gallery_user_source_created_idx
|
||||
ON workspace_assets (user_id, source, created_at DESC, id DESC)
|
||||
WHERE deleted_at IS NULL
|
||||
AND status = 'completed'
|
||||
AND (
|
||||
json_extract(metadata, '$.workspaceAsset.role') = 'output'
|
||||
OR json_extract(metadata, '$.workflowUi.type') = 'output'
|
||||
OR (
|
||||
source IN ('picture-agent', 'workflow-studio')
|
||||
AND json_type(metadata, '$.responseItem') IS NOT NULL
|
||||
)
|
||||
OR json_type(metadata, '$.moneyPrinter') IS NOT NULL
|
||||
OR json_type(metadata, '$.digitalHuman') IS NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS workspace_assets_gallery_user_kind_created_idx
|
||||
ON workspace_assets (user_id, kind, created_at DESC, id DESC)
|
||||
WHERE deleted_at IS NULL
|
||||
AND status = 'completed'
|
||||
AND (
|
||||
json_extract(metadata, '$.workspaceAsset.role') = 'output'
|
||||
OR json_extract(metadata, '$.workflowUi.type') = 'output'
|
||||
OR (
|
||||
source IN ('picture-agent', 'workflow-studio')
|
||||
AND json_type(metadata, '$.responseItem') IS NOT NULL
|
||||
)
|
||||
OR json_type(metadata, '$.moneyPrinter') IS NOT NULL
|
||||
OR json_type(metadata, '$.digitalHuman') IS NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,125 @@
|
||||
-- Account-level API billing. Workflow points remain in user_credits and are
|
||||
-- intentionally not migrated into this account.
|
||||
CREATE TABLE IF NOT EXISTS api_billing_accounts (
|
||||
user_id INTEGER PRIMARY KEY NOT NULL,
|
||||
available_micros INTEGER NOT NULL DEFAULT 0,
|
||||
reserved_micros INTEGER NOT NULL DEFAULT 0,
|
||||
debt_micros INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
version INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CHECK (available_micros >= 0),
|
||||
CHECK (reserved_micros >= 0),
|
||||
CHECK (debt_micros >= 0),
|
||||
CHECK (status IN ('active', 'frozen', 'closed'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS api_credit_grants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
public_id TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
source_type TEXT NOT NULL,
|
||||
source_id TEXT,
|
||||
payment_order_id INTEGER,
|
||||
total_micros INTEGER NOT NULL,
|
||||
remaining_micros INTEGER NOT NULL,
|
||||
reserved_micros INTEGER NOT NULL DEFAULT 0,
|
||||
starts_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
expires_at TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
scope_json TEXT,
|
||||
idempotency_key TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (payment_order_id) REFERENCES payment_orders(id) ON DELETE SET NULL,
|
||||
CHECK (total_micros > 0),
|
||||
CHECK (remaining_micros >= 0),
|
||||
CHECK (reserved_micros >= 0),
|
||||
CHECK (remaining_micros + reserved_micros <= total_micros),
|
||||
CHECK (status IN ('scheduled', 'active', 'refund_pending', 'exhausted', 'expired', 'revoked'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS api_credit_grants_public_id_unique
|
||||
ON api_credit_grants (public_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS api_credit_grants_idempotency_unique
|
||||
ON api_credit_grants (idempotency_key);
|
||||
CREATE INDEX IF NOT EXISTS api_credit_grants_spend_idx
|
||||
ON api_credit_grants (user_id, status, starts_at, expires_at, id);
|
||||
CREATE INDEX IF NOT EXISTS api_credit_grants_order_idx
|
||||
ON api_credit_grants (payment_order_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS api_entitlement_ledger (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
entry_type TEXT NOT NULL,
|
||||
amount_micros INTEGER NOT NULL,
|
||||
balance_after_micros INTEGER NOT NULL,
|
||||
grant_id INTEGER,
|
||||
ref_type TEXT,
|
||||
ref_id TEXT,
|
||||
model TEXT,
|
||||
operator TEXT,
|
||||
note TEXT,
|
||||
idempotency_key TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (grant_id) REFERENCES api_credit_grants(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS api_entitlement_ledger_idempotency_unique
|
||||
ON api_entitlement_ledger (idempotency_key);
|
||||
CREATE INDEX IF NOT EXISTS api_entitlement_ledger_user_created_idx
|
||||
ON api_entitlement_ledger (user_id, created_at DESC, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS api_entitlement_ledger_ref_idx
|
||||
ON api_entitlement_ledger (ref_type, ref_id);
|
||||
|
||||
-- Preserve the finite remaining API credit users had before account billing.
|
||||
-- NULL max_cost was an accidental unlimited bypass and is never converted to
|
||||
-- an unlimited account entitlement.
|
||||
INSERT OR IGNORE INTO api_billing_accounts (user_id, available_micros)
|
||||
SELECT
|
||||
u.id,
|
||||
CAST(ROUND(COALESCE(SUM(
|
||||
CASE
|
||||
WHEN k.max_cost IS NOT NULL
|
||||
THEN MAX(k.max_cost - COALESCE(k.used_cost, 0), 0) * 1000000.0
|
||||
ELSE 0
|
||||
END
|
||||
), 0)) AS INTEGER)
|
||||
FROM users u
|
||||
LEFT JOIN downstream_api_keys k ON k.user_id = u.id
|
||||
GROUP BY u.id;
|
||||
|
||||
INSERT OR IGNORE INTO api_credit_grants (
|
||||
public_id, user_id, source_type, source_id,
|
||||
total_micros, remaining_micros, idempotency_key
|
||||
)
|
||||
SELECT
|
||||
'grant_legacy_' || user_id,
|
||||
user_id,
|
||||
'legacy_opening_balance',
|
||||
CAST(user_id AS TEXT),
|
||||
available_micros,
|
||||
available_micros,
|
||||
'migration:20260710000003:user:' || user_id
|
||||
FROM api_billing_accounts
|
||||
WHERE available_micros > 0;
|
||||
|
||||
INSERT OR IGNORE INTO api_entitlement_ledger (
|
||||
user_id, entry_type, amount_micros, balance_after_micros,
|
||||
ref_type, ref_id, operator, idempotency_key
|
||||
)
|
||||
SELECT
|
||||
user_id,
|
||||
'opening_balance',
|
||||
available_micros,
|
||||
available_micros,
|
||||
'migration',
|
||||
'20260710000003',
|
||||
'system',
|
||||
'migration:20260710000003:ledger:user:' || user_id
|
||||
FROM api_billing_accounts
|
||||
WHERE available_micros > 0;
|
||||
@@ -0,0 +1,365 @@
|
||||
-- Commerce catalog and immutable purchase snapshots. Money is always stored in
|
||||
-- integer minor units; API credit is always stored in integer micros. Workflow
|
||||
-- points deliberately have no representation in this schema.
|
||||
CREATE TABLE IF NOT EXISTS commerce_products (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
public_id TEXT NOT NULL,
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
product_type TEXT NOT NULL,
|
||||
api_credit_micros INTEGER NOT NULL,
|
||||
subscription_days INTEGER,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
CHECK (product_type IN ('credit_pack', 'subscription')),
|
||||
CHECK (api_credit_micros > 0),
|
||||
CHECK (
|
||||
(product_type = 'credit_pack' AND subscription_days IS NULL) OR
|
||||
(product_type = 'subscription' AND subscription_days > 0)
|
||||
),
|
||||
CHECK (enabled IN (0, 1))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS commerce_products_public_id_unique
|
||||
ON commerce_products (public_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS commerce_products_code_unique
|
||||
ON commerce_products (code);
|
||||
CREATE INDEX IF NOT EXISTS commerce_products_enabled_sort_idx
|
||||
ON commerce_products (enabled, sort_order, id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS commerce_product_prices (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
amount_minor INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (product_id) REFERENCES commerce_products(id) ON DELETE CASCADE,
|
||||
UNIQUE (product_id, currency),
|
||||
CHECK (currency IN ('CNY', 'USD')),
|
||||
CHECK (amount_minor > 0)
|
||||
);
|
||||
|
||||
-- Credit scopes decide which models may consume the API credit grant. An empty
|
||||
-- set means all models. Prefixes are converted to billing's `model*` pattern at
|
||||
-- fulfillment time.
|
||||
CREATE TABLE IF NOT EXISTS commerce_product_credit_scopes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
scope_type TEXT NOT NULL,
|
||||
scope_value TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (product_id) REFERENCES commerce_products(id) ON DELETE CASCADE,
|
||||
UNIQUE (product_id, scope_type, scope_value),
|
||||
CHECK (scope_type IN ('model', 'model_prefix')),
|
||||
CHECK (length(trim(scope_value)) > 0)
|
||||
);
|
||||
|
||||
-- Unlock scopes are membership entitlements and never flow into an API credit
|
||||
-- grant. They are intentionally separate from credit scopes.
|
||||
CREATE TABLE IF NOT EXISTS commerce_product_unlock_scopes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
scope_type TEXT NOT NULL,
|
||||
scope_value TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (product_id) REFERENCES commerce_products(id) ON DELETE CASCADE,
|
||||
UNIQUE (product_id, scope_type, scope_value),
|
||||
CHECK (scope_type IN ('model', 'model_prefix')),
|
||||
CHECK (length(trim(scope_value)) > 0)
|
||||
);
|
||||
|
||||
-- A model gate is a durable policy fact, not a property of the currently
|
||||
-- visible catalog. Once an unlock scope has been advertised by a product or
|
||||
-- sealed into an order snapshot, archiving/editing that product must not make
|
||||
-- the protected model public. Deliberate ungating requires an explicit future
|
||||
-- policy migration instead of happening as a side effect of catalog CRUD.
|
||||
CREATE TABLE IF NOT EXISTS commerce_model_gate_registry (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
scope_type TEXT NOT NULL,
|
||||
scope_value TEXT NOT NULL,
|
||||
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (scope_type, scope_value),
|
||||
CHECK (scope_type IN ('model', 'model_prefix')),
|
||||
CHECK (length(trim(scope_value)) > 0)
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO commerce_model_gate_registry (scope_type, scope_value)
|
||||
SELECT scope_type, scope_value FROM commerce_product_unlock_scopes;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_product_unlock_scope_register_gate
|
||||
AFTER INSERT ON commerce_product_unlock_scopes
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
INSERT OR IGNORE INTO commerce_model_gate_registry (scope_type, scope_value)
|
||||
VALUES (NEW.scope_type, NEW.scope_value);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_product_unlock_scope_register_gate_update
|
||||
AFTER UPDATE OF scope_type, scope_value ON commerce_product_unlock_scopes
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
INSERT OR IGNORE INTO commerce_model_gate_registry (scope_type, scope_value)
|
||||
VALUES (NEW.scope_type, NEW.scope_value);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_model_gate_registry_no_update
|
||||
BEFORE UPDATE ON commerce_model_gate_registry
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'commerce model gate registry is append-only');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_model_gate_registry_no_delete
|
||||
BEFORE DELETE ON commerce_model_gate_registry
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'commerce model gate registry is append-only');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_unlock_scope_subscription_only_insert
|
||||
BEFORE INSERT ON commerce_product_unlock_scopes
|
||||
FOR EACH ROW
|
||||
WHEN (SELECT product_type FROM commerce_products WHERE id = NEW.product_id) <> 'subscription'
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'unlock scopes require a subscription product');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_unlock_scope_subscription_only_update
|
||||
BEFORE UPDATE ON commerce_product_unlock_scopes
|
||||
FOR EACH ROW
|
||||
WHEN (SELECT product_type FROM commerce_products WHERE id = NEW.product_id) <> 'subscription'
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'unlock scopes require a subscription product');
|
||||
END;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS commerce_orders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
public_id TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
payment_order_id INTEGER NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
product_public_id_snapshot TEXT NOT NULL,
|
||||
product_code_snapshot TEXT NOT NULL,
|
||||
product_name_snapshot TEXT NOT NULL,
|
||||
product_type_snapshot TEXT NOT NULL,
|
||||
order_kind TEXT NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
amount_minor INTEGER NOT NULL,
|
||||
api_credit_micros_snapshot INTEGER NOT NULL,
|
||||
subscription_days_snapshot INTEGER,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
idempotency_key TEXT NOT NULL,
|
||||
snapshot_sealed INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
expires_at TEXT NOT NULL,
|
||||
paid_at TEXT,
|
||||
fulfilled_at TEXT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (payment_order_id) REFERENCES payment_orders(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (product_id) REFERENCES commerce_products(id) ON DELETE RESTRICT,
|
||||
UNIQUE (payment_order_id),
|
||||
UNIQUE (user_id, idempotency_key),
|
||||
CHECK (product_type_snapshot IN ('credit_pack', 'subscription')),
|
||||
CHECK (order_kind IN ('credit_pack', 'subscription')),
|
||||
CHECK (order_kind = product_type_snapshot),
|
||||
CHECK (currency IN ('CNY', 'USD')),
|
||||
CHECK (amount_minor > 0),
|
||||
CHECK (api_credit_micros_snapshot > 0),
|
||||
CHECK (
|
||||
datetime(expires_at) IS NOT NULL
|
||||
AND datetime(expires_at) > datetime(created_at)
|
||||
),
|
||||
CHECK (
|
||||
(product_type_snapshot = 'credit_pack' AND subscription_days_snapshot IS NULL) OR
|
||||
(product_type_snapshot = 'subscription' AND subscription_days_snapshot > 0)
|
||||
),
|
||||
CHECK (status IN ('pending', 'paid', 'fulfilled', 'partially_refunded', 'cancelled', 'expired', 'refunded')),
|
||||
CHECK (snapshot_sealed IN (0, 1))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS commerce_orders_public_id_unique
|
||||
ON commerce_orders (public_id);
|
||||
CREATE INDEX IF NOT EXISTS commerce_orders_user_created_idx
|
||||
ON commerce_orders (user_id, created_at DESC, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS commerce_orders_status_created_idx
|
||||
ON commerce_orders (status, created_at, id);
|
||||
CREATE INDEX IF NOT EXISTS commerce_orders_pending_expiry_idx
|
||||
ON commerce_orders (expires_at, id) WHERE status = 'pending';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS commerce_order_credit_scopes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
order_id INTEGER NOT NULL,
|
||||
scope_type TEXT NOT NULL,
|
||||
scope_value TEXT NOT NULL,
|
||||
FOREIGN KEY (order_id) REFERENCES commerce_orders(id) ON DELETE CASCADE,
|
||||
UNIQUE (order_id, scope_type, scope_value),
|
||||
CHECK (scope_type IN ('model', 'model_prefix')),
|
||||
CHECK (length(trim(scope_value)) > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS commerce_order_unlock_scopes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
order_id INTEGER NOT NULL,
|
||||
scope_type TEXT NOT NULL,
|
||||
scope_value TEXT NOT NULL,
|
||||
FOREIGN KEY (order_id) REFERENCES commerce_orders(id) ON DELETE CASCADE,
|
||||
UNIQUE (order_id, scope_type, scope_value),
|
||||
CHECK (scope_type IN ('model', 'model_prefix')),
|
||||
CHECK (length(trim(scope_value)) > 0)
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO commerce_model_gate_registry (scope_type, scope_value)
|
||||
SELECT scope_type, scope_value FROM commerce_order_unlock_scopes;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_order_unlock_scope_register_gate
|
||||
AFTER INSERT ON commerce_order_unlock_scopes
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
INSERT OR IGNORE INTO commerce_model_gate_registry (scope_type, scope_value)
|
||||
VALUES (NEW.scope_type, NEW.scope_value);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_order_unlock_scope_register_gate_update
|
||||
AFTER UPDATE OF scope_type, scope_value ON commerce_order_unlock_scopes
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
INSERT OR IGNORE INTO commerce_model_gate_registry (scope_type, scope_value)
|
||||
VALUES (NEW.scope_type, NEW.scope_value);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_order_snapshot_fields_immutable
|
||||
BEFORE UPDATE OF
|
||||
user_id, payment_order_id, product_id, product_public_id_snapshot,
|
||||
product_code_snapshot, product_name_snapshot, product_type_snapshot,
|
||||
order_kind, currency, amount_minor, api_credit_micros_snapshot,
|
||||
subscription_days_snapshot, idempotency_key, expires_at
|
||||
ON commerce_orders
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'commerce order snapshot is immutable');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_product_unlock_scope_type_guard
|
||||
BEFORE UPDATE OF product_type ON commerce_products
|
||||
FOR EACH ROW
|
||||
WHEN NEW.product_type <> 'subscription'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM commerce_product_unlock_scopes WHERE product_id = OLD.id
|
||||
)
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'unlock scopes require a subscription product');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_order_snapshot_cannot_unseal
|
||||
BEFORE UPDATE OF snapshot_sealed ON commerce_orders
|
||||
FOR EACH ROW
|
||||
WHEN OLD.snapshot_sealed = 1 AND NEW.snapshot_sealed <> 1
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'commerce order snapshot cannot be unsealed');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_order_credit_scope_sealed_insert
|
||||
BEFORE INSERT ON commerce_order_credit_scopes
|
||||
FOR EACH ROW
|
||||
WHEN (SELECT snapshot_sealed FROM commerce_orders WHERE id = NEW.order_id) = 1
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'commerce order credit scope snapshot is sealed');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_order_credit_scope_sealed_update
|
||||
BEFORE UPDATE ON commerce_order_credit_scopes
|
||||
FOR EACH ROW
|
||||
WHEN (SELECT snapshot_sealed FROM commerce_orders WHERE id = OLD.order_id) = 1
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'commerce order credit scope snapshot is sealed');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_order_credit_scope_sealed_delete
|
||||
BEFORE DELETE ON commerce_order_credit_scopes
|
||||
FOR EACH ROW
|
||||
WHEN (SELECT snapshot_sealed FROM commerce_orders WHERE id = OLD.order_id) = 1
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'commerce order credit scope snapshot is sealed');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_order_unlock_scope_sealed_insert
|
||||
BEFORE INSERT ON commerce_order_unlock_scopes
|
||||
FOR EACH ROW
|
||||
WHEN (SELECT snapshot_sealed FROM commerce_orders WHERE id = NEW.order_id) = 1
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'commerce order unlock scope snapshot is sealed');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_order_unlock_scope_sealed_update
|
||||
BEFORE UPDATE ON commerce_order_unlock_scopes
|
||||
FOR EACH ROW
|
||||
WHEN (SELECT snapshot_sealed FROM commerce_orders WHERE id = OLD.order_id) = 1
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'commerce order unlock scope snapshot is sealed');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_order_unlock_scope_sealed_delete
|
||||
BEFORE DELETE ON commerce_order_unlock_scopes
|
||||
FOR EACH ROW
|
||||
WHEN (SELECT snapshot_sealed FROM commerce_orders WHERE id = OLD.order_id) = 1
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'commerce order unlock scope snapshot is sealed');
|
||||
END;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS commerce_subscription_periods (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
public_id TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
source_order_id INTEGER NOT NULL,
|
||||
starts_at TEXT NOT NULL,
|
||||
ends_at TEXT NOT NULL,
|
||||
access_ends_at TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (product_id) REFERENCES commerce_products(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (source_order_id) REFERENCES commerce_orders(id) ON DELETE RESTRICT,
|
||||
UNIQUE (source_order_id),
|
||||
CHECK (datetime(ends_at) > datetime(starts_at)),
|
||||
CHECK (
|
||||
access_ends_at IS NULL OR (
|
||||
datetime(access_ends_at) >= datetime(starts_at)
|
||||
AND datetime(access_ends_at) <= datetime(ends_at)
|
||||
)
|
||||
),
|
||||
CHECK (status IN ('active', 'revoked'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS commerce_subscription_periods_public_id_unique
|
||||
ON commerce_subscription_periods (public_id);
|
||||
CREATE INDEX IF NOT EXISTS commerce_subscription_periods_user_product_end_idx
|
||||
ON commerce_subscription_periods (user_id, product_id, ends_at DESC, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS commerce_subscription_periods_active_window_idx
|
||||
ON commerce_subscription_periods (user_id, status, starts_at, ends_at);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS commerce_subscription_period_window_immutable
|
||||
BEFORE UPDATE OF user_id, product_id, source_order_id, starts_at, ends_at
|
||||
ON commerce_subscription_periods
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'subscription period window is immutable');
|
||||
END;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS commerce_order_fulfillments (
|
||||
order_id INTEGER PRIMARY KEY NOT NULL,
|
||||
idempotency_key TEXT NOT NULL,
|
||||
api_credit_grant_id INTEGER,
|
||||
subscription_period_id INTEGER,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (order_id) REFERENCES commerce_orders(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (api_credit_grant_id) REFERENCES api_credit_grants(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (subscription_period_id) REFERENCES commerce_subscription_periods(id) ON DELETE RESTRICT,
|
||||
UNIQUE (idempotency_key)
|
||||
);
|
||||
@@ -0,0 +1,409 @@
|
||||
-- Multi-provider payment routing. Credentials are encrypted by the application
|
||||
-- with an independent master key; only the versioned ciphertext is persisted.
|
||||
CREATE TABLE IF NOT EXISTS payment_provider_instances (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
public_id TEXT NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
credentials_ciphertext BLOB NOT NULL,
|
||||
credentials_version INTEGER NOT NULL DEFAULT 1,
|
||||
callback_token_hash TEXT NOT NULL,
|
||||
callback_token_ciphertext BLOB NOT NULL,
|
||||
config_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
CHECK (provider IN ('easypay', 'alipay', 'wechatpay', 'stripe', 'comfy_cloud')),
|
||||
CHECK (enabled IN (0, 1)),
|
||||
CHECK (credentials_version > 0),
|
||||
CHECK (length(credentials_ciphertext) > 29),
|
||||
CHECK (length(callback_token_hash) = 64),
|
||||
CHECK (length(callback_token_ciphertext) > 29),
|
||||
CHECK (json_valid(config_json))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS payment_provider_instances_public_id_unique
|
||||
ON payment_provider_instances (public_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS payment_provider_instances_callback_token_unique
|
||||
ON payment_provider_instances (callback_token_hash)
|
||||
WHERE callback_token_hash IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS payment_provider_instances_provider_enabled_idx
|
||||
ON payment_provider_instances (provider, enabled, id);
|
||||
|
||||
-- These values are part of the AEAD associated data. Changing either would
|
||||
-- make the stored credential envelope undecryptable.
|
||||
CREATE TRIGGER IF NOT EXISTS payment_provider_instance_identity_immutable
|
||||
BEFORE UPDATE OF public_id, provider ON payment_provider_instances
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'payment provider instance identity is immutable');
|
||||
END;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_method_routes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
public_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
payment_method TEXT NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
selection_strategy TEXT NOT NULL DEFAULT 'round_robin',
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
CHECK (payment_method IN ('alipay', 'wechatpay', 'card', 'comfy_cloud')),
|
||||
CHECK (currency IN ('CNY', 'USD')),
|
||||
CHECK (selection_strategy IN ('round_robin', 'least_amount')),
|
||||
CHECK (enabled IN (0, 1))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS payment_method_routes_public_id_unique
|
||||
ON payment_method_routes (public_id);
|
||||
CREATE INDEX IF NOT EXISTS payment_method_routes_method_enabled_idx
|
||||
ON payment_method_routes (payment_method, currency, enabled, id);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS payment_method_route_identity_immutable
|
||||
BEFORE UPDATE OF public_id, payment_method, currency ON payment_method_routes
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'payment method route identity is immutable');
|
||||
END;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_method_route_members (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
route_id INTEGER NOT NULL,
|
||||
provider_instance_id INTEGER NOT NULL,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
daily_capacity_minor INTEGER,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (route_id) REFERENCES payment_method_routes(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (provider_instance_id) REFERENCES payment_provider_instances(id) ON DELETE RESTRICT,
|
||||
UNIQUE (route_id, provider_instance_id),
|
||||
CHECK (daily_capacity_minor IS NULL OR daily_capacity_minor > 0),
|
||||
CHECK (enabled IN (0, 1))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS payment_method_route_members_selection_idx
|
||||
ON payment_method_route_members (route_id, enabled, position, id);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS payment_route_member_provider_guard_insert
|
||||
BEFORE INSERT ON payment_method_route_members
|
||||
FOR EACH ROW
|
||||
WHEN NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM payment_method_routes r
|
||||
JOIN payment_provider_instances i ON i.id = NEW.provider_instance_id
|
||||
WHERE r.id = NEW.route_id
|
||||
AND (
|
||||
(r.payment_method = 'alipay' AND i.provider IN ('easypay', 'alipay')) OR
|
||||
(r.payment_method = 'wechatpay' AND i.provider IN ('easypay', 'wechatpay')) OR
|
||||
(r.payment_method = 'card' AND i.provider = 'stripe') OR
|
||||
(r.payment_method = 'comfy_cloud' AND i.provider = 'comfy_cloud')
|
||||
)
|
||||
)
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'provider instance does not support route payment method');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS payment_route_member_provider_guard_update
|
||||
BEFORE UPDATE OF route_id, provider_instance_id ON payment_method_route_members
|
||||
FOR EACH ROW
|
||||
WHEN NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM payment_method_routes r
|
||||
JOIN payment_provider_instances i ON i.id = NEW.provider_instance_id
|
||||
WHERE r.id = NEW.route_id
|
||||
AND (
|
||||
(r.payment_method = 'alipay' AND i.provider IN ('easypay', 'alipay')) OR
|
||||
(r.payment_method = 'wechatpay' AND i.provider IN ('easypay', 'wechatpay')) OR
|
||||
(r.payment_method = 'card' AND i.provider = 'stripe') OR
|
||||
(r.payment_method = 'comfy_cloud' AND i.provider = 'comfy_cloud')
|
||||
)
|
||||
)
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'provider instance does not support route payment method');
|
||||
END;
|
||||
|
||||
-- One lock/cursor row per route. `version` is bumped before selection to turn
|
||||
-- SQLite's deferred transaction into a serialized route allocation.
|
||||
CREATE TABLE IF NOT EXISTS payment_method_route_cursors (
|
||||
route_id INTEGER PRIMARY KEY NOT NULL,
|
||||
last_member_id INTEGER,
|
||||
version INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (route_id) REFERENCES payment_method_routes(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (last_member_id) REFERENCES payment_method_route_members(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_route_member_daily_usage (
|
||||
member_id INTEGER NOT NULL,
|
||||
usage_date TEXT NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
reserved_minor INTEGER NOT NULL DEFAULT 0,
|
||||
captured_minor INTEGER NOT NULL DEFAULT 0,
|
||||
refunded_minor INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (member_id, usage_date, currency),
|
||||
FOREIGN KEY (member_id) REFERENCES payment_method_route_members(id) ON DELETE CASCADE,
|
||||
CHECK (currency IN ('CNY', 'USD')),
|
||||
CHECK (reserved_minor >= 0),
|
||||
CHECK (captured_minor >= 0),
|
||||
CHECK (refunded_minor >= 0),
|
||||
CHECK (refunded_minor <= captured_minor)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_order_provider_bindings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
public_id TEXT NOT NULL,
|
||||
payment_order_id INTEGER NOT NULL,
|
||||
route_id INTEGER NOT NULL,
|
||||
member_id INTEGER NOT NULL,
|
||||
provider_instance_id INTEGER NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
payment_method TEXT NOT NULL,
|
||||
amount_minor INTEGER NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'bound',
|
||||
capacity_state TEXT NOT NULL DEFAULT 'reserved',
|
||||
provider_session_id TEXT,
|
||||
provider_payment_id TEXT,
|
||||
bound_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (payment_order_id) REFERENCES payment_orders(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (route_id) REFERENCES payment_method_routes(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (member_id) REFERENCES payment_method_route_members(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (provider_instance_id) REFERENCES payment_provider_instances(id) ON DELETE RESTRICT,
|
||||
UNIQUE (payment_order_id),
|
||||
CHECK (provider IN ('easypay', 'alipay', 'wechatpay', 'stripe', 'comfy_cloud')),
|
||||
CHECK (payment_method IN ('alipay', 'wechatpay', 'card', 'comfy_cloud')),
|
||||
CHECK (amount_minor > 0),
|
||||
CHECK (currency IN ('CNY', 'USD')),
|
||||
CHECK (status IN ('bound', 'processing', 'paid', 'failed', 'cancelled', 'refunded')),
|
||||
CHECK (capacity_state IN ('reserved', 'released', 'captured', 'refunded'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS payment_order_provider_bindings_public_id_unique
|
||||
ON payment_order_provider_bindings (public_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS payment_order_provider_session_unique
|
||||
ON payment_order_provider_bindings (provider_instance_id, provider_session_id)
|
||||
WHERE provider_session_id IS NOT NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS payment_order_provider_payment_unique
|
||||
ON payment_order_provider_bindings (provider_instance_id, provider_payment_id)
|
||||
WHERE provider_payment_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS payment_order_provider_bindings_instance_status_idx
|
||||
ON payment_order_provider_bindings (provider_instance_id, status, id);
|
||||
CREATE INDEX IF NOT EXISTS payment_order_provider_bindings_capacity_status_idx
|
||||
ON payment_order_provider_bindings (capacity_state, status, payment_order_id, id);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS payment_order_provider_binding_identity_immutable
|
||||
BEFORE UPDATE OF
|
||||
payment_order_id, route_id, member_id, provider_instance_id, provider,
|
||||
payment_method, amount_minor, currency
|
||||
ON payment_order_provider_bindings
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'payment order provider binding is immutable');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS payment_order_provider_payment_id_immutable
|
||||
BEFORE UPDATE OF provider_payment_id
|
||||
ON payment_order_provider_bindings
|
||||
FOR EACH ROW
|
||||
WHEN OLD.provider_payment_id IS NOT NULL
|
||||
AND NEW.provider_payment_id IS NOT OLD.provider_payment_id
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'provider payment ID is immutable after first assignment');
|
||||
END;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_provider_event_inbox (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
public_id TEXT NOT NULL,
|
||||
provider_instance_id INTEGER NOT NULL,
|
||||
provider_event_id TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
payload_json TEXT NOT NULL,
|
||||
payload_sha256 TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'received',
|
||||
attempt_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_error TEXT,
|
||||
received_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
processed_at TEXT,
|
||||
FOREIGN KEY (provider_instance_id) REFERENCES payment_provider_instances(id) ON DELETE RESTRICT,
|
||||
UNIQUE (provider_instance_id, provider_event_id),
|
||||
CHECK (length(trim(provider_event_id)) > 0),
|
||||
CHECK (length(trim(event_type)) > 0),
|
||||
CHECK (json_valid(payload_json)),
|
||||
CHECK (length(payload_sha256) = 64),
|
||||
CHECK (status IN ('received', 'processing', 'processed', 'failed')),
|
||||
CHECK (attempt_count >= 0)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS payment_provider_event_inbox_public_id_unique
|
||||
ON payment_provider_event_inbox (public_id);
|
||||
CREATE INDEX IF NOT EXISTS payment_provider_event_inbox_work_idx
|
||||
ON payment_provider_event_inbox (status, received_at, id);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS payment_provider_event_content_immutable
|
||||
BEFORE UPDATE OF
|
||||
public_id, provider_instance_id, provider_event_id, event_type,
|
||||
payload_json, payload_sha256, received_at
|
||||
ON payment_provider_event_inbox
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'payment provider event content is immutable');
|
||||
END;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
public_id TEXT NOT NULL,
|
||||
job_type TEXT NOT NULL,
|
||||
payment_order_id INTEGER,
|
||||
binding_id INTEGER,
|
||||
provider_instance_id INTEGER NOT NULL,
|
||||
event_id INTEGER,
|
||||
payload_json TEXT NOT NULL DEFAULT '{}',
|
||||
result_json TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
attempt_count INTEGER NOT NULL DEFAULT 0,
|
||||
max_attempts INTEGER NOT NULL DEFAULT 8,
|
||||
available_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
lease_token TEXT,
|
||||
leased_until TEXT,
|
||||
last_error TEXT,
|
||||
idempotency_key TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (payment_order_id) REFERENCES payment_orders(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (binding_id) REFERENCES payment_order_provider_bindings(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (provider_instance_id) REFERENCES payment_provider_instances(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (event_id) REFERENCES payment_provider_event_inbox(id) ON DELETE RESTRICT,
|
||||
CHECK (job_type IN ('create_checkout', 'query_payment', 'capture', 'process_event', 'refund')),
|
||||
CHECK (json_valid(payload_json)),
|
||||
CHECK (result_json IS NULL OR json_valid(result_json)),
|
||||
CHECK (status IN ('pending', 'running', 'succeeded', 'failed', 'dead')),
|
||||
CHECK (attempt_count >= 0),
|
||||
CHECK (max_attempts > 0)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS payment_jobs_public_id_unique
|
||||
ON payment_jobs (public_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS payment_jobs_idempotency_unique
|
||||
ON payment_jobs (idempotency_key);
|
||||
CREATE INDEX IF NOT EXISTS payment_jobs_work_idx
|
||||
ON payment_jobs (status, available_at, id);
|
||||
CREATE INDEX IF NOT EXISTS payment_jobs_lease_idx
|
||||
ON payment_jobs (status, leased_until, id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_refund_requests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
public_id TEXT NOT NULL,
|
||||
user_id INTEGER,
|
||||
payment_order_id INTEGER NOT NULL,
|
||||
binding_id INTEGER NOT NULL,
|
||||
amount_minor INTEGER NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
reason TEXT,
|
||||
requested_by TEXT NOT NULL,
|
||||
force_reversal INTEGER NOT NULL DEFAULT 0,
|
||||
entitlement_target_micros INTEGER NOT NULL DEFAULT 0,
|
||||
entitlement_shortfall_micros INTEGER NOT NULL DEFAULT 0,
|
||||
subscription_target_seconds INTEGER NOT NULL DEFAULT 0,
|
||||
subscription_shortfall_seconds INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
idempotency_key TEXT NOT NULL,
|
||||
reviewed_by TEXT,
|
||||
review_note TEXT,
|
||||
reviewed_at TEXT,
|
||||
provider_attempt_token TEXT,
|
||||
provider_attempt_started_at TEXT,
|
||||
provider_attempt_expires_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (payment_order_id) REFERENCES payment_orders(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (binding_id) REFERENCES payment_order_provider_bindings(id) ON DELETE RESTRICT,
|
||||
CHECK (amount_minor > 0),
|
||||
CHECK (currency IN ('CNY', 'USD')),
|
||||
CHECK (force_reversal IN (0, 1)),
|
||||
CHECK (entitlement_target_micros >= 0),
|
||||
CHECK (entitlement_shortfall_micros >= 0),
|
||||
CHECK (subscription_target_seconds >= 0),
|
||||
CHECK (subscription_shortfall_seconds >= 0),
|
||||
CHECK (status IN ('pending', 'approved', 'rejected', 'processing', 'completed', 'failed'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS payment_refund_requests_public_id_unique
|
||||
ON payment_refund_requests (public_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS payment_refund_requests_idempotency_unique
|
||||
ON payment_refund_requests (requested_by, idempotency_key);
|
||||
CREATE INDEX IF NOT EXISTS payment_refund_requests_status_created_idx
|
||||
ON payment_refund_requests (status, created_at, id);
|
||||
CREATE INDEX IF NOT EXISTS payment_refund_requests_user_created_idx
|
||||
ON payment_refund_requests (user_id, created_at DESC, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS payment_refund_requests_attempt_idx
|
||||
ON payment_refund_requests (status, provider_attempt_expires_at)
|
||||
WHERE provider_attempt_token IS NOT NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS payment_refund_requests_one_active_per_order
|
||||
ON payment_refund_requests (payment_order_id)
|
||||
WHERE status IN ('pending', 'approved', 'processing');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_refund_entitlement_reservations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
refund_request_id INTEGER NOT NULL,
|
||||
grant_id INTEGER NOT NULL,
|
||||
reserved_micros INTEGER NOT NULL,
|
||||
grant_status_before TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'reserved',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (refund_request_id) REFERENCES payment_refund_requests(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (grant_id) REFERENCES api_credit_grants(id) ON DELETE RESTRICT,
|
||||
UNIQUE (refund_request_id, grant_id),
|
||||
CHECK (reserved_micros > 0),
|
||||
CHECK (status IN ('reserved', 'finalized', 'released'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_refund_subscription_reservations (
|
||||
refund_request_id INTEGER PRIMARY KEY NOT NULL,
|
||||
subscription_period_id INTEGER NOT NULL,
|
||||
previous_access_ends_at TEXT NOT NULL,
|
||||
previous_status TEXT NOT NULL,
|
||||
reserved_access_ends_at TEXT NOT NULL,
|
||||
reversal_seconds INTEGER NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'reserved',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (refund_request_id) REFERENCES payment_refund_requests(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (subscription_period_id) REFERENCES commerce_subscription_periods(id) ON DELETE RESTRICT,
|
||||
CHECK (reversal_seconds >= 0),
|
||||
CHECK (previous_status IN ('active', 'revoked')),
|
||||
CHECK (status IN ('reserved', 'finalized', 'released'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_refunds (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
public_id TEXT NOT NULL,
|
||||
refund_request_id INTEGER NOT NULL,
|
||||
provider_instance_id INTEGER NOT NULL,
|
||||
provider_refund_id TEXT,
|
||||
amount_minor INTEGER NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'processing',
|
||||
response_json TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
completed_at TEXT,
|
||||
FOREIGN KEY (refund_request_id) REFERENCES payment_refund_requests(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (provider_instance_id) REFERENCES payment_provider_instances(id) ON DELETE RESTRICT,
|
||||
UNIQUE (refund_request_id),
|
||||
CHECK (amount_minor > 0),
|
||||
CHECK (currency IN ('CNY', 'USD')),
|
||||
CHECK (status IN ('processing', 'succeeded', 'failed')),
|
||||
CHECK (response_json IS NULL OR json_valid(response_json))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS payment_refunds_public_id_unique
|
||||
ON payment_refunds (public_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS payment_refunds_provider_reference_unique
|
||||
ON payment_refunds (provider_instance_id, provider_refund_id)
|
||||
WHERE provider_refund_id IS NOT NULL;
|
||||
@@ -0,0 +1,23 @@
|
||||
-- End-user social-login identities. Provider tokens are intentionally not
|
||||
-- persisted; this table only links a stable provider subject to a local user.
|
||||
CREATE TABLE IF NOT EXISTS user_oauth_identities (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
provider_subject TEXT NOT NULL,
|
||||
email_snapshot TEXT,
|
||||
display_name TEXT,
|
||||
avatar_url TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
last_login_at TEXT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CHECK (provider IN ('google', 'github', 'wechat')),
|
||||
CHECK (length(provider_subject) > 0)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS user_oauth_identities_provider_subject_unique
|
||||
ON user_oauth_identities (provider, provider_subject);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS user_oauth_identities_user_id_idx
|
||||
ON user_oauth_identities (user_id);
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Durable pre-authorization leases bound ordinary API spend to one in-flight
|
||||
-- request per user. These are deliberately separate from credit/refund
|
||||
-- reservations: no balance is moved into reserved_micros for a request lease.
|
||||
CREATE TABLE IF NOT EXISTS api_request_leases (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
public_id TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
downstream_key_id INTEGER,
|
||||
model TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
acquired_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
expires_at TEXT NOT NULL,
|
||||
released_at TEXT,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (downstream_key_id) REFERENCES downstream_api_keys(id) ON DELETE SET NULL,
|
||||
CHECK (status IN ('active', 'released', 'expired'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS api_request_leases_public_id_unique
|
||||
ON api_request_leases (public_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS api_request_leases_one_active_per_user
|
||||
ON api_request_leases (user_id) WHERE status = 'active';
|
||||
CREATE INDEX IF NOT EXISTS api_request_leases_expiry_idx
|
||||
ON api_request_leases (status, expires_at);
|
||||
CREATE INDEX IF NOT EXISTS api_request_leases_user_created_idx
|
||||
ON api_request_leases (user_id, acquired_at DESC, id DESC);
|
||||
@@ -0,0 +1,72 @@
|
||||
-- Durable Agent Workbench run lifecycle and per-session workspace identity.
|
||||
--
|
||||
-- Active statuses deliberately include `cancelling`: requesting cancellation
|
||||
-- does not release the session lease until the worker has actually stopped.
|
||||
|
||||
ALTER TABLE agent_sessions ADD COLUMN workspace_id TEXT;
|
||||
ALTER TABLE agent_sessions ADD COLUMN revision INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE agent_sessions ADD COLUMN runtime_status TEXT NOT NULL DEFAULT 'idle';
|
||||
ALTER TABLE agent_sessions ADD COLUMN context_version INTEGER NOT NULL DEFAULT 1;
|
||||
|
||||
UPDATE agent_sessions
|
||||
SET workspace_id = lower(hex(randomblob(4))) || '-' ||
|
||||
lower(hex(randomblob(2))) || '-' ||
|
||||
'4' || substr(lower(hex(randomblob(2))), 2) || '-' ||
|
||||
substr('89ab', abs(random()) % 4 + 1, 1) ||
|
||||
substr(lower(hex(randomblob(2))), 2) || '-' ||
|
||||
lower(hex(randomblob(6)))
|
||||
WHERE workspace_id IS NULL OR trim(workspace_id) = '';
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS agent_sessions_workspace_id_unique
|
||||
ON agent_sessions(workspace_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_runs (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
user_id INTEGER,
|
||||
actor_key TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'starting',
|
||||
model TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL DEFAULT 0,
|
||||
error_message TEXT,
|
||||
cancel_requested_at TEXT,
|
||||
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
finished_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY(session_id) REFERENCES agent_sessions(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
CHECK(status IN (
|
||||
'starting', 'running', 'waiting_approval', 'waiting_input', 'cancelling',
|
||||
'succeeded', 'failed', 'cancelled', 'interrupted'
|
||||
))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS agent_runs_one_active_per_session
|
||||
ON agent_runs(session_id)
|
||||
WHERE status IN ('starting', 'running', 'waiting_approval', 'waiting_input', 'cancelling');
|
||||
|
||||
CREATE INDEX IF NOT EXISTS agent_runs_actor_status_created
|
||||
ON agent_runs(actor_key, status, created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS agent_runs_session_created
|
||||
ON agent_runs(session_id, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_tool_audits (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
actor_key TEXT NOT NULL,
|
||||
tool_name TEXT NOT NULL,
|
||||
risk TEXT NOT NULL,
|
||||
decision TEXT NOT NULL,
|
||||
input_summary TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY(run_id) REFERENCES agent_runs(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(session_id) REFERENCES agent_sessions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS agent_tool_audits_run_created
|
||||
ON agent_tool_audits(run_id, created_at DESC);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Link proxy usage records to durable Agent Workbench runs.
|
||||
|
||||
ALTER TABLE proxy_logs ADD COLUMN agent_run_id TEXT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS proxy_logs_agent_run_id_idx
|
||||
ON proxy_logs(agent_run_id)
|
||||
WHERE agent_run_id IS NOT NULL;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
-- Ownership index for media stored under the shared Comfy Cloud API key.
|
||||
-- Cloud remains the blob store; this table is the local authorization source.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS comfy_cloud_jobs (
|
||||
job_id TEXT PRIMARY KEY NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
last_synced_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS comfy_cloud_jobs_user_updated_idx
|
||||
ON comfy_cloud_jobs(user_id, updated_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS comfy_cloud_assets (
|
||||
asset_id TEXT PRIMARY KEY NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
job_id TEXT,
|
||||
asset_hash TEXT,
|
||||
name TEXT,
|
||||
tags_json TEXT NOT NULL DEFAULT '[]',
|
||||
snapshot_json TEXT,
|
||||
is_preview INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS comfy_cloud_assets_user_created_idx
|
||||
ON comfy_cloud_assets(user_id, created_at DESC, asset_id DESC);
|
||||
CREATE INDEX IF NOT EXISTS comfy_cloud_assets_user_job_idx
|
||||
ON comfy_cloud_assets(user_id, job_id);
|
||||
CREATE INDEX IF NOT EXISTS comfy_cloud_assets_user_name_idx
|
||||
ON comfy_cloud_assets(user_id, name);
|
||||
CREATE INDEX IF NOT EXISTS comfy_cloud_assets_user_hash_idx
|
||||
ON comfy_cloud_assets(user_id, asset_hash);
|
||||
@@ -0,0 +1 @@
|
||||
-- MySQL/MariaDB constraint parity is implemented in the matching MySQL migration.
|
||||
@@ -0,0 +1,144 @@
|
||||
ALTER TABLE api_credit_grants ADD COLUMN grant_kind TEXT NOT NULL DEFAULT 'balance';
|
||||
|
||||
UPDATE api_credit_grants
|
||||
SET grant_kind = CASE
|
||||
WHEN source_type = 'subscription' THEN 'package'
|
||||
WHEN source_type = 'package' AND payment_order_id IS NULL THEN 'package'
|
||||
ELSE 'balance'
|
||||
END;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS api_credit_grants_kind_spend_idx
|
||||
ON api_credit_grants (user_id, grant_kind, status, expires_at, id);
|
||||
|
||||
CREATE TABLE comfy_billing_accounts (
|
||||
user_id INTEGER PRIMARY KEY NOT NULL,
|
||||
available_micros INTEGER NOT NULL DEFAULT 0,
|
||||
reserved_micros INTEGER NOT NULL DEFAULT 0,
|
||||
debt_micros INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
version INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CHECK (available_micros >= 0), CHECK (reserved_micros >= 0), CHECK (debt_micros >= 0),
|
||||
CHECK (status IN ('active', 'frozen', 'closed'))
|
||||
);
|
||||
|
||||
CREATE TABLE comfy_credit_grants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
public_id TEXT NOT NULL UNIQUE,
|
||||
user_id INTEGER NOT NULL,
|
||||
grant_kind TEXT NOT NULL,
|
||||
source_type TEXT NOT NULL,
|
||||
source_id TEXT,
|
||||
payment_order_id INTEGER,
|
||||
total_micros INTEGER NOT NULL,
|
||||
remaining_micros INTEGER NOT NULL,
|
||||
reserved_micros INTEGER NOT NULL DEFAULT 0,
|
||||
starts_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
expires_at TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
idempotency_key TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (payment_order_id) REFERENCES payment_orders(id) ON DELETE SET NULL,
|
||||
CHECK (grant_kind IN ('package', 'balance')),
|
||||
CHECK (total_micros > 0), CHECK (remaining_micros >= 0), CHECK (reserved_micros >= 0),
|
||||
CHECK (remaining_micros + reserved_micros <= total_micros),
|
||||
CHECK (status IN ('scheduled', 'active', 'refund_pending', 'exhausted', 'expired', 'revoked'))
|
||||
);
|
||||
CREATE INDEX comfy_credit_grants_spend_idx
|
||||
ON comfy_credit_grants (user_id, grant_kind, status, starts_at, expires_at, id);
|
||||
|
||||
CREATE TABLE comfy_charge_holds (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
public_id TEXT NOT NULL UNIQUE,
|
||||
user_id INTEGER NOT NULL,
|
||||
task_type TEXT NOT NULL,
|
||||
ref_id TEXT NOT NULL,
|
||||
amount_micros INTEGER NOT NULL,
|
||||
package_micros INTEGER NOT NULL DEFAULT 0,
|
||||
balance_micros INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
idempotency_key TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
settled_at TEXT,
|
||||
released_at TEXT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CHECK (amount_micros > 0), CHECK (status IN ('active', 'settled', 'released'))
|
||||
);
|
||||
CREATE INDEX comfy_charge_holds_ref_idx ON comfy_charge_holds (task_type, ref_id, status);
|
||||
|
||||
CREATE TABLE comfy_charge_hold_allocations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
hold_id INTEGER NOT NULL,
|
||||
grant_id INTEGER NOT NULL,
|
||||
amount_micros INTEGER NOT NULL,
|
||||
grant_kind TEXT NOT NULL,
|
||||
FOREIGN KEY (hold_id) REFERENCES comfy_charge_holds(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (grant_id) REFERENCES comfy_credit_grants(id) ON DELETE RESTRICT,
|
||||
UNIQUE (hold_id, grant_id), CHECK (amount_micros > 0),
|
||||
CHECK (grant_kind IN ('package', 'balance'))
|
||||
);
|
||||
|
||||
CREATE TABLE comfy_entitlement_ledger (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
entry_type TEXT NOT NULL,
|
||||
amount_micros INTEGER NOT NULL,
|
||||
balance_after_micros INTEGER NOT NULL,
|
||||
package_amount_micros INTEGER NOT NULL DEFAULT 0,
|
||||
balance_amount_micros INTEGER NOT NULL DEFAULT 0,
|
||||
grant_id INTEGER,
|
||||
task_type TEXT,
|
||||
ref_type TEXT,
|
||||
ref_id TEXT,
|
||||
operator TEXT,
|
||||
note TEXT,
|
||||
idempotency_key TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (grant_id) REFERENCES comfy_credit_grants(id) ON DELETE SET NULL
|
||||
);
|
||||
CREATE INDEX comfy_entitlement_ledger_user_idx
|
||||
ON comfy_entitlement_ledger (user_id, created_at DESC, id DESC);
|
||||
|
||||
CREATE TABLE comfy_task_prices (
|
||||
task_type TEXT PRIMARY KEY NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
points_micros INTEGER NOT NULL DEFAULT 0,
|
||||
enabled INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
CHECK (points_micros >= 0), CHECK (enabled IN (0, 1))
|
||||
);
|
||||
|
||||
INSERT INTO comfy_task_prices (task_type, display_name) VALUES
|
||||
('comfy-native-workflow', 'Comfy 工作流'),
|
||||
('money-printer-video', 'Money Printer 视频'),
|
||||
('digital-human-generation', '数字人生成'),
|
||||
('jimeng-image-generation', '即梦图片'),
|
||||
('jimeng-video-generation', '即梦视频'),
|
||||
('seedance-video-generation', 'Seedance 视频');
|
||||
|
||||
INSERT INTO comfy_billing_accounts (user_id, available_micros)
|
||||
SELECT user_id, ROUND(MAX(points_balance, 0) * 1000000.0)
|
||||
FROM user_credits WHERE points_balance > 0;
|
||||
|
||||
INSERT INTO comfy_credit_grants
|
||||
(public_id, user_id, grant_kind, source_type, source_id, total_micros,
|
||||
remaining_micros, idempotency_key)
|
||||
SELECT 'cgrant_legacy_' || user_id, user_id, 'balance', 'legacy_opening_balance',
|
||||
'user_credits', ROUND(MAX(points_balance, 0) * 1000000.0),
|
||||
ROUND(MAX(points_balance, 0) * 1000000.0),
|
||||
'migration:20260711000004:comfy:user:' || user_id
|
||||
FROM user_credits WHERE points_balance > 0;
|
||||
|
||||
INSERT INTO comfy_entitlement_ledger
|
||||
(user_id, entry_type, amount_micros, balance_after_micros, ref_type,
|
||||
ref_id, operator, idempotency_key)
|
||||
SELECT user_id, 'opening_balance', available_micros, available_micros,
|
||||
'migration', '20260711000004', 'system',
|
||||
'migration:20260711000004:comfy:ledger:user:' || user_id
|
||||
FROM comfy_billing_accounts WHERE available_micros > 0;
|
||||
@@ -0,0 +1,15 @@
|
||||
ALTER TABLE commerce_products ADD COLUMN billing_track TEXT NOT NULL DEFAULT 'api';
|
||||
ALTER TABLE commerce_products ADD COLUMN grant_kind TEXT NOT NULL DEFAULT 'package';
|
||||
ALTER TABLE commerce_products ADD COLUMN benefit_type TEXT NOT NULL DEFAULT 'api_credit';
|
||||
ALTER TABLE commerce_products ADD COLUMN validity_days INTEGER;
|
||||
UPDATE commerce_products SET grant_kind = CASE WHEN product_type = 'subscription' THEN 'package' ELSE 'balance' END;
|
||||
|
||||
ALTER TABLE commerce_orders ADD COLUMN billing_track_snapshot TEXT NOT NULL DEFAULT 'api';
|
||||
ALTER TABLE commerce_orders ADD COLUMN grant_kind_snapshot TEXT NOT NULL DEFAULT 'package';
|
||||
ALTER TABLE commerce_orders ADD COLUMN benefit_type_snapshot TEXT NOT NULL DEFAULT 'api_credit';
|
||||
ALTER TABLE commerce_orders ADD COLUMN validity_days_snapshot INTEGER;
|
||||
UPDATE commerce_orders SET grant_kind_snapshot = CASE WHEN product_type_snapshot = 'subscription' THEN 'package' ELSE 'balance' END;
|
||||
|
||||
ALTER TABLE commerce_order_fulfillments ADD COLUMN comfy_credit_grant_id INTEGER;
|
||||
ALTER TABLE payment_orders ADD COLUMN billing_target TEXT NOT NULL DEFAULT 'api';
|
||||
ALTER TABLE user_recharge_records ADD COLUMN billing_target TEXT NOT NULL DEFAULT 'api';
|
||||
@@ -0,0 +1,25 @@
|
||||
ALTER TABLE api_entitlement_ledger ADD COLUMN package_amount_micros INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE api_entitlement_ledger ADD COLUMN balance_amount_micros INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE TABLE comfy_refund_entitlement_reservations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
refund_request_id INTEGER NOT NULL,
|
||||
grant_id INTEGER NOT NULL,
|
||||
reserved_micros INTEGER NOT NULL,
|
||||
grant_status_before TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'reserved',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (refund_request_id) REFERENCES payment_refund_requests(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (grant_id) REFERENCES comfy_credit_grants(id) ON DELETE RESTRICT,
|
||||
UNIQUE (refund_request_id, grant_id),
|
||||
CHECK (reserved_micros > 0),
|
||||
CHECK (status IN ('reserved', 'finalized', 'released'))
|
||||
);
|
||||
|
||||
CREATE TRIGGER commerce_order_billing_snapshot_immutable
|
||||
BEFORE UPDATE OF billing_track_snapshot, grant_kind_snapshot, benefit_type_snapshot, validity_days_snapshot
|
||||
ON commerce_orders FOR EACH ROW
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'commerce order billing snapshot is immutable');
|
||||
END;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE user_profile_avatars (
|
||||
user_id INTEGER PRIMARY KEY NOT NULL,
|
||||
public_id TEXT NOT NULL UNIQUE,
|
||||
image_bytes BLOB NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
size_bytes INTEGER NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CHECK (size_bytes > 0)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user