90ac0b81d1
Add full V2 architecture: identity, content, studio (.NET 10) and file, render, notification, gateway (Go) services with vendored deps, plus DB migrations, event/API contracts, and an init-db script. Wire the Next.js frontend to the gateway: server-side JWT auth routes (login/register/refresh/logout/me), gateway fetch helper, and session/ cookie/jwt helpers under src/lib. Containerize the stack via docker-compose.v2.yml and per-service Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via next/font/local to avoid Google Fonts (geo-blocked). Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
315 lines
14 KiB
SQL
315 lines
14 KiB
SQL
-- =====================================================================
|
|
-- IDENTITY SCHEMA — Part 3: Plans, Subscriptions, Payments, Discounts
|
|
-- =====================================================================
|
|
|
|
SET search_path TO identity, public;
|
|
|
|
CREATE TYPE plan_scope AS ENUM ('User','Tenant');
|
|
CREATE TYPE billing_period AS ENUM ('Monthly','Quarterly','SemiAnnual','Annual','Lifetime','OneTime');
|
|
CREATE TYPE payment_gateway AS ENUM ('ZarinPal','IdPay','Bazaar','Stripe','Balance','Manual');
|
|
CREATE TYPE payment_status AS ENUM ('Pending','Succeeded','Failed','Refunded','Cancelled');
|
|
CREATE TYPE payment_action AS ENUM ('PlanPurchase','BalanceCharge','ProjectRender','UserProject','StorageUpgrade','Other');
|
|
CREATE TYPE discount_kind AS ENUM ('Percentage','FixedAmount','FreeMonths','RenderCredits');
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- plans — subscription tiers (FlatRender or tenant-defined)
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE plans (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
|
-- NULL tenant_id = global plan available to all tenants
|
|
|
|
scope plan_scope NOT NULL DEFAULT 'User',
|
|
code TEXT NOT NULL, -- 'pro_monthly', 'business_annual'
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
|
|
-- Pricing
|
|
price_minor BIGINT NOT NULL DEFAULT 0,
|
|
before_price_minor BIGINT, -- shown crossed-out
|
|
currency TEXT NOT NULL DEFAULT 'IRR',
|
|
discount_percentage NUMERIC(5,2) NOT NULL DEFAULT 0,
|
|
|
|
billing_period billing_period NOT NULL DEFAULT 'Monthly',
|
|
months_duration INT, -- for non-monthly
|
|
|
|
-- Quotas
|
|
seconds_charge INT NOT NULL DEFAULT 0, -- render seconds included
|
|
monthly_renders_quota INT, -- NULL = unlimited
|
|
storage_gb INT NOT NULL DEFAULT 1,
|
|
parallel_renders INT NOT NULL DEFAULT 1,
|
|
max_resolution TEXT NOT NULL DEFAULT 'FullHD',
|
|
min_video_length_sec INT NOT NULL DEFAULT 0,
|
|
render_speed_factor NUMERIC(4,2) NOT NULL DEFAULT 1.0,
|
|
|
|
-- UI
|
|
sort INT NOT NULL DEFAULT 0,
|
|
icon TEXT,
|
|
cover TEXT,
|
|
loyalty_mark TEXT,
|
|
color TEXT,
|
|
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
-- Feature flags carried by this plan
|
|
features JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
|
|
-- Lifecycle
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
available_from TIMESTAMPTZ,
|
|
available_until TIMESTAMPTZ,
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
deleted_at TIMESTAMPTZ,
|
|
|
|
CONSTRAINT uq_plans_tenant_code UNIQUE (tenant_id, code)
|
|
);
|
|
|
|
CREATE INDEX idx_plans_tenant_active ON plans(tenant_id) WHERE is_active = TRUE AND deleted_at IS NULL;
|
|
|
|
CREATE TRIGGER tg_plans_updated_at
|
|
BEFORE UPDATE ON plans
|
|
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- user_plans — active subscriptions
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE user_plans (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
plan_id UUID NOT NULL REFERENCES plans(id),
|
|
|
|
-- Snapshot at purchase time
|
|
plan_code TEXT NOT NULL,
|
|
plan_name TEXT NOT NULL,
|
|
price_minor_paid BIGINT NOT NULL,
|
|
currency TEXT NOT NULL,
|
|
|
|
-- Quota state
|
|
initial_seconds_charge INT NOT NULL,
|
|
remain_charge_sec INT NOT NULL,
|
|
added_charge_from_past_plan INT NOT NULL DEFAULT 0,
|
|
monthly_renders_used INT NOT NULL DEFAULT 0,
|
|
monthly_renders_reset_at TIMESTAMPTZ,
|
|
|
|
-- Lifecycle
|
|
register_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
starts_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
cancelled_at TIMESTAMPTZ,
|
|
cancel_reason TEXT,
|
|
auto_renew BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
-- Reference to payment
|
|
payment_id UUID,
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_user_plans_active ON user_plans(user_id) WHERE cancelled_at IS NULL;
|
|
CREATE INDEX idx_user_plans_expire ON user_plans(expires_at) WHERE cancelled_at IS NULL;
|
|
|
|
CREATE TRIGGER tg_user_plans_updated_at
|
|
BEFORE UPDATE ON user_plans
|
|
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- discounts — coupons / affiliate codes
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE discounts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
|
|
name TEXT NOT NULL,
|
|
code CITEXT NOT NULL,
|
|
kind discount_kind NOT NULL,
|
|
value NUMERIC(14,2) NOT NULL, -- percent or amount
|
|
|
|
-- Affiliate split
|
|
owner_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
owner_profit_percentage NUMERIC(5,2) NOT NULL DEFAULT 0,
|
|
only_owner BOOLEAN NOT NULL DEFAULT FALSE, -- usable only by owner
|
|
|
|
-- Constraints
|
|
max_use_count INT, -- NULL = unlimited
|
|
used_count INT NOT NULL DEFAULT 0,
|
|
min_purchase_minor BIGINT NOT NULL DEFAULT 0,
|
|
applies_to_plan_ids UUID[],
|
|
|
|
starts_at TIMESTAMPTZ,
|
|
expires_at TIMESTAMPTZ,
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
CONSTRAINT uq_discounts_tenant_code UNIQUE (tenant_id, code)
|
|
);
|
|
|
|
CREATE INDEX idx_discounts_active ON discounts(tenant_id) WHERE is_active = TRUE;
|
|
CREATE INDEX idx_discounts_owner ON discounts(owner_user_id) WHERE owner_user_id IS NOT NULL;
|
|
|
|
CREATE TRIGGER tg_discounts_updated_at
|
|
BEFORE UPDATE ON discounts
|
|
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- used_discounts — usage log per user
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE used_discounts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
discount_id UUID NOT NULL REFERENCES discounts(id) ON DELETE RESTRICT,
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
payment_id UUID,
|
|
code CITEXT NOT NULL,
|
|
amount_discounted_minor BIGINT NOT NULL,
|
|
use_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_used_discounts_user ON used_discounts(user_id, use_date DESC);
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- payments
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE payments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
|
|
|
gateway payment_gateway NOT NULL,
|
|
status payment_status NOT NULL DEFAULT 'Pending',
|
|
action payment_action NOT NULL,
|
|
|
|
-- Amounts
|
|
amount_minor BIGINT NOT NULL,
|
|
currency TEXT NOT NULL DEFAULT 'IRR',
|
|
balance_reducer_minor BIGINT NOT NULL DEFAULT 0, -- portion paid from balance
|
|
discount_value_minor BIGINT NOT NULL DEFAULT 0,
|
|
|
|
-- Gateway refs
|
|
gateway_token TEXT,
|
|
gateway_order_id TEXT,
|
|
gateway_track_id TEXT,
|
|
gateway_response JSONB,
|
|
card_last4 TEXT,
|
|
card_hash TEXT,
|
|
|
|
-- Description
|
|
title TEXT,
|
|
description TEXT,
|
|
|
|
-- Affiliate
|
|
owner_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
affiliate_profit_minor BIGINT NOT NULL DEFAULT 0,
|
|
after_pay_score NUMERIC(8,2) NOT NULL DEFAULT 0,
|
|
|
|
-- Polymorphic product reference
|
|
used_discount_id UUID REFERENCES used_discounts(id),
|
|
plan_id UUID REFERENCES plans(id),
|
|
user_project_id UUID, -- FK added later if user_projects keeps existing
|
|
render_job_id UUID,
|
|
product_id UUID,
|
|
|
|
-- Lifecycle
|
|
confirmed_at TIMESTAMPTZ,
|
|
failed_at TIMESTAMPTZ,
|
|
failure_reason TEXT,
|
|
refunded_at TIMESTAMPTZ,
|
|
refund_amount_minor BIGINT,
|
|
refund_reason TEXT,
|
|
|
|
-- Checkout/receipt
|
|
checkouted BOOLEAN NOT NULL DEFAULT FALSE,
|
|
checkout_date TIMESTAMPTZ,
|
|
checkout_recipe TEXT,
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_payments_user ON payments(user_id, created_at DESC);
|
|
CREATE INDEX idx_payments_tenant ON payments(tenant_id, created_at DESC);
|
|
CREATE INDEX idx_payments_status ON payments(status) WHERE status IN ('Pending','Failed');
|
|
CREATE INDEX idx_payments_track ON payments(gateway, gateway_track_id) WHERE gateway_track_id IS NOT NULL;
|
|
|
|
CREATE TRIGGER tg_payments_updated_at
|
|
BEFORE UPDATE ON payments
|
|
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
|
|
|
-- Now wire up the FKs that were dangling
|
|
ALTER TABLE used_discounts
|
|
ADD CONSTRAINT fk_used_disc_payment
|
|
FOREIGN KEY (payment_id) REFERENCES payments(id) ON DELETE SET NULL;
|
|
|
|
ALTER TABLE user_plans
|
|
ADD CONSTRAINT fk_user_plans_payment
|
|
FOREIGN KEY (payment_id) REFERENCES payments(id) ON DELETE SET NULL;
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- checkouts — receipts / affiliate payouts log
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE checkouts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
affiliate_owner_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
|
|
title TEXT,
|
|
description TEXT,
|
|
amount_minor BIGINT NOT NULL,
|
|
currency TEXT NOT NULL DEFAULT 'IRR',
|
|
card_number_last4 TEXT,
|
|
|
|
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
paid_at TIMESTAMPTZ,
|
|
recipe TEXT,
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_checkouts_user ON checkouts(user_id, requested_at DESC);
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- user_projects — freelance custom-template requests
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TYPE user_project_status AS ENUM (
|
|
'Draft','Submitted','Quoted','InProgress','Review','Completed','Cancelled'
|
|
);
|
|
|
|
CREATE TABLE user_projects (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
|
|
title TEXT,
|
|
description TEXT,
|
|
graphist_description TEXT,
|
|
attachment_path TEXT,
|
|
|
|
price_minor BIGINT,
|
|
user_suggested_price_minor BIGINT,
|
|
status user_project_status NOT NULL DEFAULT 'Draft',
|
|
|
|
user_max_requested_day_to_create INT,
|
|
user_graphist_max_requested_day_to_create INT,
|
|
|
|
created_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
end_time TIMESTAMPTZ,
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_user_projects_user ON user_projects(user_id, status);
|
|
|
|
CREATE TRIGGER tg_user_projects_updated_at
|
|
BEFORE UPDATE ON user_projects
|
|
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
|
|
|
ALTER TABLE payments
|
|
ADD CONSTRAINT fk_payments_user_project
|
|
FOREIGN KEY (user_project_id) REFERENCES user_projects(id) ON DELETE SET NULL;
|