template-saas-database-v2/ddl/schemas/commissions/02-tables.sql
Adrian Flores Cortes 8915b7ce71 [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 <noreply@anthropic.com>
2026-01-24 22:51:06 -06:00

196 lines
7.6 KiB
PL/PgSQL

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