template-saas-database-v2/ddl/schemas/commissions/03-functions.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

229 lines
7.0 KiB
PL/PgSQL

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