-- ===================================================================== -- Migration 0037 — Whish receive / payout support. -- -- WHISH_SEND already exists. This adds WHISH_RECEIVE using the same -- payout detail table and receive accounting as OMT/WU receive: -- cash leaves the drawer, provider float increases because Whish owes -- the shop settlement. -- ===================================================================== set search_path = app, public; insert into app.services(code, name, category) values ('WHISH_RECEIVE', 'Whish — Receive', 'money_transfer') on conflict (code) do update set name = excluded.name, category = excluded.category; -- Receive details can back OMT, WU, and Whish payout transactions. create or replace function app.omt_recv_check() returns trigger language plpgsql as $$ declare svc text; begin select service_code into svc from app.transactions where id = new.txn_id; if svc not in ('OMT_RECEIVE','WU_RECEIVE','WHISH_RECEIVE') then raise exception 'omt_receive_details only valid for receive services (got %)', svc; end if; return new; end; $$; -- Map Whish receive to the Whish float pool. 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 when 'WU_RECEIVE' then 'OMT_CASH'::app.float_provider when 'WHISH_SEND' then 'WHISH'::app.float_provider when 'WHISH_RECEIVE' then 'WHISH'::app.float_provider when 'EDL_BILL' then 'OMT_CASH'::app.float_provider end; $$; -- Re-define receive RPC to include WHISH_RECEIVE. 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' ) 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','WHISH_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' when 'WHISH_RECEIVE' 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_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 ); 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); 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; -- Detail-required check with REFUND preservation from migration 0008. create or replace function app.txn_require_detail() returns trigger language plpgsql as $$ declare ok boolean; begin if new.status <> 'completed' then return null; end if; case new.service_code when 'OMT_SEND' then select exists(select 1 from app.omt_send_details where txn_id = new.id) into ok; when 'OMT_RECEIVE' then select exists(select 1 from app.omt_receive_details where txn_id = new.id) into ok; when 'WU_SEND' then select exists(select 1 from app.omt_send_details where txn_id = new.id) into ok; when 'WU_RECEIVE' then select exists(select 1 from app.omt_receive_details where txn_id = new.id) into ok; when 'WHISH_SEND' then select exists(select 1 from app.omt_send_details where txn_id = new.id) into ok; when 'WHISH_RECEIVE' then select exists(select 1 from app.omt_receive_details where txn_id = new.id) into ok; when 'OMT_BILL' then select exists(select 1 from app.bill_payment_details where txn_id = new.id) into ok; when 'EDL_BILL' then select exists(select 1 from app.bill_payment_details where txn_id = new.id) into ok; when 'ALFA_RECHARGE' then select exists(select 1 from app.recharge_details where txn_id = new.id) into ok; when 'TOUCH_RECHARGE' then select exists(select 1 from app.recharge_details where txn_id = new.id) into ok; when 'OGERO_RECHARGE' then select exists(select 1 from app.recharge_details where txn_id = new.id) into ok; when 'INTERNET_RECHARGE'then select exists(select 1 from app.recharge_details where txn_id = new.id) into ok; when 'SIM_SALE' then select exists(select 1 from app.goods_sale_details where txn_id = new.id) into ok; when 'PHONE_SALE' then select exists(select 1 from app.goods_sale_details where txn_id = new.id) into ok; when 'ACCESSORY_SALE' then select exists(select 1 from app.goods_sale_details where txn_id = new.id) into ok; when 'GOODS_SALE' then select exists(select 1 from app.goods_sale_details where txn_id = new.id) into ok; when 'REPAIR' then select exists(select 1 from app.repair_details where txn_id = new.id) into ok; when 'REFUND' then select exists(select 1 from app.refunds where refund_txn_id = new.id) into ok; else ok := true; end case; if not ok then raise exception 'transaction % (service %) is missing its detail/refund row', new.id, new.service_code; end if; return null; end; $$; 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','WHISH_RECEIVE'); if not is_money_transfer then return null; end if; 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 index if exists app.ux_txn_external_ref_active; create unique index ux_txn_external_ref_active on app.transactions (shop_id, external_ref_provider, external_ref) where external_ref is not null and external_ref_provider is not null and status <> 'voided' and service_code in ( 'OMT_SEND','OMT_RECEIVE','WU_SEND','WU_RECEIVE', 'WHISH_SEND','WHISH_RECEIVE','OMT_BILL','EDL_BILL' );