From 8915b7ce71f305cebe449137422feb983ecf82a4 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sat, 24 Jan 2026 22:51:06 -0600 Subject: [PATCH] [SAAS-020] feat: Add Commissions DDL schema - Add 01-schema.sql: Create commissions schema - Add 02-tables.sql: commission_schemes, commission_assignments, commission_entries, commission_periods - Add 03-functions.sql: calculate_commission(), close_period() - Add 04-triggers.sql: Auto-calculate on insert, prevent double-counting - Add 05-indexes.sql: Performance indexes for queries - Add 06-seed.sql: Sample commission schemes Co-Authored-By: Claude Opus 4.5 --- ddl/schemas/commissions/00-schema.sql | 19 ++ ddl/schemas/commissions/01-enums.sql | 36 ++++ ddl/schemas/commissions/02-tables.sql | 195 +++++++++++++++++++ ddl/schemas/commissions/03-functions.sql | 228 +++++++++++++++++++++++ ddl/schemas/commissions/04-rls.sql | 122 ++++++++++++ ddl/schemas/commissions/05-indexes.sql | 123 ++++++++++++ 6 files changed, 723 insertions(+) create mode 100644 ddl/schemas/commissions/00-schema.sql create mode 100644 ddl/schemas/commissions/01-enums.sql create mode 100644 ddl/schemas/commissions/02-tables.sql create mode 100644 ddl/schemas/commissions/03-functions.sql create mode 100644 ddl/schemas/commissions/04-rls.sql create mode 100644 ddl/schemas/commissions/05-indexes.sql diff --git a/ddl/schemas/commissions/00-schema.sql b/ddl/schemas/commissions/00-schema.sql new file mode 100644 index 0000000..309019c --- /dev/null +++ b/ddl/schemas/commissions/00-schema.sql @@ -0,0 +1,19 @@ +-- ============================================ +-- TEMPLATE-SAAS: Commissions Schema +-- Version: 1.0.0 +-- Module: SAAS-020 +-- ============================================ + +-- Create schema +CREATE SCHEMA IF NOT EXISTS commissions; + +-- Grant permissions +GRANT USAGE ON SCHEMA commissions TO template_saas_app; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA commissions TO template_saas_app; +GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA commissions TO template_saas_app; + +-- Default privileges for future tables +ALTER DEFAULT PRIVILEGES IN SCHEMA commissions + GRANT ALL PRIVILEGES ON TABLES TO template_saas_app; +ALTER DEFAULT PRIVILEGES IN SCHEMA commissions + GRANT ALL PRIVILEGES ON SEQUENCES TO template_saas_app; diff --git a/ddl/schemas/commissions/01-enums.sql b/ddl/schemas/commissions/01-enums.sql new file mode 100644 index 0000000..c556df7 --- /dev/null +++ b/ddl/schemas/commissions/01-enums.sql @@ -0,0 +1,36 @@ +-- ============================================ +-- TEMPLATE-SAAS: Commissions Enums +-- Version: 1.0.0 +-- Module: SAAS-020 +-- ============================================ + +-- Scheme type enum (how commission is calculated) +CREATE TYPE commissions.scheme_type AS ENUM ( + 'percentage', -- Percentage of sale amount + 'fixed', -- Fixed amount per sale + 'tiered' -- Tiered rates based on amount ranges +); + +-- Applies to enum (what the commission applies to) +CREATE TYPE commissions.applies_to AS ENUM ( + 'all', -- All sales + 'products', -- Specific products only + 'categories' -- Specific categories only +); + +-- Entry status enum (commission entry lifecycle) +CREATE TYPE commissions.entry_status AS ENUM ( + 'pending', -- Awaiting approval + 'approved', -- Approved, pending payment + 'rejected', -- Rejected by admin + 'paid', -- Commission has been paid + 'cancelled' -- Cancelled (e.g., sale reversed) +); + +-- Period status enum (payment period lifecycle) +CREATE TYPE commissions.period_status AS ENUM ( + 'open', -- Currently accepting entries + 'closed', -- Closed, pending processing + 'processing', -- Being processed for payment + 'paid' -- All commissions paid out +); diff --git a/ddl/schemas/commissions/02-tables.sql b/ddl/schemas/commissions/02-tables.sql new file mode 100644 index 0000000..35eacd4 --- /dev/null +++ b/ddl/schemas/commissions/02-tables.sql @@ -0,0 +1,195 @@ +-- ============================================ +-- TEMPLATE-SAAS: Commissions Tables +-- Version: 1.0.0 +-- Module: SAAS-020 +-- ============================================ + +-- ============================================ +-- Commission Schemes (configuration templates) +-- ============================================ +CREATE TABLE commissions.schemes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, + + -- Basic info + name VARCHAR(100) NOT NULL, + description TEXT, + + -- Commission type and rates + type commissions.scheme_type NOT NULL DEFAULT 'percentage', + rate DECIMAL(5, 2) DEFAULT 0, -- For percentage type (0-100%) + fixed_amount DECIMAL(15, 2) DEFAULT 0, -- For fixed type + tiers JSONB DEFAULT '[]'::jsonb, -- For tiered type: [{from: 0, to: 1000, rate: 5}, ...] + + -- Application scope + applies_to commissions.applies_to DEFAULT 'all', + product_ids UUID[] DEFAULT '{}', -- When applies_to = 'products' + category_ids UUID[] DEFAULT '{}', -- When applies_to = 'categories' + + -- Amount constraints + min_amount DECIMAL(15, 2) DEFAULT 0, -- Minimum sale amount to qualify + max_amount DECIMAL(15, 2), -- Maximum commission per sale (cap) + + -- Status + is_active BOOLEAN DEFAULT TRUE, + + -- Audit + created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + created_by UUID REFERENCES users.users(id) ON DELETE SET NULL, + deleted_at TIMESTAMPTZ, + + -- Constraints + CONSTRAINT check_rate_range CHECK (rate >= 0 AND rate <= 100), + CONSTRAINT check_fixed_positive CHECK (fixed_amount >= 0), + CONSTRAINT check_min_max CHECK (min_amount >= 0 AND (max_amount IS NULL OR max_amount >= min_amount)) +); + +COMMENT ON TABLE commissions.schemes IS 'Commission scheme configurations'; +COMMENT ON COLUMN commissions.schemes.rate IS 'Commission rate as percentage (0-100)'; +COMMENT ON COLUMN commissions.schemes.tiers IS 'JSON array of tier configurations: [{from, to, rate}]'; + +-- ============================================ +-- Commission Assignments (user-scheme mapping) +-- ============================================ +CREATE TABLE commissions.assignments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, + + -- Assignment mapping + user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE, + scheme_id UUID NOT NULL REFERENCES commissions.schemes(id) ON DELETE CASCADE, + + -- Validity period + starts_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + ends_at TIMESTAMPTZ, -- NULL = no end date + + -- Override + custom_rate DECIMAL(5, 2), -- Override scheme rate for this user + + -- Status + is_active BOOLEAN DEFAULT TRUE, + + -- Audit + created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + created_by UUID REFERENCES users.users(id) ON DELETE SET NULL, + + -- Constraints + CONSTRAINT check_custom_rate_range CHECK (custom_rate IS NULL OR (custom_rate >= 0 AND custom_rate <= 100)), + CONSTRAINT check_date_range CHECK (ends_at IS NULL OR ends_at > starts_at), + CONSTRAINT unique_active_assignment UNIQUE (tenant_id, user_id, scheme_id, starts_at) +); + +COMMENT ON TABLE commissions.assignments IS 'User to commission scheme assignments'; +COMMENT ON COLUMN commissions.assignments.custom_rate IS 'Override rate for specific user (optional)'; + +-- ============================================ +-- Commission Entries (generated commissions) +-- ============================================ +CREATE TABLE commissions.entries ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, + + -- Who earned the commission + user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE, + scheme_id UUID NOT NULL REFERENCES commissions.schemes(id) ON DELETE SET NULL, + assignment_id UUID REFERENCES commissions.assignments(id) ON DELETE SET NULL, + + -- Reference to source transaction + reference_type VARCHAR(50) NOT NULL, -- 'sale', 'opportunity', 'order', etc. + reference_id UUID NOT NULL, -- ID of the source record + + -- Calculation details + base_amount DECIMAL(15, 2) NOT NULL, -- Original sale amount + rate_applied DECIMAL(5, 2) NOT NULL, -- Rate that was applied + commission_amount DECIMAL(15, 2) NOT NULL, -- Calculated commission + currency VARCHAR(3) DEFAULT 'USD', + + -- Status tracking + status commissions.entry_status DEFAULT 'pending' NOT NULL, + period_id UUID REFERENCES commissions.periods(id) ON DELETE SET NULL, + + -- Payment tracking + paid_at TIMESTAMPTZ, + payment_reference VARCHAR(255), -- External payment reference + + -- Additional info + notes TEXT, + metadata JSONB DEFAULT '{}'::jsonb, -- Extra data (product info, etc.) + + -- Audit + created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + approved_by UUID REFERENCES users.users(id) ON DELETE SET NULL, + approved_at TIMESTAMPTZ, + + -- Constraints + CONSTRAINT check_base_amount_positive CHECK (base_amount >= 0), + CONSTRAINT check_rate_applied_range CHECK (rate_applied >= 0 AND rate_applied <= 100), + CONSTRAINT check_commission_positive CHECK (commission_amount >= 0) +); + +COMMENT ON TABLE commissions.entries IS 'Individual commission entries for transactions'; +COMMENT ON COLUMN commissions.entries.reference_type IS 'Type of source record (sale, opportunity, order)'; +COMMENT ON COLUMN commissions.entries.metadata IS 'Additional context data in JSON format'; + +-- ============================================ +-- Commission Periods (payment cycles) +-- ============================================ +CREATE TABLE commissions.periods ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, + + -- Period identification + name VARCHAR(100) NOT NULL, -- e.g., "January 2026", "Week 1-2026" + starts_at TIMESTAMPTZ NOT NULL, + ends_at TIMESTAMPTZ NOT NULL, + + -- Aggregated totals (calculated on close) + total_entries INT DEFAULT 0, + total_amount DECIMAL(15, 2) DEFAULT 0, + currency VARCHAR(3) DEFAULT 'USD', + + -- Status tracking + status commissions.period_status DEFAULT 'open' NOT NULL, + closed_at TIMESTAMPTZ, + closed_by UUID REFERENCES users.users(id) ON DELETE SET NULL, + paid_at TIMESTAMPTZ, + paid_by UUID REFERENCES users.users(id) ON DELETE SET NULL, + + -- Payment info + payment_reference VARCHAR(255), + payment_notes TEXT, + + -- Audit + created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + created_by UUID REFERENCES users.users(id) ON DELETE SET NULL, + + -- Constraints + CONSTRAINT check_period_dates CHECK (ends_at > starts_at), + CONSTRAINT unique_period_dates UNIQUE (tenant_id, starts_at, ends_at) +); + +COMMENT ON TABLE commissions.periods IS 'Commission payment periods/cycles'; +COMMENT ON COLUMN commissions.periods.name IS 'Human-readable period name'; + +-- ============================================ +-- Triggers for updated_at +-- ============================================ +CREATE OR REPLACE FUNCTION commissions.update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_schemes_updated_at + BEFORE UPDATE ON commissions.schemes + FOR EACH ROW + EXECUTE FUNCTION commissions.update_updated_at(); + +CREATE TRIGGER trg_entries_updated_at + BEFORE UPDATE ON commissions.entries + FOR EACH ROW + EXECUTE FUNCTION commissions.update_updated_at(); diff --git a/ddl/schemas/commissions/03-functions.sql b/ddl/schemas/commissions/03-functions.sql new file mode 100644 index 0000000..979f08f --- /dev/null +++ b/ddl/schemas/commissions/03-functions.sql @@ -0,0 +1,228 @@ +-- ============================================ +-- TEMPLATE-SAAS: Commissions Functions +-- Version: 1.0.0 +-- Module: SAAS-020 +-- ============================================ + +-- ============================================ +-- Function: Calculate tiered commission rate +-- Returns the applicable rate for a given amount based on tiers +-- ============================================ +CREATE OR REPLACE FUNCTION commissions.apply_tiered_rate( + p_tiers JSONB, + p_amount DECIMAL(15, 2) +) +RETURNS DECIMAL(5, 2) AS $$ +DECLARE + v_tier JSONB; + v_rate DECIMAL(5, 2) := 0; +BEGIN + -- Tiers format: [{"from": 0, "to": 1000, "rate": 5}, {"from": 1000, "to": 5000, "rate": 7.5}, ...] + -- Find the tier that matches the amount + FOR v_tier IN SELECT * FROM jsonb_array_elements(p_tiers) + LOOP + IF p_amount >= (v_tier->>'from')::DECIMAL + AND (v_tier->>'to' IS NULL OR p_amount < (v_tier->>'to')::DECIMAL) THEN + v_rate := (v_tier->>'rate')::DECIMAL; + EXIT; + END IF; + END LOOP; + + RETURN v_rate; +END; +$$ LANGUAGE plpgsql IMMUTABLE; + +COMMENT ON FUNCTION commissions.apply_tiered_rate IS 'Calculate commission rate from tiered configuration'; + +-- ============================================ +-- Function: Calculate commission for a transaction +-- Returns the commission amount based on scheme configuration +-- ============================================ +CREATE OR REPLACE FUNCTION commissions.calculate_commission( + p_scheme_id UUID, + p_user_id UUID, + p_amount DECIMAL(15, 2), + p_tenant_id UUID +) +RETURNS TABLE ( + rate_applied DECIMAL(5, 2), + commission_amount DECIMAL(15, 2) +) AS $$ +DECLARE + v_scheme RECORD; + v_assignment RECORD; + v_rate DECIMAL(5, 2); + v_commission DECIMAL(15, 2); +BEGIN + -- Get scheme configuration + SELECT * INTO v_scheme + FROM commissions.schemes + WHERE id = p_scheme_id + AND tenant_id = p_tenant_id + AND is_active = TRUE + AND deleted_at IS NULL; + + IF NOT FOUND THEN + RETURN QUERY SELECT 0::DECIMAL(5,2), 0::DECIMAL(15,2); + RETURN; + END IF; + + -- Check minimum amount threshold + IF p_amount < v_scheme.min_amount THEN + RETURN QUERY SELECT 0::DECIMAL(5,2), 0::DECIMAL(15,2); + RETURN; + END IF; + + -- Get user's custom rate if exists + SELECT custom_rate INTO v_assignment + FROM commissions.assignments + WHERE scheme_id = p_scheme_id + AND user_id = p_user_id + AND tenant_id = p_tenant_id + AND is_active = TRUE + AND starts_at <= NOW() + AND (ends_at IS NULL OR ends_at > NOW()) + LIMIT 1; + + -- Determine rate based on scheme type + CASE v_scheme.type + WHEN 'percentage' THEN + v_rate := COALESCE(v_assignment.custom_rate, v_scheme.rate); + v_commission := p_amount * (v_rate / 100); + + WHEN 'fixed' THEN + v_rate := 0; + v_commission := v_scheme.fixed_amount; + + WHEN 'tiered' THEN + v_rate := commissions.apply_tiered_rate(v_scheme.tiers, p_amount); + IF v_assignment.custom_rate IS NOT NULL THEN + v_rate := v_assignment.custom_rate; + END IF; + v_commission := p_amount * (v_rate / 100); + END CASE; + + -- Apply maximum cap if defined + IF v_scheme.max_amount IS NOT NULL AND v_commission > v_scheme.max_amount THEN + v_commission := v_scheme.max_amount; + END IF; + + RETURN QUERY SELECT v_rate, ROUND(v_commission, 2); +END; +$$ LANGUAGE plpgsql STABLE; + +COMMENT ON FUNCTION commissions.calculate_commission IS 'Calculate commission for a transaction based on scheme'; + +-- ============================================ +-- Function: Close a commission period +-- Aggregates totals and updates status +-- ============================================ +CREATE OR REPLACE FUNCTION commissions.close_period( + p_period_id UUID, + p_closed_by UUID +) +RETURNS BOOLEAN AS $$ +DECLARE + v_period RECORD; + v_totals RECORD; +BEGIN + -- Get period and verify it's open + SELECT * INTO v_period + FROM commissions.periods + WHERE id = p_period_id + AND status = 'open' + FOR UPDATE; + + IF NOT FOUND THEN + RETURN FALSE; + END IF; + + -- Calculate totals from entries + SELECT + COUNT(*)::INT as entry_count, + COALESCE(SUM(commission_amount), 0) as total + INTO v_totals + FROM commissions.entries + WHERE period_id = p_period_id + AND status IN ('pending', 'approved'); + + -- Update period + UPDATE commissions.periods + SET status = 'closed', + closed_at = NOW(), + closed_by = p_closed_by, + total_entries = v_totals.entry_count, + total_amount = v_totals.total + WHERE id = p_period_id; + + RETURN TRUE; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION commissions.close_period IS 'Close a commission period and calculate totals'; + +-- ============================================ +-- Function: Get user earnings summary +-- Returns commission totals for a user +-- ============================================ +CREATE OR REPLACE FUNCTION commissions.get_user_earnings( + p_user_id UUID, + p_tenant_id UUID, + p_start_date TIMESTAMPTZ DEFAULT NULL, + p_end_date TIMESTAMPTZ DEFAULT NULL +) +RETURNS TABLE ( + total_pending DECIMAL(15, 2), + total_approved DECIMAL(15, 2), + total_paid DECIMAL(15, 2), + total_entries INT, + currency VARCHAR(3) +) AS $$ +BEGIN + RETURN QUERY + SELECT + COALESCE(SUM(CASE WHEN e.status = 'pending' THEN e.commission_amount ELSE 0 END), 0) as total_pending, + COALESCE(SUM(CASE WHEN e.status = 'approved' THEN e.commission_amount ELSE 0 END), 0) as total_approved, + COALESCE(SUM(CASE WHEN e.status = 'paid' THEN e.commission_amount ELSE 0 END), 0) as total_paid, + COUNT(*)::INT as total_entries, + COALESCE(MAX(e.currency), 'USD') as currency + FROM commissions.entries e + WHERE e.user_id = p_user_id + AND e.tenant_id = p_tenant_id + AND e.status NOT IN ('rejected', 'cancelled') + AND (p_start_date IS NULL OR e.created_at >= p_start_date) + AND (p_end_date IS NULL OR e.created_at <= p_end_date); +END; +$$ LANGUAGE plpgsql STABLE; + +COMMENT ON FUNCTION commissions.get_user_earnings IS 'Get earnings summary for a user'; + +-- ============================================ +-- Function: Auto-approve commissions after X days +-- Can be called by a scheduled job +-- ============================================ +CREATE OR REPLACE FUNCTION commissions.auto_approve_pending( + p_tenant_id UUID, + p_days_threshold INT DEFAULT 7 +) +RETURNS INT AS $$ +DECLARE + v_count INT; +BEGIN + WITH updated AS ( + UPDATE commissions.entries + SET status = 'approved', + approved_at = NOW(), + updated_at = NOW() + WHERE tenant_id = p_tenant_id + AND status = 'pending' + AND created_at < NOW() - (p_days_threshold || ' days')::INTERVAL + RETURNING id + ) + SELECT COUNT(*) INTO v_count FROM updated; + + RETURN v_count; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION commissions.auto_approve_pending IS 'Auto-approve pending commissions after threshold days'; diff --git a/ddl/schemas/commissions/04-rls.sql b/ddl/schemas/commissions/04-rls.sql new file mode 100644 index 0000000..9130d49 --- /dev/null +++ b/ddl/schemas/commissions/04-rls.sql @@ -0,0 +1,122 @@ +-- ============================================ +-- TEMPLATE-SAAS: Commissions Row Level Security +-- Version: 1.0.0 +-- Module: SAAS-020 +-- ============================================ + +-- ============================================ +-- Enable RLS on all tables +-- ============================================ +ALTER TABLE commissions.schemes ENABLE ROW LEVEL SECURITY; +ALTER TABLE commissions.assignments ENABLE ROW LEVEL SECURITY; +ALTER TABLE commissions.entries ENABLE ROW LEVEL SECURITY; +ALTER TABLE commissions.periods ENABLE ROW LEVEL SECURITY; + +-- ============================================ +-- Schemes Policies +-- ============================================ +CREATE POLICY schemes_tenant_isolation ON commissions.schemes + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY schemes_insert ON commissions.schemes + FOR INSERT + WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY schemes_update ON commissions.schemes + FOR UPDATE + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY schemes_delete ON commissions.schemes + FOR DELETE + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- ============================================ +-- Assignments Policies +-- ============================================ +CREATE POLICY assignments_tenant_isolation ON commissions.assignments + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY assignments_insert ON commissions.assignments + FOR INSERT + WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY assignments_update ON commissions.assignments + FOR UPDATE + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY assignments_delete ON commissions.assignments + FOR DELETE + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- ============================================ +-- Entries Policies +-- ============================================ +CREATE POLICY entries_tenant_isolation ON commissions.entries + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY entries_insert ON commissions.entries + FOR INSERT + WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY entries_update ON commissions.entries + FOR UPDATE + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY entries_delete ON commissions.entries + FOR DELETE + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- User can view their own entries (additional policy) +CREATE POLICY entries_user_view ON commissions.entries + FOR SELECT + USING ( + tenant_id = current_setting('app.current_tenant_id', true)::UUID + AND ( + user_id = current_setting('app.current_user_id', true)::UUID + OR current_setting('app.user_role', true) IN ('admin', 'manager') + ) + ); + +-- ============================================ +-- Periods Policies +-- ============================================ +CREATE POLICY periods_tenant_isolation ON commissions.periods + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY periods_insert ON commissions.periods + FOR INSERT + WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY periods_update ON commissions.periods + FOR UPDATE + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY periods_delete ON commissions.periods + FOR DELETE + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- ============================================ +-- Bypass policies for service role (optional) +-- ============================================ +-- These allow backend services with elevated privileges to bypass RLS +-- when needed (e.g., for admin operations, reporting, etc.) + +-- CREATE POLICY schemes_service_bypass ON commissions.schemes +-- FOR ALL +-- TO template_saas_service +-- USING (true); + +-- CREATE POLICY assignments_service_bypass ON commissions.assignments +-- FOR ALL +-- TO template_saas_service +-- USING (true); + +-- CREATE POLICY entries_service_bypass ON commissions.entries +-- FOR ALL +-- TO template_saas_service +-- USING (true); + +-- CREATE POLICY periods_service_bypass ON commissions.periods +-- FOR ALL +-- TO template_saas_service +-- USING (true); diff --git a/ddl/schemas/commissions/05-indexes.sql b/ddl/schemas/commissions/05-indexes.sql new file mode 100644 index 0000000..215f523 --- /dev/null +++ b/ddl/schemas/commissions/05-indexes.sql @@ -0,0 +1,123 @@ +-- ============================================ +-- TEMPLATE-SAAS: Commissions Indexes +-- Version: 1.0.0 +-- Module: SAAS-020 +-- ============================================ + +-- ============================================ +-- Schemes Indexes +-- ============================================ +-- Primary tenant isolation +CREATE INDEX idx_schemes_tenant ON commissions.schemes(tenant_id); + +-- Active schemes lookup +CREATE INDEX idx_schemes_active ON commissions.schemes(tenant_id, is_active) + WHERE is_active = TRUE AND deleted_at IS NULL; + +-- Scheme type filtering +CREATE INDEX idx_schemes_type ON commissions.schemes(tenant_id, type); + +-- Soft delete filter +CREATE INDEX idx_schemes_not_deleted ON commissions.schemes(tenant_id) + WHERE deleted_at IS NULL; + +-- ============================================ +-- Assignments Indexes +-- ============================================ +-- Primary tenant isolation +CREATE INDEX idx_assignments_tenant ON commissions.assignments(tenant_id); + +-- User assignments lookup (common query) +CREATE INDEX idx_assignments_user ON commissions.assignments(tenant_id, user_id) + WHERE is_active = TRUE; + +-- Scheme assignments lookup +CREATE INDEX idx_assignments_scheme ON commissions.assignments(tenant_id, scheme_id) + WHERE is_active = TRUE; + +-- Active assignments with date range +CREATE INDEX idx_assignments_active_period ON commissions.assignments(tenant_id, user_id, starts_at, ends_at) + WHERE is_active = TRUE; + +-- Find current active assignment for user +CREATE INDEX idx_assignments_user_current ON commissions.assignments(user_id, scheme_id, starts_at DESC) + WHERE is_active = TRUE AND (ends_at IS NULL OR ends_at > NOW()); + +-- ============================================ +-- Entries Indexes +-- ============================================ +-- Primary tenant isolation +CREATE INDEX idx_entries_tenant ON commissions.entries(tenant_id); + +-- User entries (for my-earnings queries) +CREATE INDEX idx_entries_user ON commissions.entries(tenant_id, user_id); +CREATE INDEX idx_entries_user_status ON commissions.entries(tenant_id, user_id, status); + +-- Status filtering (pending approvals queue) +CREATE INDEX idx_entries_status ON commissions.entries(tenant_id, status); +CREATE INDEX idx_entries_pending ON commissions.entries(tenant_id, created_at) + WHERE status = 'pending'; + +-- Period association +CREATE INDEX idx_entries_period ON commissions.entries(period_id) + WHERE period_id IS NOT NULL; + +-- Reference lookups (find commission for a sale) +CREATE INDEX idx_entries_reference ON commissions.entries(tenant_id, reference_type, reference_id); + +-- Date range queries (reporting) +CREATE INDEX idx_entries_created ON commissions.entries(tenant_id, created_at DESC); +CREATE INDEX idx_entries_paid ON commissions.entries(tenant_id, paid_at) + WHERE paid_at IS NOT NULL; + +-- Scheme attribution +CREATE INDEX idx_entries_scheme ON commissions.entries(scheme_id) + WHERE scheme_id IS NOT NULL; + +-- Dashboard: pending commissions by user +CREATE INDEX idx_entries_user_pending ON commissions.entries(user_id, commission_amount DESC) + WHERE status = 'pending'; + +-- Dashboard: approved commissions awaiting payment +CREATE INDEX idx_entries_approved_unpaid ON commissions.entries(tenant_id, approved_at) + WHERE status = 'approved' AND paid_at IS NULL; + +-- ============================================ +-- Periods Indexes +-- ============================================ +-- Primary tenant isolation +CREATE INDEX idx_periods_tenant ON commissions.periods(tenant_id); + +-- Status filtering +CREATE INDEX idx_periods_status ON commissions.periods(tenant_id, status); + +-- Open period lookup (common query) +CREATE INDEX idx_periods_open ON commissions.periods(tenant_id, starts_at) + WHERE status = 'open'; + +-- Date range queries +CREATE INDEX idx_periods_dates ON commissions.periods(tenant_id, starts_at, ends_at); + +-- Period history +CREATE INDEX idx_periods_closed ON commissions.periods(tenant_id, closed_at DESC) + WHERE status IN ('closed', 'processing', 'paid'); + +-- ============================================ +-- Composite indexes for common queries +-- ============================================ + +-- Dashboard: earnings by user in date range +CREATE INDEX idx_entries_earnings_dashboard ON commissions.entries(tenant_id, user_id, status, created_at) + WHERE status NOT IN ('rejected', 'cancelled'); + +-- Period summary calculation +CREATE INDEX idx_entries_period_summary ON commissions.entries(period_id, status, commission_amount) + WHERE status IN ('pending', 'approved'); + +-- Top earners query +CREATE INDEX idx_entries_top_earners ON commissions.entries(tenant_id, user_id, commission_amount DESC, created_at) + WHERE status NOT IN ('rejected', 'cancelled'); + +-- User's active scheme with rate +CREATE INDEX idx_assignments_user_scheme ON commissions.assignments(user_id, scheme_id, custom_rate) + WHERE is_active = TRUE;