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:
@@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS usage_daily_snapshots (
|
||||
total_cost DOUBLE PRECISION NOT NULL DEFAULT 0.0,
|
||||
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
UNIQUE(),
|
||||
UNIQUE(local_day, downstream_key_id),
|
||||
FOREIGN KEY (downstream_key_id) REFERENCES downstream_api_keys(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE IF NOT EXISTS user_recharge_records (
|
||||
id BIGSERIAL PRIMARY KEY, public_id TEXT NOT NULL UNIQUE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
option_id TEXT, label TEXT, title TEXT, amount_usd DOUBLE PRECISION, provider TEXT NOT NULL DEFAULT 'comfy_checkout',
|
||||
checkout_url TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', note TEXT, metadata TEXT,
|
||||
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT), updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT), completed_at TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS user_recharge_records_user_created_idx ON user_recharge_records (user_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS user_recharge_records_user_status_idx ON user_recharge_records (user_id, status);
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE IF NOT EXISTS payment_orders (
|
||||
id BIGSERIAL PRIMARY KEY, public_id TEXT NOT NULL UNIQUE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
recharge_record_id BIGINT REFERENCES user_recharge_records(id) ON DELETE SET NULL, provider TEXT NOT NULL,
|
||||
payment_method TEXT NOT NULL DEFAULT 'stripe', provider_session_id TEXT, provider_payment_id TEXT,
|
||||
amount_usd DOUBLE PRECISION NOT NULL, amount_minor BIGINT NOT NULL, currency TEXT NOT NULL DEFAULT 'usd',
|
||||
status TEXT NOT NULL DEFAULT 'pending', checkout_url TEXT, success_url TEXT, cancel_url TEXT, note TEXT, metadata TEXT,
|
||||
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT), updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT), paid_at TEXT, completed_at TEXT, expires_at TEXT
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS payment_orders_provider_session_unique ON payment_orders (provider, provider_session_id) WHERE provider_session_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS payment_orders_user_created_idx ON payment_orders (user_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS payment_orders_status_idx ON payment_orders (status);
|
||||
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE IF NOT EXISTS agent_sessions (
|
||||
id TEXT PRIMARY KEY, user_id BIGINT, title TEXT, model TEXT NOT NULL DEFAULT '', permission_mode TEXT NOT NULL DEFAULT 'default',
|
||||
input_tokens BIGINT NOT NULL DEFAULT 0, output_tokens BIGINT NOT NULL DEFAULT 0, total_tokens BIGINT NOT NULL DEFAULT 0,
|
||||
cost_usd DOUBLE PRECISION NOT NULL DEFAULT 0, num_turns BIGINT NOT NULL DEFAULT 0, message_count BIGINT NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT), updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS agent_sessions_user_id_idx ON agent_sessions (user_id);
|
||||
CREATE INDEX IF NOT EXISTS agent_sessions_updated_at_idx ON agent_sessions (updated_at);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS transcript TEXT;
|
||||
@@ -0,0 +1,3 @@
|
||||
UPDATE downstream_api_keys SET max_cost = 1.0, updated_at = CURRENT_TIMESTAMP::TEXT
|
||||
WHERE user_id IS NOT NULL AND (max_cost IS NULL OR max_cost <= 0.0)
|
||||
AND (name LIKE '% 的默认密钥' OR name LIKE '% default key' OR key LIKE 'sk-user-%');
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE INDEX IF NOT EXISTS proxy_logs_created_at_desc_idx ON proxy_logs (created_at DESC, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS route_channels_route_enabled_cooldown_idx ON route_channels (route_id, enabled, cooldown_until, account_id, token_id);
|
||||
CREATE INDEX IF NOT EXISTS token_routes_enabled_id_idx ON token_routes (enabled, id);
|
||||
CREATE INDEX IF NOT EXISTS accounts_id_status_site_idx ON accounts (id, status, site_id);
|
||||
CREATE INDEX IF NOT EXISTS sites_id_status_url_idx ON sites (id, status, url);
|
||||
CREATE INDEX IF NOT EXISTS account_tokens_id_enabled_idx ON account_tokens (id, enabled);
|
||||
@@ -0,0 +1,3 @@
|
||||
CREATE INDEX IF NOT EXISTS token_routes_enabled_model_pattern_nocase_idx
|
||||
ON token_routes (COALESCE(enabled, TRUE), lower(model_pattern))
|
||||
WHERE model_pattern IS NOT NULL AND model_pattern <> '';
|
||||
@@ -0,0 +1,3 @@
|
||||
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,6 @@
|
||||
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,25 @@
|
||||
ALTER TABLE payment_orders ADD COLUMN IF NOT EXISTS credit_amount DOUBLE PRECISION NOT NULL DEFAULT 0;
|
||||
ALTER TABLE payment_orders ADD COLUMN IF NOT EXISTS credit_unit TEXT NOT NULL DEFAULT 'USD API quota';
|
||||
ALTER TABLE user_recharge_records ADD COLUMN IF NOT EXISTS currency TEXT;
|
||||
ALTER TABLE user_recharge_records ADD COLUMN IF NOT EXISTS credit_amount DOUBLE PRECISION;
|
||||
ALTER TABLE user_recharge_records ADD COLUMN IF NOT EXISTS credit_unit TEXT;
|
||||
CREATE TABLE IF NOT EXISTS payment_order_fulfillments (
|
||||
payment_order_id BIGINT PRIMARY KEY REFERENCES payment_orders(id) ON DELETE CASCADE,
|
||||
credited_key_id BIGINT REFERENCES downstream_api_keys(id) ON DELETE SET NULL,
|
||||
credit_amount DOUBLE PRECISION NOT NULL, credit_unit TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS payment_order_fulfillments_key_idx ON payment_order_fulfillments (credited_key_id);
|
||||
CREATE TABLE IF NOT EXISTS credit_ledger (
|
||||
id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, track TEXT NOT NULL, entry_type TEXT NOT NULL,
|
||||
amount DOUBLE PRECISION NOT NULL, balance_after DOUBLE PRECISION NOT NULL, ref_type TEXT, ref_id TEXT,
|
||||
model TEXT, operator TEXT, note TEXT, created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
|
||||
);
|
||||
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 BIGINT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
points_balance DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
points_total_recharged DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
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 (
|
||||
metadata::jsonb #>> '{workspaceAsset,role}' = 'output' OR metadata::jsonb #>> '{workflowUi,type}' = 'output'
|
||||
OR (source IN ('picture-agent','workflow-studio') AND metadata::jsonb ? 'responseItem')
|
||||
OR metadata::jsonb ? 'moneyPrinter' OR metadata::jsonb ? 'digitalHuman');
|
||||
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 (
|
||||
metadata::jsonb #>> '{workspaceAsset,role}' = 'output' OR metadata::jsonb #>> '{workflowUi,type}' = 'output'
|
||||
OR (source IN ('picture-agent','workflow-studio') AND metadata::jsonb ? 'responseItem')
|
||||
OR metadata::jsonb ? 'moneyPrinter' OR metadata::jsonb ? 'digitalHuman');
|
||||
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 (
|
||||
metadata::jsonb #>> '{workspaceAsset,role}' = 'output' OR metadata::jsonb #>> '{workflowUi,type}' = 'output'
|
||||
OR (source IN ('picture-agent','workflow-studio') AND metadata::jsonb ? 'responseItem')
|
||||
OR metadata::jsonb ? 'moneyPrinter' OR metadata::jsonb ? 'digitalHuman');
|
||||
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 (
|
||||
metadata::jsonb #>> '{workspaceAsset,role}' = 'output' OR metadata::jsonb #>> '{workflowUi,type}' = 'output'
|
||||
OR (source IN ('picture-agent','workflow-studio') AND metadata::jsonb ? 'responseItem')
|
||||
OR metadata::jsonb ? 'moneyPrinter' OR metadata::jsonb ? 'digitalHuman');
|
||||
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 (
|
||||
metadata::jsonb #>> '{workspaceAsset,role}' = 'output' OR metadata::jsonb #>> '{workflowUi,type}' = 'output'
|
||||
OR (source IN ('picture-agent','workflow-studio') AND metadata::jsonb ? 'responseItem')
|
||||
OR metadata::jsonb ? 'moneyPrinter' OR metadata::jsonb ? 'digitalHuman');
|
||||
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 (
|
||||
metadata::jsonb #>> '{workspaceAsset,role}' = 'output' OR metadata::jsonb #>> '{workflowUi,type}' = 'output'
|
||||
OR (source IN ('picture-agent','workflow-studio') AND metadata::jsonb ? 'responseItem')
|
||||
OR metadata::jsonb ? 'moneyPrinter' OR metadata::jsonb ? 'digitalHuman');
|
||||
@@ -0,0 +1,119 @@
|
||||
-- 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 BIGINT PRIMARY KEY,
|
||||
available_micros BIGINT NOT NULL DEFAULT 0,
|
||||
reserved_micros BIGINT NOT NULL DEFAULT 0,
|
||||
debt_micros BIGINT NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
version BIGINT NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
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 BIGSERIAL PRIMARY KEY,
|
||||
public_id TEXT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
source_type TEXT NOT NULL,
|
||||
source_id TEXT,
|
||||
payment_order_id BIGINT,
|
||||
total_micros BIGINT NOT NULL,
|
||||
remaining_micros BIGINT NOT NULL,
|
||||
reserved_micros BIGINT NOT NULL DEFAULT 0,
|
||||
starts_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
expires_at TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
scope_json TEXT,
|
||||
idempotency_key TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
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 BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
entry_type TEXT NOT NULL,
|
||||
amount_micros BIGINT NOT NULL,
|
||||
balance_after_micros BIGINT NOT NULL,
|
||||
grant_id BIGINT,
|
||||
ref_type TEXT,
|
||||
ref_id TEXT,
|
||||
model TEXT,
|
||||
operator TEXT,
|
||||
note TEXT,
|
||||
idempotency_key TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
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.
|
||||
INSERT INTO api_billing_accounts (user_id, available_micros)
|
||||
SELECT
|
||||
u.id,
|
||||
ROUND(COALESCE(SUM(
|
||||
CASE
|
||||
WHEN k.max_cost IS NOT NULL
|
||||
THEN GREATEST(k.max_cost - COALESCE(k.used_cost, 0), 0) * 1000000.0
|
||||
ELSE 0
|
||||
END
|
||||
), 0))::BIGINT
|
||||
FROM users u
|
||||
LEFT JOIN downstream_api_keys k ON k.user_id = u.id
|
||||
GROUP BY u.id
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
INSERT 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',
|
||||
user_id::TEXT,
|
||||
available_micros,
|
||||
available_micros,
|
||||
'migration:20260710000003:user:' || user_id
|
||||
FROM api_billing_accounts
|
||||
WHERE available_micros > 0
|
||||
ON CONFLICT (idempotency_key) DO NOTHING;
|
||||
|
||||
INSERT 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
|
||||
ON CONFLICT (idempotency_key) DO NOTHING;
|
||||
@@ -0,0 +1,296 @@
|
||||
-- Commerce catalog and immutable purchase snapshots. Money is stored in
|
||||
-- integer minor units and API credit in integer micros.
|
||||
CREATE TABLE IF NOT EXISTS commerce_products (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
public_id TEXT NOT NULL,
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
product_type TEXT NOT NULL,
|
||||
api_credit_micros BIGINT NOT NULL,
|
||||
subscription_days BIGINT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
sort_order BIGINT NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
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))
|
||||
);
|
||||
|
||||
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 BIGSERIAL PRIMARY KEY,
|
||||
product_id BIGINT NOT NULL REFERENCES commerce_products(id) ON DELETE CASCADE,
|
||||
currency TEXT NOT NULL,
|
||||
amount_minor BIGINT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
UNIQUE (product_id, currency),
|
||||
CHECK (currency IN ('CNY', 'USD')),
|
||||
CHECK (amount_minor > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS commerce_product_credit_scopes (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
product_id BIGINT NOT NULL REFERENCES commerce_products(id) ON DELETE CASCADE,
|
||||
scope_type TEXT NOT NULL,
|
||||
scope_value TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
UNIQUE (product_id, scope_type, scope_value),
|
||||
CHECK (scope_type IN ('model', 'model_prefix')),
|
||||
CHECK (length(btrim(scope_value)) > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS commerce_product_unlock_scopes (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
product_id BIGINT NOT NULL REFERENCES commerce_products(id) ON DELETE CASCADE,
|
||||
scope_type TEXT NOT NULL,
|
||||
scope_value TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
UNIQUE (product_id, scope_type, scope_value),
|
||||
CHECK (scope_type IN ('model', 'model_prefix')),
|
||||
CHECK (length(btrim(scope_value)) > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS commerce_model_gate_registry (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
scope_type TEXT NOT NULL,
|
||||
scope_value TEXT NOT NULL,
|
||||
first_seen_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
UNIQUE (scope_type, scope_value),
|
||||
CHECK (scope_type IN ('model', 'model_prefix')),
|
||||
CHECK (length(btrim(scope_value)) > 0)
|
||||
);
|
||||
|
||||
INSERT INTO commerce_model_gate_registry (scope_type, scope_value)
|
||||
SELECT scope_type, scope_value FROM commerce_product_unlock_scopes
|
||||
ON CONFLICT (scope_type, scope_value) DO NOTHING;
|
||||
|
||||
CREATE OR REPLACE FUNCTION commerce_register_model_gate() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
INSERT INTO commerce_model_gate_registry (scope_type, scope_value)
|
||||
VALUES (NEW.scope_type, NEW.scope_value)
|
||||
ON CONFLICT (scope_type, scope_value) DO NOTHING;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER commerce_product_unlock_scope_register_gate
|
||||
AFTER INSERT OR UPDATE OF scope_type, scope_value ON commerce_product_unlock_scopes
|
||||
FOR EACH ROW EXECUTE FUNCTION commerce_register_model_gate();
|
||||
|
||||
CREATE OR REPLACE FUNCTION commerce_gate_registry_append_only() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'commerce model gate registry is append-only';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER commerce_model_gate_registry_no_update
|
||||
BEFORE UPDATE ON commerce_model_gate_registry
|
||||
FOR EACH ROW EXECUTE FUNCTION commerce_gate_registry_append_only();
|
||||
CREATE TRIGGER commerce_model_gate_registry_no_delete
|
||||
BEFORE DELETE ON commerce_model_gate_registry
|
||||
FOR EACH ROW EXECUTE FUNCTION commerce_gate_registry_append_only();
|
||||
|
||||
CREATE OR REPLACE FUNCTION commerce_unlock_scope_subscription_only() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF (SELECT product_type FROM commerce_products WHERE id = NEW.product_id) IS DISTINCT FROM 'subscription' THEN
|
||||
RAISE EXCEPTION 'unlock scopes require a subscription product';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER commerce_unlock_scope_subscription_only_insert
|
||||
BEFORE INSERT ON commerce_product_unlock_scopes
|
||||
FOR EACH ROW EXECUTE FUNCTION commerce_unlock_scope_subscription_only();
|
||||
CREATE TRIGGER commerce_unlock_scope_subscription_only_update
|
||||
BEFORE UPDATE ON commerce_product_unlock_scopes
|
||||
FOR EACH ROW EXECUTE FUNCTION commerce_unlock_scope_subscription_only();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS commerce_orders (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
public_id TEXT NOT NULL,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
payment_order_id BIGINT NOT NULL REFERENCES payment_orders(id) ON DELETE RESTRICT,
|
||||
product_id BIGINT NOT NULL REFERENCES commerce_products(id) ON DELETE RESTRICT,
|
||||
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 BIGINT NOT NULL,
|
||||
api_credit_micros_snapshot BIGINT NOT NULL,
|
||||
subscription_days_snapshot BIGINT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
idempotency_key TEXT NOT NULL,
|
||||
snapshot_sealed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
expires_at TEXT NOT NULL,
|
||||
paid_at TEXT,
|
||||
fulfilled_at TEXT,
|
||||
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 (expires_at::timestamptz > created_at::timestamptz),
|
||||
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'))
|
||||
);
|
||||
|
||||
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 BIGSERIAL PRIMARY KEY,
|
||||
order_id BIGINT NOT NULL REFERENCES commerce_orders(id) ON DELETE CASCADE,
|
||||
scope_type TEXT NOT NULL,
|
||||
scope_value TEXT NOT NULL,
|
||||
UNIQUE (order_id, scope_type, scope_value),
|
||||
CHECK (scope_type IN ('model', 'model_prefix')),
|
||||
CHECK (length(btrim(scope_value)) > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS commerce_order_unlock_scopes (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
order_id BIGINT NOT NULL REFERENCES commerce_orders(id) ON DELETE CASCADE,
|
||||
scope_type TEXT NOT NULL,
|
||||
scope_value TEXT NOT NULL,
|
||||
UNIQUE (order_id, scope_type, scope_value),
|
||||
CHECK (scope_type IN ('model', 'model_prefix')),
|
||||
CHECK (length(btrim(scope_value)) > 0)
|
||||
);
|
||||
|
||||
INSERT INTO commerce_model_gate_registry (scope_type, scope_value)
|
||||
SELECT scope_type, scope_value FROM commerce_order_unlock_scopes
|
||||
ON CONFLICT (scope_type, scope_value) DO NOTHING;
|
||||
|
||||
CREATE TRIGGER commerce_order_unlock_scope_register_gate
|
||||
AFTER INSERT OR UPDATE OF scope_type, scope_value ON commerce_order_unlock_scopes
|
||||
FOR EACH ROW EXECUTE FUNCTION commerce_register_model_gate();
|
||||
|
||||
CREATE OR REPLACE FUNCTION commerce_order_snapshot_immutable() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF ROW(NEW.user_id, NEW.payment_order_id, NEW.product_id, NEW.product_public_id_snapshot,
|
||||
NEW.product_code_snapshot, NEW.product_name_snapshot, NEW.product_type_snapshot,
|
||||
NEW.order_kind, NEW.currency, NEW.amount_minor, NEW.api_credit_micros_snapshot,
|
||||
NEW.subscription_days_snapshot, NEW.idempotency_key, NEW.expires_at)
|
||||
IS DISTINCT FROM
|
||||
ROW(OLD.user_id, OLD.payment_order_id, OLD.product_id, OLD.product_public_id_snapshot,
|
||||
OLD.product_code_snapshot, OLD.product_name_snapshot, OLD.product_type_snapshot,
|
||||
OLD.order_kind, OLD.currency, OLD.amount_minor, OLD.api_credit_micros_snapshot,
|
||||
OLD.subscription_days_snapshot, OLD.idempotency_key, OLD.expires_at) THEN
|
||||
RAISE EXCEPTION 'commerce order snapshot is immutable';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER commerce_order_snapshot_fields_immutable
|
||||
BEFORE UPDATE ON commerce_orders FOR EACH ROW EXECUTE FUNCTION commerce_order_snapshot_immutable();
|
||||
|
||||
CREATE OR REPLACE FUNCTION commerce_product_unlock_scope_type_guard() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.product_type <> 'subscription' AND EXISTS (
|
||||
SELECT 1 FROM commerce_product_unlock_scopes WHERE product_id = OLD.id
|
||||
) THEN
|
||||
RAISE EXCEPTION 'unlock scopes require a subscription product';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER commerce_product_unlock_scope_type_guard
|
||||
BEFORE UPDATE OF product_type ON commerce_products
|
||||
FOR EACH ROW EXECUTE FUNCTION commerce_product_unlock_scope_type_guard();
|
||||
|
||||
CREATE OR REPLACE FUNCTION commerce_order_cannot_unseal() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF OLD.snapshot_sealed AND NOT NEW.snapshot_sealed THEN
|
||||
RAISE EXCEPTION 'commerce order snapshot cannot be unsealed';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER commerce_order_snapshot_cannot_unseal
|
||||
BEFORE UPDATE OF snapshot_sealed ON commerce_orders
|
||||
FOR EACH ROW EXECUTE FUNCTION commerce_order_cannot_unseal();
|
||||
|
||||
CREATE OR REPLACE FUNCTION commerce_order_scope_sealed_guard() RETURNS TRIGGER AS $$
|
||||
DECLARE target_order_id BIGINT;
|
||||
BEGIN
|
||||
target_order_id := CASE WHEN TG_OP = 'DELETE' THEN OLD.order_id ELSE NEW.order_id END;
|
||||
IF (SELECT snapshot_sealed FROM commerce_orders WHERE id = target_order_id) THEN
|
||||
RAISE EXCEPTION 'commerce order scope snapshot is sealed';
|
||||
END IF;
|
||||
RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER commerce_order_credit_scope_sealed
|
||||
BEFORE INSERT OR UPDATE OR DELETE ON commerce_order_credit_scopes
|
||||
FOR EACH ROW EXECUTE FUNCTION commerce_order_scope_sealed_guard();
|
||||
CREATE TRIGGER commerce_order_unlock_scope_sealed
|
||||
BEFORE INSERT OR UPDATE OR DELETE ON commerce_order_unlock_scopes
|
||||
FOR EACH ROW EXECUTE FUNCTION commerce_order_scope_sealed_guard();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS commerce_subscription_periods (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
public_id TEXT NOT NULL,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
product_id BIGINT NOT NULL REFERENCES commerce_products(id) ON DELETE RESTRICT,
|
||||
source_order_id BIGINT NOT NULL REFERENCES commerce_orders(id) ON DELETE RESTRICT,
|
||||
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 (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
UNIQUE (source_order_id),
|
||||
CHECK (ends_at::timestamptz > starts_at::timestamptz),
|
||||
CHECK (access_ends_at IS NULL OR
|
||||
(access_ends_at::timestamptz >= starts_at::timestamptz AND
|
||||
access_ends_at::timestamptz <= ends_at::timestamptz)),
|
||||
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 OR REPLACE FUNCTION commerce_subscription_period_window_immutable() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF ROW(NEW.user_id, NEW.product_id, NEW.source_order_id, NEW.starts_at, NEW.ends_at)
|
||||
IS DISTINCT FROM ROW(OLD.user_id, OLD.product_id, OLD.source_order_id, OLD.starts_at, OLD.ends_at) THEN
|
||||
RAISE EXCEPTION 'subscription period window is immutable';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER commerce_subscription_period_window_immutable
|
||||
BEFORE UPDATE ON commerce_subscription_periods
|
||||
FOR EACH ROW EXECUTE FUNCTION commerce_subscription_period_window_immutable();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS commerce_order_fulfillments (
|
||||
order_id BIGINT PRIMARY KEY REFERENCES commerce_orders(id) ON DELETE RESTRICT,
|
||||
idempotency_key TEXT NOT NULL UNIQUE,
|
||||
api_credit_grant_id BIGINT REFERENCES api_credit_grants(id) ON DELETE RESTRICT,
|
||||
subscription_period_id BIGINT REFERENCES commerce_subscription_periods(id) ON DELETE RESTRICT,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT)
|
||||
);
|
||||
@@ -0,0 +1,341 @@
|
||||
-- Multi-provider payment routing. Credentials are encrypted by the application.
|
||||
CREATE TABLE IF NOT EXISTS payment_provider_instances (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
public_id TEXT NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
credentials_ciphertext BYTEA NOT NULL,
|
||||
credentials_version BIGINT NOT NULL DEFAULT 1,
|
||||
callback_token_hash TEXT NOT NULL,
|
||||
callback_token_ciphertext BYTEA NOT NULL,
|
||||
config_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
CHECK (provider IN ('easypay', 'alipay', 'wechatpay', 'stripe', 'comfy_cloud')),
|
||||
CHECK (credentials_version > 0),
|
||||
CHECK (octet_length(credentials_ciphertext) > 29),
|
||||
CHECK (length(callback_token_hash) = 64),
|
||||
CHECK (octet_length(callback_token_ciphertext) > 29),
|
||||
CHECK (jsonb_typeof(config_json::jsonb) IS NOT NULL)
|
||||
);
|
||||
|
||||
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);
|
||||
CREATE INDEX IF NOT EXISTS payment_provider_instances_provider_enabled_idx ON payment_provider_instances (provider, enabled, id);
|
||||
|
||||
CREATE OR REPLACE FUNCTION payment_provider_instance_identity_immutable() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF ROW(NEW.public_id, NEW.provider) IS DISTINCT FROM ROW(OLD.public_id, OLD.provider) THEN
|
||||
RAISE EXCEPTION 'payment provider instance identity is immutable';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
CREATE TRIGGER payment_provider_instance_identity_immutable
|
||||
BEFORE UPDATE ON payment_provider_instances
|
||||
FOR EACH ROW EXECUTE FUNCTION payment_provider_instance_identity_immutable();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_method_routes (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
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 BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
CHECK (payment_method IN ('alipay', 'wechatpay', 'card', 'comfy_cloud')),
|
||||
CHECK (currency IN ('CNY', 'USD')),
|
||||
CHECK (selection_strategy IN ('round_robin', 'least_amount'))
|
||||
);
|
||||
|
||||
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 OR REPLACE FUNCTION payment_method_route_identity_immutable() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF ROW(NEW.public_id, NEW.payment_method, NEW.currency)
|
||||
IS DISTINCT FROM ROW(OLD.public_id, OLD.payment_method, OLD.currency) THEN
|
||||
RAISE EXCEPTION 'payment method route identity is immutable';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
CREATE TRIGGER payment_method_route_identity_immutable
|
||||
BEFORE UPDATE ON payment_method_routes
|
||||
FOR EACH ROW EXECUTE FUNCTION payment_method_route_identity_immutable();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_method_route_members (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
route_id BIGINT NOT NULL REFERENCES payment_method_routes(id) ON DELETE CASCADE,
|
||||
provider_instance_id BIGINT NOT NULL REFERENCES payment_provider_instances(id) ON DELETE RESTRICT,
|
||||
position BIGINT NOT NULL DEFAULT 0,
|
||||
daily_capacity_minor BIGINT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
UNIQUE (route_id, provider_instance_id),
|
||||
CHECK (daily_capacity_minor IS NULL OR daily_capacity_minor > 0)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS payment_method_route_members_selection_idx ON payment_method_route_members (route_id, enabled, position, id);
|
||||
|
||||
CREATE OR REPLACE FUNCTION payment_route_member_provider_guard() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF 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'))
|
||||
) THEN
|
||||
RAISE EXCEPTION 'provider instance does not support route payment method';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
CREATE TRIGGER payment_route_member_provider_guard
|
||||
BEFORE INSERT OR UPDATE OF route_id, provider_instance_id ON payment_method_route_members
|
||||
FOR EACH ROW EXECUTE FUNCTION payment_route_member_provider_guard();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_method_route_cursors (
|
||||
route_id BIGINT PRIMARY KEY REFERENCES payment_method_routes(id) ON DELETE CASCADE,
|
||||
last_member_id BIGINT REFERENCES payment_method_route_members(id) ON DELETE SET NULL,
|
||||
version BIGINT NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_route_member_daily_usage (
|
||||
member_id BIGINT NOT NULL REFERENCES payment_method_route_members(id) ON DELETE CASCADE,
|
||||
usage_date TEXT NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
reserved_minor BIGINT NOT NULL DEFAULT 0,
|
||||
captured_minor BIGINT NOT NULL DEFAULT 0,
|
||||
refunded_minor BIGINT NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
PRIMARY KEY (member_id, usage_date, currency),
|
||||
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 BIGSERIAL PRIMARY KEY,
|
||||
public_id TEXT NOT NULL,
|
||||
payment_order_id BIGINT NOT NULL REFERENCES payment_orders(id) ON DELETE RESTRICT,
|
||||
route_id BIGINT NOT NULL REFERENCES payment_method_routes(id) ON DELETE RESTRICT,
|
||||
member_id BIGINT NOT NULL REFERENCES payment_method_route_members(id) ON DELETE RESTRICT,
|
||||
provider_instance_id BIGINT NOT NULL REFERENCES payment_provider_instances(id) ON DELETE RESTRICT,
|
||||
provider TEXT NOT NULL,
|
||||
payment_method TEXT NOT NULL,
|
||||
amount_minor BIGINT 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 (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
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 OR REPLACE FUNCTION payment_order_provider_binding_immutable() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF ROW(NEW.payment_order_id, NEW.route_id, NEW.member_id, NEW.provider_instance_id,
|
||||
NEW.provider, NEW.payment_method, NEW.amount_minor, NEW.currency)
|
||||
IS DISTINCT FROM
|
||||
ROW(OLD.payment_order_id, OLD.route_id, OLD.member_id, OLD.provider_instance_id,
|
||||
OLD.provider, OLD.payment_method, OLD.amount_minor, OLD.currency) THEN
|
||||
RAISE EXCEPTION 'payment order provider binding is immutable';
|
||||
END IF;
|
||||
IF OLD.provider_payment_id IS NOT NULL
|
||||
AND NEW.provider_payment_id IS DISTINCT FROM OLD.provider_payment_id THEN
|
||||
RAISE EXCEPTION 'provider payment ID is immutable after first assignment';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
CREATE TRIGGER payment_order_provider_binding_identity_immutable
|
||||
BEFORE UPDATE ON payment_order_provider_bindings
|
||||
FOR EACH ROW EXECUTE FUNCTION payment_order_provider_binding_immutable();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_provider_event_inbox (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
public_id TEXT NOT NULL,
|
||||
provider_instance_id BIGINT NOT NULL REFERENCES payment_provider_instances(id) ON DELETE RESTRICT,
|
||||
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 BIGINT NOT NULL DEFAULT 0,
|
||||
last_error TEXT,
|
||||
received_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
processed_at TEXT,
|
||||
UNIQUE (provider_instance_id, provider_event_id),
|
||||
CHECK (length(btrim(provider_event_id)) > 0),
|
||||
CHECK (length(btrim(event_type)) > 0),
|
||||
CHECK (jsonb_typeof(payload_json::jsonb) IS NOT NULL),
|
||||
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 OR REPLACE FUNCTION payment_provider_event_content_immutable() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF ROW(NEW.public_id, NEW.provider_instance_id, NEW.provider_event_id, NEW.event_type,
|
||||
NEW.payload_json, NEW.payload_sha256, NEW.received_at)
|
||||
IS DISTINCT FROM
|
||||
ROW(OLD.public_id, OLD.provider_instance_id, OLD.provider_event_id, OLD.event_type,
|
||||
OLD.payload_json, OLD.payload_sha256, OLD.received_at) THEN
|
||||
RAISE EXCEPTION 'payment provider event content is immutable';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
CREATE TRIGGER payment_provider_event_content_immutable
|
||||
BEFORE UPDATE ON payment_provider_event_inbox
|
||||
FOR EACH ROW EXECUTE FUNCTION payment_provider_event_content_immutable();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_jobs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
public_id TEXT NOT NULL,
|
||||
job_type TEXT NOT NULL,
|
||||
payment_order_id BIGINT REFERENCES payment_orders(id) ON DELETE RESTRICT,
|
||||
binding_id BIGINT REFERENCES payment_order_provider_bindings(id) ON DELETE RESTRICT,
|
||||
provider_instance_id BIGINT NOT NULL REFERENCES payment_provider_instances(id) ON DELETE RESTRICT,
|
||||
event_id BIGINT REFERENCES payment_provider_event_inbox(id) ON DELETE RESTRICT,
|
||||
payload_json TEXT NOT NULL DEFAULT '{}',
|
||||
result_json TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
attempt_count BIGINT NOT NULL DEFAULT 0,
|
||||
max_attempts BIGINT NOT NULL DEFAULT 8,
|
||||
available_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
lease_token TEXT,
|
||||
leased_until TEXT,
|
||||
last_error TEXT,
|
||||
idempotency_key TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
CHECK (job_type IN ('create_checkout', 'query_payment', 'capture', 'process_event', 'refund')),
|
||||
CHECK (jsonb_typeof(payload_json::jsonb) IS NOT NULL),
|
||||
CHECK (result_json IS NULL OR jsonb_typeof(result_json::jsonb) IS NOT NULL),
|
||||
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 BIGSERIAL PRIMARY KEY,
|
||||
public_id TEXT NOT NULL,
|
||||
user_id BIGINT REFERENCES users(id) ON DELETE RESTRICT,
|
||||
payment_order_id BIGINT NOT NULL REFERENCES payment_orders(id) ON DELETE RESTRICT,
|
||||
binding_id BIGINT NOT NULL REFERENCES payment_order_provider_bindings(id) ON DELETE RESTRICT,
|
||||
amount_minor BIGINT NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
reason TEXT,
|
||||
requested_by TEXT NOT NULL,
|
||||
force_reversal BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
entitlement_target_micros BIGINT NOT NULL DEFAULT 0,
|
||||
entitlement_shortfall_micros BIGINT NOT NULL DEFAULT 0,
|
||||
subscription_target_seconds BIGINT NOT NULL DEFAULT 0,
|
||||
subscription_shortfall_seconds BIGINT 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 (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
CHECK (amount_minor > 0),
|
||||
CHECK (currency IN ('CNY', 'USD')),
|
||||
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 BIGSERIAL PRIMARY KEY,
|
||||
refund_request_id BIGINT NOT NULL REFERENCES payment_refund_requests(id) ON DELETE RESTRICT,
|
||||
grant_id BIGINT NOT NULL REFERENCES api_credit_grants(id) ON DELETE RESTRICT,
|
||||
reserved_micros BIGINT NOT NULL,
|
||||
grant_status_before TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'reserved',
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
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 BIGINT PRIMARY KEY REFERENCES payment_refund_requests(id) ON DELETE RESTRICT,
|
||||
subscription_period_id BIGINT NOT NULL REFERENCES commerce_subscription_periods(id) ON DELETE RESTRICT,
|
||||
previous_access_ends_at TEXT NOT NULL,
|
||||
previous_status TEXT NOT NULL,
|
||||
reserved_access_ends_at TEXT NOT NULL,
|
||||
reversal_seconds BIGINT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'reserved',
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
CHECK (reversal_seconds >= 0),
|
||||
CHECK (previous_status IN ('active', 'revoked')),
|
||||
CHECK (status IN ('reserved', 'finalized', 'released'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_refunds (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
public_id TEXT NOT NULL,
|
||||
refund_request_id BIGINT NOT NULL REFERENCES payment_refund_requests(id) ON DELETE RESTRICT,
|
||||
provider_instance_id BIGINT NOT NULL REFERENCES payment_provider_instances(id) ON DELETE RESTRICT,
|
||||
provider_refund_id TEXT,
|
||||
amount_minor BIGINT NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'processing',
|
||||
response_json TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
completed_at TEXT,
|
||||
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 jsonb_typeof(response_json::jsonb) IS NOT NULL)
|
||||
);
|
||||
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 BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT 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 (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
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,26 @@
|
||||
-- Durable pre-authorization leases bind ordinary API spend to one in-flight
|
||||
-- request per user. No balance is moved into reserved_micros for a lease.
|
||||
CREATE TABLE IF NOT EXISTS api_request_leases (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
public_id TEXT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
downstream_key_id BIGINT,
|
||||
model TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
acquired_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
expires_at TEXT NOT NULL,
|
||||
released_at TEXT,
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
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,74 @@
|
||||
-- Durable Agent Workbench run lifecycle and per-session workspace identity.
|
||||
-- Active statuses include `cancelling` until the worker has actually stopped.
|
||||
|
||||
ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS workspace_id TEXT;
|
||||
ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS revision BIGINT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS runtime_status TEXT NOT NULL DEFAULT 'idle';
|
||||
ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS context_version BIGINT NOT NULL DEFAULT 1;
|
||||
|
||||
UPDATE agent_sessions
|
||||
SET workspace_id = substr(workspace_hash, 1, 8) || '-' ||
|
||||
substr(workspace_hash, 9, 4) || '-' ||
|
||||
'4' || substr(workspace_hash, 14, 3) || '-' ||
|
||||
'a' || substr(workspace_hash, 18, 3) || '-' ||
|
||||
substr(workspace_hash, 21, 12)
|
||||
FROM (
|
||||
SELECT id,
|
||||
md5(random()::TEXT || clock_timestamp()::TEXT || id::TEXT) AS workspace_hash
|
||||
FROM agent_sessions
|
||||
WHERE workspace_id IS NULL OR btrim(workspace_id) = ''
|
||||
) AS generated
|
||||
WHERE agent_sessions.id = generated.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,
|
||||
session_id TEXT NOT NULL,
|
||||
user_id BIGINT,
|
||||
actor_key TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'starting',
|
||||
model TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL DEFAULT 0,
|
||||
error_message TEXT,
|
||||
cancel_requested_at TEXT,
|
||||
started_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
finished_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
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,
|
||||
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 (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
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,6 @@
|
||||
-- Link proxy usage records to durable Agent Workbench runs.
|
||||
ALTER TABLE proxy_logs ADD COLUMN IF NOT EXISTS 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,32 @@
|
||||
CREATE TABLE IF NOT EXISTS comfy_cloud_jobs (
|
||||
job_id TEXT PRIMARY KEY NOT NULL,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
last_synced_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
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 BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
job_id TEXT,
|
||||
asset_hash TEXT,
|
||||
name TEXT,
|
||||
tags_json TEXT NOT NULL DEFAULT '[]',
|
||||
snapshot_json TEXT,
|
||||
is_preview BIGINT NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
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,59 @@
|
||||
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 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 BIGINT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
available_micros BIGINT NOT NULL DEFAULT 0, reserved_micros BIGINT NOT NULL DEFAULT 0,
|
||||
debt_micros BIGINT NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'active', version BIGINT NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT), updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
CHECK (available_micros >= 0), CHECK (reserved_micros >= 0), CHECK (debt_micros >= 0), CHECK (status IN ('active','frozen','closed'))
|
||||
);
|
||||
CREATE TABLE comfy_credit_grants (
|
||||
id BIGSERIAL PRIMARY KEY, public_id TEXT NOT NULL UNIQUE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
grant_kind TEXT NOT NULL, source_type TEXT NOT NULL, source_id TEXT, payment_order_id BIGINT REFERENCES payment_orders(id) ON DELETE SET NULL,
|
||||
total_micros BIGINT NOT NULL, remaining_micros BIGINT NOT NULL, reserved_micros BIGINT NOT NULL DEFAULT 0,
|
||||
starts_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT), expires_at TEXT, status TEXT NOT NULL DEFAULT 'active',
|
||||
idempotency_key TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT), updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
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 BIGSERIAL PRIMARY KEY, public_id TEXT NOT NULL UNIQUE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
task_type TEXT NOT NULL, ref_id TEXT NOT NULL, amount_micros BIGINT NOT NULL, package_micros BIGINT NOT NULL DEFAULT 0,
|
||||
balance_micros BIGINT NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'active', idempotency_key TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT), updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT), settled_at TEXT, released_at TEXT,
|
||||
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 BIGSERIAL PRIMARY KEY, hold_id BIGINT NOT NULL REFERENCES comfy_charge_holds(id) ON DELETE CASCADE,
|
||||
grant_id BIGINT NOT NULL REFERENCES comfy_credit_grants(id) ON DELETE RESTRICT, amount_micros BIGINT NOT NULL,
|
||||
grant_kind TEXT NOT NULL, UNIQUE (hold_id, grant_id), CHECK (amount_micros > 0), CHECK (grant_kind IN ('package','balance'))
|
||||
);
|
||||
CREATE TABLE comfy_entitlement_ledger (
|
||||
id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, entry_type TEXT NOT NULL,
|
||||
amount_micros BIGINT NOT NULL, balance_after_micros BIGINT NOT NULL, package_amount_micros BIGINT NOT NULL DEFAULT 0,
|
||||
balance_amount_micros BIGINT NOT NULL DEFAULT 0, grant_id BIGINT REFERENCES comfy_credit_grants(id) ON DELETE SET NULL,
|
||||
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 (CURRENT_TIMESTAMP::TEXT)
|
||||
);
|
||||
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, display_name TEXT NOT NULL, points_micros BIGINT NOT NULL DEFAULT 0,
|
||||
enabled BOOLEAN NOT NULL DEFAULT FALSE, updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT), CHECK (points_micros >= 0)
|
||||
);
|
||||
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(GREATEST(points_balance,0) * 1000000.0)::BIGINT 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(GREATEST(points_balance,0)*1000000.0)::BIGINT,ROUND(GREATEST(points_balance,0)*1000000.0)::BIGINT,
|
||||
'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 BIGINT;
|
||||
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 BIGINT;
|
||||
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 BIGINT REFERENCES comfy_credit_grants(id) ON DELETE RESTRICT;
|
||||
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,33 @@
|
||||
ALTER TABLE api_entitlement_ledger ADD COLUMN package_amount_micros BIGINT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE api_entitlement_ledger ADD COLUMN balance_amount_micros BIGINT NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE TABLE comfy_refund_entitlement_reservations (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
refund_request_id BIGINT NOT NULL REFERENCES payment_refund_requests(id) ON DELETE RESTRICT,
|
||||
grant_id BIGINT NOT NULL REFERENCES comfy_credit_grants(id) ON DELETE RESTRICT,
|
||||
reserved_micros BIGINT NOT NULL,
|
||||
grant_status_before TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'reserved',
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
UNIQUE (refund_request_id, grant_id),
|
||||
CHECK (reserved_micros > 0),
|
||||
CHECK (status IN ('reserved', 'finalized', 'released'))
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION commerce_order_billing_snapshot_immutable() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF ROW(NEW.billing_track_snapshot, NEW.grant_kind_snapshot,
|
||||
NEW.benefit_type_snapshot, NEW.validity_days_snapshot)
|
||||
IS DISTINCT FROM
|
||||
ROW(OLD.billing_track_snapshot, OLD.grant_kind_snapshot,
|
||||
OLD.benefit_type_snapshot, OLD.validity_days_snapshot) THEN
|
||||
RAISE EXCEPTION 'commerce order billing snapshot is immutable';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER commerce_order_billing_snapshot_fields_immutable
|
||||
BEFORE UPDATE ON commerce_orders FOR EACH ROW
|
||||
EXECUTE FUNCTION commerce_order_billing_snapshot_immutable();
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE user_profile_avatars (
|
||||
user_id BIGINT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
public_id TEXT NOT NULL UNIQUE,
|
||||
image_bytes BYTEA NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
size_bytes BIGINT NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
|
||||
CHECK (size_bytes > 0)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user