- 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>
229 lines
7.0 KiB
PL/PgSQL
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';
|