Files
geo/server/internal/tenant/repository/queries/kol_subscription.sql
T
root 70725193eb fix(kol): Phase 1 review — ACL lineage, expiry filters, tighter tenant scope
Addresses findings from consolidated Phase 1 code review:

- Add composite unique indexes + composite FKs so subscription_prompts and
  packages cannot reference mismatched (tenant_id, package_id, prompt_id)
  tuples, enforcing ACL lineage at the schema level.
- Add status CHECK constraints on all KOL tables as defense-in-depth.
- ListActiveSubscribersForPackage: drop meaningless tenant_id IS NOT NULL,
  add end_at expiry filter so fan-out skips expired subscribers.
- ApproveKolSubscription: add status='pending' guard to prevent double-approval.
- ListSubscriptionPromptsByTenant: join subscription/prompt/package/profile
  lifecycle so hidden/archived content no longer leaks; surface
  kol_display_name for UI.
- ListPublishedKolPackages: exclude expired subscribers from marketplace count.
- KolDashboardTrend: rewrite as CTE with day-series, returning both daily
  usage and new subscription counts.
- Remove cosmetic tenant_id IS NOT NULL from dashboard overview queries
  (the column is NOT NULL by schema).
- check_tenant_scope.sh: require tenant_id = sqlc.(n?)arg filter, reject
  tenant_id IS NOT NULL as a fake scope, maintain explicit exemption list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 08:34:12 +08:00

109 lines
4.3 KiB
SQL

-- name: CreateKolSubscription :one
INSERT INTO kol_subscriptions (tenant_id, package_id, status)
VALUES (sqlc.arg(tenant_id)::bigint, sqlc.arg(package_id), 'pending')
RETURNING id, tenant_id, package_id, status, approved_by, start_at, end_at, created_at, updated_at;
-- name: GetKolSubscriptionByID :one
SELECT id, tenant_id, package_id, status, approved_by, start_at, end_at, created_at, updated_at
FROM kol_subscriptions
WHERE id = sqlc.arg(id)
AND tenant_id = sqlc.arg(tenant_id)::bigint;
-- name: GetActiveKolSubscription :one
SELECT id, tenant_id, package_id, status, approved_by, start_at, end_at, created_at, updated_at
FROM kol_subscriptions
WHERE tenant_id = sqlc.arg(tenant_id)::bigint
AND package_id = sqlc.arg(package_id)
AND status IN ('pending', 'active');
-- name: ListKolSubscriptions :many
SELECT id, tenant_id, package_id, status, approved_by, start_at, end_at, created_at, updated_at
FROM kol_subscriptions
WHERE tenant_id = sqlc.arg(tenant_id)::bigint
ORDER BY created_at DESC;
-- name: ListActiveSubscribersForPackage :many
-- Cross-tenant read: used by platform admin for subscription fan-out.
-- Excludes expired subscriptions so new prompts are not granted to them.
SELECT id, tenant_id, package_id, status, approved_by, start_at, end_at, created_at, updated_at
FROM kol_subscriptions
WHERE package_id = sqlc.arg(package_id)
AND status = 'active'
AND (end_at IS NULL OR end_at > NOW());
-- name: ApproveKolSubscription :exec
UPDATE kol_subscriptions
SET status = 'active',
approved_by = sqlc.arg(approved_by),
start_at = NOW(),
end_at = sqlc.arg(end_at),
updated_at = NOW()
WHERE id = sqlc.arg(id)
AND tenant_id = sqlc.arg(tenant_id)::bigint
AND status = 'pending';
-- name: RevokeKolSubscription :exec
UPDATE kol_subscriptions
SET status = 'revoked', updated_at = NOW()
WHERE id = sqlc.arg(id)
AND tenant_id = sqlc.arg(tenant_id)::bigint;
-- name: CreateSubscriptionPrompt :exec
INSERT INTO kol_subscription_prompts (tenant_id, subscription_id, package_id, prompt_id, status)
VALUES (sqlc.arg(tenant_id)::bigint, sqlc.arg(subscription_id),
sqlc.arg(package_id), sqlc.arg(prompt_id), 'active')
ON CONFLICT (subscription_id, prompt_id) DO NOTHING;
-- name: ListSubscriptionPromptsByTenant :many
SELECT sp.id, sp.tenant_id, sp.subscription_id, sp.package_id, sp.prompt_id,
sp.status, sp.granted_at, sp.revoked_at,
p.name AS prompt_name, p.platform_hint, p.published_revision_no,
k.name AS package_name,
pf.display_name AS kol_display_name
FROM kol_subscription_prompts sp
JOIN kol_subscriptions s ON s.id = sp.subscription_id
AND s.tenant_id = sp.tenant_id
AND s.package_id = sp.package_id
AND s.status = 'active'
AND (s.end_at IS NULL OR s.end_at > NOW())
JOIN kol_prompts p ON p.id = sp.prompt_id
AND p.package_id = sp.package_id
AND p.status = 'active'
AND p.published_revision_no IS NOT NULL
AND p.deleted_at IS NULL
JOIN kol_packages k ON k.id = sp.package_id
AND k.status = 'published'
AND k.deleted_at IS NULL
JOIN kol_profiles pf ON pf.id = k.kol_profile_id
AND pf.status = 'active'
AND pf.deleted_at IS NULL
WHERE sp.tenant_id = sqlc.arg(tenant_id)::bigint
AND sp.status = 'active'
ORDER BY sp.granted_at DESC;
-- name: GetSubscriptionPromptByID :one
SELECT sp.id, sp.tenant_id, sp.subscription_id, sp.package_id, sp.prompt_id,
sp.status, sp.granted_at, sp.revoked_at,
s.status AS subscription_status, s.end_at AS subscription_end_at,
p.name AS prompt_name, p.platform_hint, p.status AS prompt_status,
p.published_revision_no,
k.name AS package_name, k.status AS package_status
FROM kol_subscription_prompts sp
JOIN kol_subscriptions s ON s.id = sp.subscription_id
JOIN kol_prompts p ON p.id = sp.prompt_id AND p.deleted_at IS NULL
JOIN kol_packages k ON k.id = sp.package_id AND k.deleted_at IS NULL
WHERE sp.id = sqlc.arg(id)
AND sp.tenant_id = sqlc.arg(tenant_id)::bigint;
-- name: RevokeSubscriptionPromptsBySubscription :exec
UPDATE kol_subscription_prompts
SET status = 'revoked', revoked_at = NOW(), updated_at = NOW()
WHERE subscription_id = sqlc.arg(subscription_id)
AND tenant_id = sqlc.arg(tenant_id)::bigint;
-- name: RevokeSubscriptionPromptsByPrompt :exec
UPDATE kol_subscription_prompts
SET status = 'revoked', revoked_at = NOW(), updated_at = NOW()
WHERE prompt_id = sqlc.arg(prompt_id)
AND tenant_id = sqlc.arg(tenant_id)::bigint;