feat: V2 microservices stack — backend services, gateway, JWT auth
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>
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
-- =====================================================================
|
||||
-- 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;
|
||||
Reference in New Issue
Block a user