260 lines
9.6 KiB
PL/PgSQL
260 lines
9.6 KiB
PL/PgSQL
DROP FUNCTION IF EXISTS unwind_from_lock_receipt(jsonb);
|
|
|
|
CREATE FUNCTION unwind_from_lock_receipt(params jsonb)
|
|
RETURNS TABLE (
|
|
signed_remaining_unwind_value numeric,
|
|
updates jsonb,
|
|
modified_rollover_ids text[],
|
|
mutation_logs jsonb
|
|
)
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
lock_receipt jsonb := params->'lock_receipt';
|
|
receipt_items jsonb := COALESCE(lock_receipt->'items', '[]'::jsonb);
|
|
requested_unwind_value numeric := COALESCE((params->>'unwind_value')::numeric, 0);
|
|
-- Live entitlement IDs: only these may be unwound; anything else is skipped and compensated
|
|
live_cus_ent_ids text[] := CASE
|
|
WHEN params->'cus_ent_ids' IS NULL OR jsonb_typeof(params->'cus_ent_ids') != 'array' THEN NULL
|
|
ELSE ARRAY(SELECT jsonb_array_elements_text(params->'cus_ent_ids'))
|
|
END;
|
|
|
|
item_index integer;
|
|
item jsonb;
|
|
item_target_type text;
|
|
customer_entitlement_id text;
|
|
rollover_id text;
|
|
entity_id text;
|
|
credit_cost numeric;
|
|
|
|
item_value_delta numeric;
|
|
item_value_magnitude numeric;
|
|
unwind_iteration_value numeric;
|
|
credits_to_unwind numeric;
|
|
|
|
inverse_balance_delta numeric;
|
|
inverse_adjustment_delta numeric;
|
|
inverse_usage_delta numeric;
|
|
inverse_value_delta numeric;
|
|
|
|
updated_balance numeric;
|
|
updated_additional_balance numeric;
|
|
updated_adjustment numeric;
|
|
updated_entities jsonb;
|
|
|
|
rows_affected integer;
|
|
|
|
remaining_value numeric := requested_unwind_value;
|
|
skipped_unwind numeric := 0;
|
|
lock_value_sum numeric := 0;
|
|
lock_sign integer := 1;
|
|
|
|
updates_json jsonb := '{}'::jsonb;
|
|
mutation_logs_json jsonb := '[]'::jsonb;
|
|
modified_rollover_ids_array text[] := ARRAY[]::text[];
|
|
BEGIN
|
|
IF requested_unwind_value <= 0 THEN
|
|
RETURN QUERY SELECT 0::numeric, '{}'::jsonb, ARRAY[]::text[], '[]'::jsonb;
|
|
RETURN;
|
|
END IF;
|
|
|
|
IF jsonb_typeof(receipt_items) != 'array' OR jsonb_array_length(receipt_items) = 0 THEN
|
|
RAISE EXCEPTION 'LOCK_RECEIPT_ITEMS_MISSING';
|
|
END IF;
|
|
|
|
-- Compute lock_sign from the sum of value_deltas across all receipt items.
|
|
-- unwind_value is always a positive magnitude; lock_sign tells us the direction
|
|
-- of the original deduction so we can correctly sign any skipped compensation.
|
|
SELECT COALESCE(SUM((item_el->>'value_delta')::numeric), 0)
|
|
INTO lock_value_sum
|
|
FROM jsonb_array_elements(receipt_items) item_el;
|
|
|
|
lock_sign := CASE WHEN lock_value_sum >= 0 THEN 1 ELSE -1 END;
|
|
|
|
FOR item_index IN REVERSE jsonb_array_length(receipt_items) - 1..0
|
|
LOOP
|
|
EXIT WHEN remaining_value <= 0;
|
|
|
|
item := receipt_items->item_index;
|
|
item_target_type := item->>'target_type';
|
|
customer_entitlement_id := NULLIF(item->>'customer_entitlement_id', '');
|
|
rollover_id := NULLIF(item->>'rollover_id', '');
|
|
entity_id := NULLIF(item->>'entity_id', '');
|
|
credit_cost := COALESCE((item->>'credit_cost')::numeric, 1);
|
|
|
|
item_value_delta := COALESCE((item->>'value_delta')::numeric, 0);
|
|
item_value_magnitude := ABS(item_value_delta);
|
|
unwind_iteration_value := LEAST(item_value_magnitude, remaining_value);
|
|
|
|
IF unwind_iteration_value <= 0 THEN
|
|
CONTINUE;
|
|
END IF;
|
|
|
|
credits_to_unwind := unwind_iteration_value * credit_cost;
|
|
|
|
inverse_balance_delta := CASE
|
|
WHEN COALESCE((item->>'balance_delta')::numeric, 0) > 0 THEN -credits_to_unwind
|
|
WHEN COALESCE((item->>'balance_delta')::numeric, 0) < 0 THEN credits_to_unwind
|
|
ELSE 0
|
|
END;
|
|
inverse_adjustment_delta := CASE
|
|
WHEN COALESCE((item->>'adjustment_delta')::numeric, 0) > 0 THEN -credits_to_unwind
|
|
WHEN COALESCE((item->>'adjustment_delta')::numeric, 0) < 0 THEN credits_to_unwind
|
|
ELSE 0
|
|
END;
|
|
inverse_usage_delta := CASE
|
|
WHEN COALESCE((item->>'usage_delta')::numeric, 0) > 0 THEN -credits_to_unwind
|
|
WHEN COALESCE((item->>'usage_delta')::numeric, 0) < 0 THEN credits_to_unwind
|
|
ELSE 0
|
|
END;
|
|
inverse_value_delta := CASE
|
|
WHEN item_value_delta > 0 THEN -unwind_iteration_value
|
|
WHEN item_value_delta < 0 THEN unwind_iteration_value
|
|
ELSE 0
|
|
END;
|
|
|
|
IF item_target_type = 'customer_entitlement' THEN
|
|
IF customer_entitlement_id IS NULL THEN
|
|
RAISE EXCEPTION 'LOCK_CUSTOMER_ENTITLEMENT_ID_MISSING';
|
|
END IF;
|
|
|
|
-- If a live entitlement set was provided, skip any receipt item whose ID is
|
|
-- not in it (e.g. the entitlement belonged to a product that was upgraded
|
|
-- mid-flight and the row still exists in the DB but is no longer active).
|
|
IF live_cus_ent_ids IS NOT NULL AND NOT (customer_entitlement_id = ANY(live_cus_ent_ids)) THEN
|
|
skipped_unwind := skipped_unwind + unwind_iteration_value;
|
|
remaining_value := remaining_value - unwind_iteration_value;
|
|
CONTINUE;
|
|
END IF;
|
|
|
|
-- Reset before the UPDATE so we can detect 0-row matches
|
|
updated_balance := NULL;
|
|
|
|
IF entity_id IS NULL THEN
|
|
UPDATE customer_entitlements ce
|
|
SET
|
|
balance = ce.balance + inverse_balance_delta,
|
|
adjustment = COALESCE(ce.adjustment, 0) + inverse_adjustment_delta
|
|
WHERE ce.id = customer_entitlement_id
|
|
RETURNING
|
|
ce.balance,
|
|
COALESCE(ce.additional_balance, 0),
|
|
COALESCE(ce.adjustment, 0),
|
|
COALESCE(ce.entities, '{}'::jsonb)
|
|
INTO updated_balance, updated_additional_balance, updated_adjustment, updated_entities;
|
|
ELSE
|
|
UPDATE customer_entitlements ce
|
|
SET entities = jsonb_set(
|
|
jsonb_set(
|
|
COALESCE(ce.entities, '{}'::jsonb),
|
|
ARRAY[entity_id, 'balance'],
|
|
to_jsonb(COALESCE((COALESCE(ce.entities, '{}'::jsonb)->entity_id->>'balance')::numeric, 0) + inverse_balance_delta),
|
|
true
|
|
),
|
|
ARRAY[entity_id, 'adjustment'],
|
|
to_jsonb(COALESCE((COALESCE(ce.entities, '{}'::jsonb)->entity_id->>'adjustment')::numeric, 0) + inverse_adjustment_delta),
|
|
true
|
|
)
|
|
WHERE ce.id = customer_entitlement_id
|
|
RETURNING
|
|
ce.balance,
|
|
COALESCE(ce.additional_balance, 0),
|
|
COALESCE(ce.adjustment, 0),
|
|
COALESCE(ce.entities, '{}'::jsonb)
|
|
INTO updated_balance, updated_additional_balance, updated_adjustment, updated_entities;
|
|
END IF;
|
|
|
|
-- Entitlement no longer exists (e.g. product upgraded mid-flight).
|
|
-- Skip this item and accumulate the skipped magnitude so the caller
|
|
-- can compensate against current live entitlements.
|
|
IF updated_balance IS NULL THEN
|
|
skipped_unwind := skipped_unwind + unwind_iteration_value;
|
|
remaining_value := remaining_value - unwind_iteration_value;
|
|
CONTINUE;
|
|
END IF;
|
|
|
|
updates_json := jsonb_set(
|
|
updates_json,
|
|
ARRAY[customer_entitlement_id],
|
|
jsonb_build_object(
|
|
'balance', updated_balance,
|
|
'additional_balance', updated_additional_balance,
|
|
'adjustment', updated_adjustment,
|
|
'entities', updated_entities,
|
|
'deducted', COALESCE((updates_json->customer_entitlement_id->>'deducted')::numeric, 0) + inverse_value_delta,
|
|
'additional_deducted', COALESCE((updates_json->customer_entitlement_id->>'additional_deducted')::numeric, 0)
|
|
),
|
|
true
|
|
);
|
|
ELSIF item_target_type = 'rollover' THEN
|
|
IF rollover_id IS NULL THEN
|
|
RAISE EXCEPTION 'LOCK_ROLLOVER_ID_MISSING';
|
|
END IF;
|
|
|
|
IF entity_id IS NULL THEN
|
|
UPDATE rollovers r
|
|
SET
|
|
balance = r.balance + inverse_balance_delta,
|
|
usage = COALESCE(r.usage, 0) + inverse_usage_delta
|
|
WHERE r.id = rollover_id;
|
|
ELSE
|
|
UPDATE rollovers r
|
|
SET entities = jsonb_set(
|
|
jsonb_set(
|
|
COALESCE(r.entities, '{}'::jsonb),
|
|
ARRAY[entity_id, 'balance'],
|
|
to_jsonb(COALESCE((COALESCE(r.entities, '{}'::jsonb)->entity_id->>'balance')::numeric, 0) + inverse_balance_delta),
|
|
true
|
|
),
|
|
ARRAY[entity_id, 'usage'],
|
|
to_jsonb(COALESCE((COALESCE(r.entities, '{}'::jsonb)->entity_id->>'usage')::numeric, 0) + inverse_usage_delta),
|
|
true
|
|
)
|
|
WHERE r.id = rollover_id;
|
|
END IF;
|
|
|
|
GET DIAGNOSTICS rows_affected = ROW_COUNT;
|
|
|
|
-- Rollover no longer exists (e.g. expired mid-flight). Skip and accumulate.
|
|
IF rows_affected = 0 THEN
|
|
skipped_unwind := skipped_unwind + unwind_iteration_value;
|
|
remaining_value := remaining_value - unwind_iteration_value;
|
|
CONTINUE;
|
|
END IF;
|
|
|
|
modified_rollover_ids_array := array_append(modified_rollover_ids_array, rollover_id);
|
|
ELSE
|
|
RAISE EXCEPTION 'INVALID_LOCK_ITEM_TARGET_TYPE|targetType:%', item_target_type;
|
|
END IF;
|
|
|
|
mutation_logs_json := mutation_logs_json || jsonb_build_array(
|
|
jsonb_build_object(
|
|
'target_type', item_target_type,
|
|
'customer_entitlement_id', customer_entitlement_id,
|
|
'rollover_id', rollover_id,
|
|
'entity_id', entity_id,
|
|
'credit_cost', credit_cost,
|
|
'balance_delta', inverse_balance_delta,
|
|
'adjustment_delta', inverse_adjustment_delta,
|
|
'usage_delta', inverse_usage_delta,
|
|
'value_delta', inverse_value_delta
|
|
)
|
|
);
|
|
|
|
remaining_value := remaining_value - unwind_iteration_value;
|
|
END LOOP;
|
|
|
|
-- signed_remaining_unwind_value: the signed compensation for any items that were
|
|
-- skipped because the target entitlement/rollover no longer exists.
|
|
-- A positive lock (deduction) that couldn't be restored → negative value (refund against current entitlements).
|
|
-- A negative lock (credit) that couldn't be taken back → positive value (deduction against current entitlements).
|
|
-- Callers add this directly to amount_to_deduct.
|
|
RETURN QUERY
|
|
SELECT
|
|
(-lock_sign * skipped_unwind)::numeric,
|
|
updates_json,
|
|
modified_rollover_ids_array,
|
|
mutation_logs_json;
|
|
END;
|
|
$$;
|