-- ===================================================================== -- 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;