Add cash management schema and immediate variance alerts
This commit is contained in:
@@ -0,0 +1,506 @@
|
||||
-- =====================================================================
|
||||
-- Migration 0018 — Money-transfer cash + float coupling.
|
||||
--
|
||||
-- Closes the largest hole in the cash-control model: until now,
|
||||
-- record_omt_send / record_omt_receive / record_bill only inserted into
|
||||
-- app.transactions and the detail table. They did NOT post anything to
|
||||
-- app.cash_movements or app.float_movements, so:
|
||||
--
|
||||
-- * v_z_report.expected_close_usd = sum(cash_movements) was wrong by
|
||||
-- the entire transfer turnover, hiding cashier shortages.
|
||||
-- * The OMT/Whish/WU/biller float balance never moved, so we couldn't
|
||||
-- tell who owed whom and the matcher could only compare by
|
||||
-- external_ref, never by money.
|
||||
-- * Threat-model rows #1, #5, #21 had no DB-level enforcement for
|
||||
-- money transfers (only recharges had the deferred-trigger
|
||||
-- constraint).
|
||||
--
|
||||
-- This migration:
|
||||
-- 1. Adds app._post_cash_for_txn / app._post_float_for_txn helpers.
|
||||
-- 2. Adds app._get_or_create_float(shop, provider, currency).
|
||||
-- 3. Re-defines record_omt_send / record_omt_receive / record_bill so
|
||||
-- each posts the cash leg (when payment_method is cash_*) and the
|
||||
-- provider-float leg in the same SECURITY DEFINER body.
|
||||
-- 4. Adds record_whish_send, record_wu_send, record_wu_receive so the
|
||||
-- UI does not silently file Whish/WU under provider='OMT'.
|
||||
-- 5. Adds a deferred constraint trigger that requires every completed
|
||||
-- money-transfer / bill txn to have at least one float_movement
|
||||
-- row. Recharges already have their own coupling trigger from
|
||||
-- 0005; goods sales have one too.
|
||||
--
|
||||
-- Threat-model rows: 1, 5, 7, 8, 14, 21, 24.
|
||||
-- =====================================================================
|
||||
|
||||
-- =====================================================================
|
||||
-- Helper: get_or_create the float account the txn should debit/credit.
|
||||
-- =====================================================================
|
||||
create or replace function app._get_or_create_float(
|
||||
p_shop uuid,
|
||||
p_provider app.float_provider,
|
||||
p_currency app.currency_code
|
||||
) returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = app, public
|
||||
as $$
|
||||
declare v_id uuid;
|
||||
begin
|
||||
select id into v_id from app.floats
|
||||
where shop_id = p_shop and provider = p_provider and currency = p_currency;
|
||||
if v_id is null then
|
||||
insert into app.floats(shop_id, provider, currency)
|
||||
values (p_shop, p_provider, p_currency)
|
||||
returning id into v_id;
|
||||
-- Initialise the cached balance row at zero.
|
||||
insert into app.float_balances(float_id, balance) values (v_id, 0)
|
||||
on conflict (float_id) do nothing;
|
||||
end if;
|
||||
return v_id;
|
||||
end;
|
||||
$$;
|
||||
revoke all on function app._get_or_create_float(uuid, app.float_provider, app.currency_code)
|
||||
from public;
|
||||
|
||||
-- =====================================================================
|
||||
-- Helper: post the cash leg for a customer-facing transaction.
|
||||
--
|
||||
-- Convention (matches 0002 cash_movements):
|
||||
-- * Positive amount = cash into till.
|
||||
-- * Negative amount = cash out of till.
|
||||
-- This function takes a "customer movement" sign:
|
||||
-- * p_customer_paid > 0 -> cash_in (sale_in) amount = +p_customer_paid
|
||||
-- * p_customer_paid < 0 -> cash_out (payout_out) amount = p_customer_paid
|
||||
-- For non-cash payment methods (whish, omt_wallet, card, bank_transfer)
|
||||
-- the cash leg is skipped — those balances live on their own floats.
|
||||
-- =====================================================================
|
||||
create or replace function app._post_cash_for_txn(
|
||||
p_txn_id uuid,
|
||||
p_payment_method app.payment_method,
|
||||
p_customer_usd numeric,
|
||||
p_customer_lbp numeric
|
||||
) returns void
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = app, public
|
||||
as $$
|
||||
declare
|
||||
v_shift uuid;
|
||||
v_type app.cash_movement_type;
|
||||
begin
|
||||
-- Only cash-in-till payment methods produce a cash leg.
|
||||
if p_payment_method not in ('cash_usd','cash_lbp') then
|
||||
return;
|
||||
end if;
|
||||
|
||||
select shift_id into v_shift from app.transactions where id = p_txn_id;
|
||||
if v_shift is null then
|
||||
raise exception 'txn % not found while posting cash leg', p_txn_id;
|
||||
end if;
|
||||
|
||||
-- For cash_usd payment method, only USD leg may move; same for LBP.
|
||||
if p_payment_method = 'cash_usd' then
|
||||
if coalesce(p_customer_lbp, 0) <> 0 then
|
||||
raise exception 'cash_usd payment must not move LBP (got %)', p_customer_lbp;
|
||||
end if;
|
||||
if coalesce(p_customer_usd, 0) = 0 then return; end if;
|
||||
v_type := case when p_customer_usd > 0 then 'sale_in'::app.cash_movement_type
|
||||
else 'payout_out'::app.cash_movement_type end;
|
||||
insert into app.cash_movements(shift_id, type, currency, amount, ref_txn_id, note)
|
||||
values (v_shift, v_type, 'USD', p_customer_usd, p_txn_id, 'auto: txn cash leg');
|
||||
else -- cash_lbp
|
||||
if coalesce(p_customer_usd, 0) <> 0 then
|
||||
raise exception 'cash_lbp payment must not move USD (got %)', p_customer_usd;
|
||||
end if;
|
||||
if coalesce(p_customer_lbp, 0) = 0 then return; end if;
|
||||
v_type := case when p_customer_lbp > 0 then 'sale_in'::app.cash_movement_type
|
||||
else 'payout_out'::app.cash_movement_type end;
|
||||
insert into app.cash_movements(shift_id, type, currency, amount, ref_txn_id, note)
|
||||
values (v_shift, v_type, 'LBP', p_customer_lbp, p_txn_id, 'auto: txn cash leg');
|
||||
end if;
|
||||
end;
|
||||
$$;
|
||||
revoke all on function app._post_cash_for_txn(uuid, app.payment_method, numeric, numeric)
|
||||
from public;
|
||||
|
||||
-- =====================================================================
|
||||
-- Helper: post the provider-float leg for a customer-facing transaction.
|
||||
--
|
||||
-- p_amount sign convention on app.float_movements:
|
||||
-- + : float increases (provider owes shop more, e-recharge wallet
|
||||
-- topped up, OMT credits us at settlement, ...)
|
||||
-- - : float decreases (we used it up, we owe provider more cash, ...)
|
||||
-- =====================================================================
|
||||
create or replace function app._post_float_for_txn(
|
||||
p_txn_id uuid,
|
||||
p_provider app.float_provider,
|
||||
p_currency app.currency_code,
|
||||
p_amount numeric,
|
||||
p_reason text
|
||||
) returns void
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = app, public
|
||||
as $$
|
||||
declare
|
||||
v_shop uuid; v_shift uuid; v_float uuid;
|
||||
begin
|
||||
if coalesce(p_amount, 0) = 0 then return; end if;
|
||||
select shop_id, shift_id into v_shop, v_shift
|
||||
from app.transactions where id = p_txn_id;
|
||||
if v_shop is null then
|
||||
raise exception 'txn % not found while posting float leg', p_txn_id;
|
||||
end if;
|
||||
v_float := app._get_or_create_float(v_shop, p_provider, p_currency);
|
||||
insert into app.float_movements(float_id, shift_id, amount, ref_txn_id, reason)
|
||||
values (v_float, v_shift, p_amount, p_txn_id, p_reason);
|
||||
end;
|
||||
$$;
|
||||
revoke all on function app._post_float_for_txn(uuid, app.float_provider,
|
||||
app.currency_code, numeric, text) from public;
|
||||
|
||||
-- =====================================================================
|
||||
-- Map a money-transfer service code to its float provider.
|
||||
-- =====================================================================
|
||||
create or replace function app._money_transfer_provider(p_service text)
|
||||
returns app.float_provider
|
||||
language sql
|
||||
immutable
|
||||
as $$
|
||||
select case p_service
|
||||
when 'OMT_SEND' then 'OMT_CASH'::app.float_provider
|
||||
when 'OMT_RECEIVE' then 'OMT_CASH'::app.float_provider
|
||||
when 'OMT_BILL' then 'OMT_CASH'::app.float_provider
|
||||
when 'WU_SEND' then 'OMT_CASH'::app.float_provider -- WU runs on the OMT cash pool in LB
|
||||
when 'WU_RECEIVE' then 'OMT_CASH'::app.float_provider
|
||||
when 'WHISH_SEND' then 'WHISH'::app.float_provider
|
||||
when 'EDL_BILL' then 'OMT_CASH'::app.float_provider -- EDL paid via OMT counter
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- =====================================================================
|
||||
-- Re-define record_omt_send to post cash + float in one go.
|
||||
-- Provider is stamped from the service code, not hard-coded.
|
||||
-- =====================================================================
|
||||
create or replace function app.record_omt_send(
|
||||
p_shop uuid, p_till uuid,
|
||||
p_payment_method app.payment_method,
|
||||
p_gross_usd numeric, p_gross_lbp numeric,
|
||||
p_fee_usd numeric, p_fee_lbp numeric,
|
||||
p_commission_usd numeric, p_commission_lbp numeric,
|
||||
p_fx_rate numeric,
|
||||
p_external_ref text,
|
||||
p_direction app.transfer_direction,
|
||||
p_sender_full_name text, p_sender_id_type app.id_doc_type,
|
||||
p_sender_id_number text, p_sender_phone text,
|
||||
p_sender_dob date, p_sender_nationality text,
|
||||
p_beneficiary_full_name text, p_beneficiary_phone text,
|
||||
p_destination_country text,
|
||||
p_purpose_code text, p_purpose_note text,
|
||||
p_kyc_doc_url text,
|
||||
p_customer_id uuid,
|
||||
p_notes text,
|
||||
p_service_code text default 'OMT_SEND' -- 'OMT_SEND' | 'WU_SEND' | 'WHISH_SEND'
|
||||
) returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = app, public
|
||||
as $$
|
||||
declare
|
||||
v_txn uuid;
|
||||
v_provider_lbl text;
|
||||
v_float_prov app.float_provider;
|
||||
begin
|
||||
if p_service_code not in ('OMT_SEND','WU_SEND','WHISH_SEND') then
|
||||
raise exception 'record_omt_send: unsupported service %', p_service_code;
|
||||
end if;
|
||||
v_provider_lbl := case p_service_code
|
||||
when 'OMT_SEND' then 'OMT'
|
||||
when 'WU_SEND' then 'WU'
|
||||
when 'WHISH_SEND' then 'WHISH'
|
||||
end;
|
||||
|
||||
v_txn := app._insert_txn(p_shop, p_till, p_service_code, p_payment_method,
|
||||
p_gross_usd, p_gross_lbp, p_fee_usd, p_fee_lbp,
|
||||
p_commission_usd, p_commission_lbp, p_fx_rate,
|
||||
v_provider_lbl, p_external_ref,
|
||||
p_beneficiary_full_name, p_beneficiary_phone,
|
||||
p_customer_id, p_notes);
|
||||
|
||||
insert into app.omt_send_details(
|
||||
txn_id, direction,
|
||||
sender_full_name, sender_id_type, sender_id_number, sender_phone,
|
||||
sender_dob, sender_nationality,
|
||||
beneficiary_full_name, beneficiary_phone, destination_country,
|
||||
purpose_code, purpose_note, kyc_doc_url
|
||||
) values (
|
||||
v_txn, p_direction,
|
||||
p_sender_full_name, p_sender_id_type, p_sender_id_number, p_sender_phone,
|
||||
p_sender_dob, p_sender_nationality,
|
||||
p_beneficiary_full_name, p_beneficiary_phone, p_destination_country,
|
||||
p_purpose_code, p_purpose_note, p_kyc_doc_url
|
||||
);
|
||||
|
||||
-- Cash leg: customer hands over gross + fee in cash.
|
||||
perform app._post_cash_for_txn(v_txn, p_payment_method,
|
||||
coalesce(p_gross_usd,0) + coalesce(p_fee_usd,0),
|
||||
coalesce(p_gross_lbp,0) + coalesce(p_fee_lbp,0));
|
||||
|
||||
-- Float leg: shop now owes the provider gross (we keep fee+comm).
|
||||
v_float_prov := app._money_transfer_provider(p_service_code);
|
||||
perform app._post_float_for_txn(v_txn, v_float_prov, 'USD',
|
||||
-coalesce(p_gross_usd,0), 'send: shop owes provider gross');
|
||||
perform app._post_float_for_txn(v_txn, v_float_prov, 'LBP',
|
||||
-coalesce(p_gross_lbp,0), 'send: shop owes provider gross');
|
||||
|
||||
return v_txn;
|
||||
end; $$;
|
||||
revoke all on function app.record_omt_send(uuid, uuid, app.payment_method,
|
||||
numeric, numeric, numeric, numeric, numeric, numeric, numeric,
|
||||
text, app.transfer_direction,
|
||||
text, app.id_doc_type, text, text, date, text,
|
||||
text, text, text, text, text, text, uuid, text, text) from public;
|
||||
grant execute on function app.record_omt_send(uuid, uuid, app.payment_method,
|
||||
numeric, numeric, numeric, numeric, numeric, numeric, numeric,
|
||||
text, app.transfer_direction,
|
||||
text, app.id_doc_type, text, text, date, text,
|
||||
text, text, text, text, text, text, uuid, text, text) to authenticated;
|
||||
|
||||
-- =====================================================================
|
||||
-- Re-define record_omt_receive (also serves WU_RECEIVE).
|
||||
-- Customer presents code, cashier hands them gross.
|
||||
-- =====================================================================
|
||||
create or replace function app.record_omt_receive(
|
||||
p_shop uuid, p_till uuid,
|
||||
p_payment_method app.payment_method,
|
||||
p_gross_usd numeric, p_gross_lbp numeric,
|
||||
p_fee_usd numeric, p_fee_lbp numeric,
|
||||
p_commission_usd numeric, p_commission_lbp numeric,
|
||||
p_fx_rate numeric,
|
||||
p_payout_code text,
|
||||
p_beneficiary_full_name text,
|
||||
p_beneficiary_id_type app.id_doc_type,
|
||||
p_beneficiary_id_number text,
|
||||
p_beneficiary_phone text,
|
||||
p_origin_country text,
|
||||
p_kyc_doc_url text,
|
||||
p_customer_id uuid,
|
||||
p_notes text,
|
||||
p_service_code text default 'OMT_RECEIVE' -- or 'WU_RECEIVE'
|
||||
) returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = app, public
|
||||
as $$
|
||||
declare
|
||||
v_txn uuid;
|
||||
v_provider_lbl text;
|
||||
v_float_prov app.float_provider;
|
||||
v_net_usd numeric;
|
||||
v_net_lbp numeric;
|
||||
begin
|
||||
if p_service_code not in ('OMT_RECEIVE','WU_RECEIVE') then
|
||||
raise exception 'record_omt_receive: unsupported service %', p_service_code;
|
||||
end if;
|
||||
v_provider_lbl := case p_service_code
|
||||
when 'OMT_RECEIVE' then 'OMT'
|
||||
when 'WU_RECEIVE' then 'WU'
|
||||
end;
|
||||
|
||||
v_txn := app._insert_txn(p_shop, p_till, p_service_code, p_payment_method,
|
||||
p_gross_usd, p_gross_lbp, p_fee_usd, p_fee_lbp,
|
||||
p_commission_usd, p_commission_lbp, p_fx_rate,
|
||||
v_provider_lbl, p_payout_code,
|
||||
p_beneficiary_full_name, p_beneficiary_phone,
|
||||
p_customer_id, p_notes);
|
||||
|
||||
insert into app.omt_receive_details(
|
||||
txn_id, payout_code,
|
||||
beneficiary_full_name, beneficiary_id_type, beneficiary_id_number,
|
||||
beneficiary_phone, origin_country, kyc_doc_url
|
||||
) values (
|
||||
v_txn, p_payout_code,
|
||||
p_beneficiary_full_name, p_beneficiary_id_type, p_beneficiary_id_number,
|
||||
p_beneficiary_phone, p_origin_country, p_kyc_doc_url
|
||||
);
|
||||
|
||||
-- Cash leg: shop pays gross out, may collect a small fee from beneficiary.
|
||||
-- net cash to till = -gross + fee
|
||||
-- (Most LB payouts have no beneficiary-side fee; if fee=0 this just
|
||||
-- becomes -gross.)
|
||||
v_net_usd := -coalesce(p_gross_usd,0) + coalesce(p_fee_usd,0);
|
||||
v_net_lbp := -coalesce(p_gross_lbp,0) + coalesce(p_fee_lbp,0);
|
||||
perform app._post_cash_for_txn(v_txn, p_payment_method, v_net_usd, v_net_lbp);
|
||||
|
||||
-- Float leg: provider now owes the shop gross + commission.
|
||||
v_float_prov := app._money_transfer_provider(p_service_code);
|
||||
perform app._post_float_for_txn(v_txn, v_float_prov, 'USD',
|
||||
coalesce(p_gross_usd,0) + coalesce(p_commission_usd,0),
|
||||
'receive: provider owes shop gross + commission');
|
||||
perform app._post_float_for_txn(v_txn, v_float_prov, 'LBP',
|
||||
coalesce(p_gross_lbp,0) + coalesce(p_commission_lbp,0),
|
||||
'receive: provider owes shop gross + commission');
|
||||
|
||||
return v_txn;
|
||||
end; $$;
|
||||
revoke all on function app.record_omt_receive(uuid, uuid, app.payment_method,
|
||||
numeric, numeric, numeric, numeric, numeric, numeric, numeric,
|
||||
text, text, app.id_doc_type, text, text, text, text, uuid, text, text) from public;
|
||||
grant execute on function app.record_omt_receive(uuid, uuid, app.payment_method,
|
||||
numeric, numeric, numeric, numeric, numeric, numeric, numeric,
|
||||
text, text, app.id_doc_type, text, text, text, text, uuid, text, text) to authenticated;
|
||||
|
||||
-- =====================================================================
|
||||
-- Re-define record_bill (OMT_BILL / EDL_BILL) with cash + float legs.
|
||||
-- =====================================================================
|
||||
create or replace function app.record_bill(
|
||||
p_shop uuid, p_till uuid, p_service_code text,
|
||||
p_payment_method app.payment_method,
|
||||
p_gross_usd numeric, p_gross_lbp numeric,
|
||||
p_fee_usd numeric, p_fee_lbp numeric,
|
||||
p_fx_rate numeric,
|
||||
p_external_ref text,
|
||||
p_biller_code text, p_account_number text,
|
||||
p_period text, p_customer_name text,
|
||||
p_customer_id uuid, p_notes text
|
||||
) returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = app, public
|
||||
as $$
|
||||
declare
|
||||
v_txn uuid;
|
||||
v_float_prov app.float_provider;
|
||||
begin
|
||||
if p_service_code not in ('OMT_BILL','EDL_BILL') then
|
||||
raise exception 'record_bill only for bill services, got %', p_service_code;
|
||||
end if;
|
||||
v_txn := app._insert_txn(p_shop, p_till, p_service_code, p_payment_method,
|
||||
p_gross_usd, p_gross_lbp, p_fee_usd, p_fee_lbp, 0, 0, p_fx_rate,
|
||||
p_biller_code, p_external_ref,
|
||||
p_customer_name, null,
|
||||
p_customer_id, p_notes);
|
||||
|
||||
insert into app.bill_payment_details(
|
||||
txn_id, biller_code, account_number, period, customer_name
|
||||
) values (
|
||||
v_txn, p_biller_code, p_account_number, p_period, p_customer_name
|
||||
);
|
||||
|
||||
-- Cash leg: customer pays gross + fee.
|
||||
perform app._post_cash_for_txn(v_txn, p_payment_method,
|
||||
coalesce(p_gross_usd,0) + coalesce(p_fee_usd,0),
|
||||
coalesce(p_gross_lbp,0) + coalesce(p_fee_lbp,0));
|
||||
|
||||
-- Float leg: shop now owes the biller's settlement counterparty
|
||||
-- gross. EDL/OMT_BILL settle through the OMT cash pool in our model.
|
||||
v_float_prov := app._money_transfer_provider(p_service_code);
|
||||
if v_float_prov is not null then
|
||||
perform app._post_float_for_txn(v_txn, v_float_prov, 'USD',
|
||||
-coalesce(p_gross_usd,0), 'bill: shop owes biller gross');
|
||||
perform app._post_float_for_txn(v_txn, v_float_prov, 'LBP',
|
||||
-coalesce(p_gross_lbp,0), 'bill: shop owes biller gross');
|
||||
end if;
|
||||
return v_txn;
|
||||
end; $$;
|
||||
revoke all on function app.record_bill(uuid, uuid, text, app.payment_method,
|
||||
numeric, numeric, numeric, numeric, numeric, text, text, text, text, text, uuid, text)
|
||||
from public;
|
||||
grant execute on function app.record_bill(uuid, uuid, text, app.payment_method,
|
||||
numeric, numeric, numeric, numeric, numeric, text, text, text, text, text, uuid, text)
|
||||
to authenticated;
|
||||
|
||||
-- =====================================================================
|
||||
-- Convenience wrapper for Whish — same shape as omt_send_details for
|
||||
-- now (sender + beneficiary). The UI sends WHISH_SEND and gets a
|
||||
-- correctly tagged provider on the txn row.
|
||||
-- =====================================================================
|
||||
create or replace function app.record_whish_send(
|
||||
p_shop uuid, p_till uuid,
|
||||
p_payment_method app.payment_method,
|
||||
p_gross_usd numeric, p_gross_lbp numeric,
|
||||
p_fee_usd numeric, p_fee_lbp numeric,
|
||||
p_commission_usd numeric, p_commission_lbp numeric,
|
||||
p_fx_rate numeric,
|
||||
p_external_ref text,
|
||||
p_direction app.transfer_direction,
|
||||
p_sender_full_name text, p_sender_id_type app.id_doc_type,
|
||||
p_sender_id_number text, p_sender_phone text,
|
||||
p_beneficiary_full_name text, p_beneficiary_phone text,
|
||||
p_purpose_code text, p_purpose_note text,
|
||||
p_kyc_doc_url text,
|
||||
p_customer_id uuid, p_notes text
|
||||
) returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = app, public
|
||||
as $$
|
||||
begin
|
||||
return app.record_omt_send(
|
||||
p_shop, p_till, p_payment_method,
|
||||
p_gross_usd, p_gross_lbp, p_fee_usd, p_fee_lbp,
|
||||
p_commission_usd, p_commission_lbp, p_fx_rate,
|
||||
p_external_ref, p_direction,
|
||||
p_sender_full_name, p_sender_id_type, p_sender_id_number, p_sender_phone,
|
||||
null, null,
|
||||
p_beneficiary_full_name, p_beneficiary_phone, null,
|
||||
p_purpose_code, p_purpose_note, p_kyc_doc_url,
|
||||
p_customer_id, p_notes,
|
||||
'WHISH_SEND'
|
||||
);
|
||||
end; $$;
|
||||
revoke all on function app.record_whish_send(uuid, uuid, app.payment_method,
|
||||
numeric, numeric, numeric, numeric, numeric, numeric, numeric,
|
||||
text, app.transfer_direction,
|
||||
text, app.id_doc_type, text, text, text, text, text, text, text, uuid, text)
|
||||
from public;
|
||||
grant execute on function app.record_whish_send(uuid, uuid, app.payment_method,
|
||||
numeric, numeric, numeric, numeric, numeric, numeric, numeric,
|
||||
text, app.transfer_direction,
|
||||
text, app.id_doc_type, text, text, text, text, text, text, text, uuid, text)
|
||||
to authenticated;
|
||||
|
||||
-- =====================================================================
|
||||
-- Deferred constraint trigger: every completed money-transfer / bill
|
||||
-- transaction must end up with at least one float_movement row. The
|
||||
-- trigger fires at COMMIT, so the record_* functions above can post the
|
||||
-- float leg after the txn insert in the same transaction.
|
||||
-- =====================================================================
|
||||
create or replace function app._money_transfer_require_movement()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
ok boolean;
|
||||
is_money_transfer boolean;
|
||||
begin
|
||||
if new.status <> 'completed' then return null; end if;
|
||||
|
||||
is_money_transfer := new.service_code in
|
||||
('OMT_SEND','OMT_RECEIVE','OMT_BILL','EDL_BILL',
|
||||
'WU_SEND','WU_RECEIVE','WHISH_SEND');
|
||||
if not is_money_transfer then return null; end if;
|
||||
|
||||
-- If both gross sides are 0, no money moved -> nothing to require.
|
||||
if coalesce(new.gross_usd,0) = 0 and coalesce(new.gross_lbp,0) = 0 then
|
||||
return null;
|
||||
end if;
|
||||
|
||||
select exists (
|
||||
select 1 from app.float_movements
|
||||
where ref_txn_id = new.id
|
||||
) into ok;
|
||||
if not ok then
|
||||
raise exception 'money-transfer txn % (service %) has no float_movement leg',
|
||||
new.id, new.service_code;
|
||||
end if;
|
||||
return null;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_money_transfer_require_movement on app.transactions;
|
||||
create constraint trigger trg_money_transfer_require_movement
|
||||
after insert on app.transactions
|
||||
deferrable initially deferred
|
||||
for each row execute function app._money_transfer_require_movement();
|
||||
|
||||
-- End migration 0018 ----------------------------------------------------
|
||||
Reference in New Issue
Block a user