Files
cfw-autumn/server/src/internal/balances/utils/sql/deductFromMainBalance.sql
2026-03-09 11:15:29 +00:00

277 lines
11 KiB
PL/PgSQL

-- Helper function: Perform deduction calculation for a single entitlement
-- This handles both entity-scoped and regular balance deductions
DROP FUNCTION IF EXISTS deduct_from_main_balance(jsonb);
CREATE FUNCTION deduct_from_main_balance(params jsonb)
RETURNS TABLE (
deducted numeric,
new_balance numeric,
new_entities jsonb,
new_adjustment numeric,
mutation_logs jsonb
)
LANGUAGE plpgsql
AS $$
DECLARE
-- Extract parameters from JSONB
customer_entitlement_id text := NULLIF(params->>'customer_entitlement_id', '');
current_balance numeric := (params->>'current_balance')::numeric;
current_entities jsonb := COALESCE(params->'current_entities', '{}'::jsonb);
current_adjustment numeric := COALESCE((params->>'current_adjustment')::numeric, 0);
amount_to_deduct numeric := (params->>'amount_to_deduct')::numeric;
credit_cost numeric := (params->>'credit_cost')::numeric;
allow_negative boolean := COALESCE((params->>'allow_negative')::boolean, false);
has_entity_scope boolean := COALESCE((params->>'has_entity_scope')::boolean, false);
target_entity_id text := NULLIF(params->>'target_entity_id', '');
min_balance numeric := CASE
WHEN params->>'min_balance' IS NULL THEN NULL
ELSE (params->>'min_balance')::numeric
END;
alter_granted_balance boolean := COALESCE((params->>'alter_granted_balance')::boolean, false);
overage_behavior_is_allow boolean := COALESCE((params->>'overage_behavior_is_allow')::boolean, false);
max_balance numeric := CASE
WHEN params->>'max_balance' IS NULL THEN NULL
ELSE (params->>'max_balance')::numeric
END;
deducted_amount numeric := 0;
result_balance numeric;
result_entities jsonb;
result_adjustment numeric;
-- Variables for entity deduction
remaining numeric;
entity_key text;
entity_balance numeric;
deduct_amount numeric;
new_balance numeric;
-- Variables for ceiling calculation (negative track)
entity_adjustment numeric;
ceiling numeric;
max_addable numeric;
mutation_logs_json jsonb := '[]'::jsonb;
BEGIN
-- Initialize return values
result_adjustment := current_adjustment;
-- ============================================================================
-- CASE 1: ENTITY-SCOPED - ALL ENTITIES (no specific entity_id)
-- ============================================================================
IF has_entity_scope AND target_entity_id IS NULL THEN
remaining := amount_to_deduct * credit_cost;
result_entities := current_entities;
deducted_amount := 0;
-- Loop through all entities and deduct iteratively (sorted for consistency with Redis)
FOR entity_key IN SELECT jsonb_object_keys(current_entities) ORDER BY 1
LOOP
EXIT WHEN remaining = 0;
entity_balance := COALESCE((result_entities->entity_key->>'balance')::numeric, 0);
-- Calculate deduction respecting allow_negative and min_balance
-- Handle negative amounts (adding credits) differently
IF remaining < 0 THEN
-- Adding credits: apply ceiling if overage_behavior_is_allow is false and max_balance exists
IF NOT overage_behavior_is_allow AND max_balance IS NOT NULL THEN
-- Get entity-level adjustment
entity_adjustment := COALESCE((result_entities->entity_key->>'adjustment')::numeric, 0);
-- Compute ceiling: max_balance + adjustment
ceiling := max_balance + entity_adjustment;
-- Cap addition so balance doesn't exceed ceiling
max_addable := GREATEST(0, ceiling - entity_balance);
-- remaining is negative, so -remaining is the amount to add
-- deduct_amount will be negative (adding to balance)
deduct_amount := -LEAST(-remaining, max_addable);
ELSE
-- No ceiling: deduct the entire negative amount (which adds)
deduct_amount := remaining;
END IF;
ELSIF allow_negative THEN
IF min_balance IS NULL THEN
deduct_amount := remaining;
ELSE
deduct_amount := LEAST(remaining, entity_balance - min_balance);
END IF;
ELSE
deduct_amount := LEAST(entity_balance, remaining);
END IF;
IF deduct_amount != 0 THEN
new_balance := entity_balance - deduct_amount;
result_entities := jsonb_set(
result_entities,
ARRAY[entity_key, 'balance'],
to_jsonb(new_balance)
);
-- If alter_granted_balance is true, update adjustment field to track the change
IF alter_granted_balance THEN
result_entities := jsonb_set(
result_entities,
ARRAY[entity_key, 'adjustment'],
to_jsonb(COALESCE((result_entities->entity_key->>'adjustment')::numeric, 0) - deduct_amount)
);
END IF;
mutation_logs_json := mutation_logs_json || jsonb_build_array(
jsonb_build_object(
'target_type', 'customer_entitlement',
'customer_entitlement_id', customer_entitlement_id,
'rollover_id', NULL,
'entity_id', entity_key,
'credit_cost', credit_cost,
'balance_delta', -deduct_amount,
'adjustment_delta', CASE WHEN alter_granted_balance THEN -deduct_amount ELSE 0 END,
'usage_delta', 0,
'value_delta', deduct_amount / credit_cost
)
);
remaining := remaining - deduct_amount;
deducted_amount := deducted_amount + deduct_amount;
END IF;
END LOOP;
result_balance := current_balance; -- Top-level balance unchanged for entity-scoped
-- ============================================================================
-- CASE 2: ENTITY-SCOPED - SINGLE ENTITY (specific entity_id provided)
-- ============================================================================
ELSIF has_entity_scope AND target_entity_id IS NOT NULL THEN
entity_balance := COALESCE((current_entities->target_entity_id->>'balance')::numeric, 0);
-- Calculate deduction respecting allow_negative and min_balance
-- Handle negative amounts (adding credits) differently
IF amount_to_deduct < 0 THEN
-- Adding credits: apply ceiling if overage_behavior_is_allow is false and max_balance exists
IF NOT overage_behavior_is_allow AND max_balance IS NOT NULL THEN
-- Get entity-level adjustment
entity_adjustment := COALESCE((current_entities->target_entity_id->>'adjustment')::numeric, 0);
-- Compute ceiling: max_balance + adjustment
ceiling := max_balance + entity_adjustment;
-- Cap addition so balance doesn't exceed ceiling
max_addable := GREATEST(0, ceiling - entity_balance);
-- amount_to_deduct is negative, so -amount_to_deduct is the amount to add
-- deducted_amount will be negative (adding to balance)
deducted_amount := -LEAST(-amount_to_deduct * credit_cost, max_addable);
ELSE
-- No ceiling: deduct the entire negative amount (which adds)
deducted_amount := amount_to_deduct * credit_cost;
END IF;
ELSIF allow_negative THEN
IF min_balance IS NULL THEN
deducted_amount := amount_to_deduct * credit_cost;
ELSE
deducted_amount := LEAST(amount_to_deduct * credit_cost, entity_balance - min_balance);
END IF;
ELSE
deducted_amount := LEAST(entity_balance, amount_to_deduct * credit_cost);
END IF;
IF deducted_amount != 0 THEN
new_balance := entity_balance - deducted_amount;
result_entities := jsonb_set(
current_entities,
ARRAY[target_entity_id, 'balance'],
to_jsonb(new_balance)
);
-- If alter_granted_balance is true, update adjustment field to track the change
IF alter_granted_balance THEN
result_entities := jsonb_set(
result_entities,
ARRAY[target_entity_id, 'adjustment'],
to_jsonb(COALESCE((result_entities->target_entity_id->>'adjustment')::numeric, 0) - deducted_amount)
);
END IF;
mutation_logs_json := mutation_logs_json || jsonb_build_array(
jsonb_build_object(
'target_type', 'customer_entitlement',
'customer_entitlement_id', customer_entitlement_id,
'rollover_id', NULL,
'entity_id', target_entity_id,
'credit_cost', credit_cost,
'balance_delta', -deducted_amount,
'adjustment_delta', CASE WHEN alter_granted_balance THEN -deducted_amount ELSE 0 END,
'usage_delta', 0,
'value_delta', deducted_amount / credit_cost
)
);
ELSE
result_entities := current_entities;
END IF;
result_balance := current_balance; -- Top-level balance unchanged for entity-scoped
-- ============================================================================
-- CASE 3: TOP-LEVEL BALANCE (no entity scope)
-- ============================================================================
ELSE
-- Calculate deduction based on allow_negative flag
-- Handle negative amounts (adding credits) differently
IF amount_to_deduct < 0 THEN
-- Adding credits: apply ceiling if overage_behavior_is_allow is false and max_balance exists
IF NOT overage_behavior_is_allow AND max_balance IS NOT NULL THEN
-- Compute ceiling: max_balance + current_adjustment (customer-level)
ceiling := max_balance + current_adjustment;
-- Cap addition so balance doesn't exceed ceiling
max_addable := GREATEST(0, ceiling - current_balance);
-- amount_to_deduct is negative, so -amount_to_deduct is the amount to add
-- deducted_amount will be negative (adding to balance)
deducted_amount := -LEAST(-amount_to_deduct * credit_cost, max_addable);
ELSE
-- No ceiling: deduct the entire negative amount (which adds)
deducted_amount := amount_to_deduct * credit_cost;
END IF;
ELSIF allow_negative THEN
-- Pass 2: Can go negative (respecting min_balance)
IF min_balance IS NULL THEN
deducted_amount := amount_to_deduct * credit_cost;
ELSE
deducted_amount := LEAST(amount_to_deduct * credit_cost, current_balance - min_balance);
END IF;
ELSE
-- Pass 1: Only deduct down to zero
deducted_amount := LEAST(current_balance, amount_to_deduct * credit_cost);
END IF;
result_balance := current_balance - deducted_amount;
result_entities := current_entities; -- Entities unchanged for non-entity-scoped
-- If alter_granted_balance is true, update adjustment field to track the change
IF alter_granted_balance THEN
result_adjustment := result_adjustment - deducted_amount;
END IF;
IF deducted_amount != 0 THEN
mutation_logs_json := mutation_logs_json || jsonb_build_array(
jsonb_build_object(
'target_type', 'customer_entitlement',
'customer_entitlement_id', customer_entitlement_id,
'rollover_id', NULL,
'entity_id', NULL,
'credit_cost', credit_cost,
'balance_delta', -deducted_amount,
'adjustment_delta', CASE WHEN alter_granted_balance THEN -deducted_amount ELSE 0 END,
'usage_delta', 0,
'value_delta', deducted_amount / credit_cost
)
);
END IF;
END IF;
-- Return results
RETURN QUERY
SELECT
deducted_amount,
result_balance,
result_entities,
result_adjustment,
mutation_logs_json;
END;
$$;