Fix RBAC, user creation and update components
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
-- =====================================================================
|
||||
-- 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'
|
||||
);
|
||||
@@ -0,0 +1,113 @@
|
||||
-- =====================================================================
|
||||
-- Migration 0038 — Shop-scoped service titles and icons.
|
||||
--
|
||||
-- Lets owners/managers customize how transaction services appear in the
|
||||
-- UI without changing canonical service codes used by accounting logic.
|
||||
-- =====================================================================
|
||||
|
||||
set search_path = app, public;
|
||||
|
||||
create table if not exists app.service_ui_settings (
|
||||
shop_id uuid not null references app.shops(id) on delete cascade,
|
||||
service_code text not null references app.services(code) on delete cascade,
|
||||
display_name text,
|
||||
icon text,
|
||||
updated_at timestamptz not null default now(),
|
||||
updated_by uuid references auth.users(id),
|
||||
primary key (shop_id, service_code),
|
||||
constraint service_ui_display_name_len check (display_name is null or length(display_name) between 1 and 80),
|
||||
constraint service_ui_icon_len check (icon is null or length(icon) between 1 and 16)
|
||||
);
|
||||
|
||||
alter table app.service_ui_settings enable row level security;
|
||||
alter table app.service_ui_settings force row level security;
|
||||
revoke insert, update, delete on app.service_ui_settings from authenticated;
|
||||
grant select on app.service_ui_settings to authenticated;
|
||||
|
||||
drop policy if exists service_ui_select on app.service_ui_settings;
|
||||
create policy service_ui_select on app.service_ui_settings
|
||||
for select using (
|
||||
app.has_any_role_in_shop(shop_id, array['owner','manager','cashier','auditor']::app.business_role[])
|
||||
);
|
||||
|
||||
create or replace view app.v_service_ui_settings as
|
||||
select
|
||||
s.id as shop_id,
|
||||
svc.code as service_code,
|
||||
svc.name as default_name,
|
||||
svc.category,
|
||||
svc.is_active,
|
||||
ui.display_name,
|
||||
ui.icon,
|
||||
ui.updated_at,
|
||||
ui.updated_by
|
||||
from app.shops s
|
||||
join app.services svc on svc.is_active = true
|
||||
left join app.service_ui_settings ui
|
||||
on ui.shop_id = s.id and ui.service_code = svc.code
|
||||
where app.has_any_role_in_shop(s.id, array['owner','manager','cashier','auditor']::app.business_role[]);
|
||||
|
||||
grant select on app.v_service_ui_settings to authenticated;
|
||||
|
||||
create or replace function app.set_service_ui_setting(
|
||||
p_shop uuid,
|
||||
p_service_code text,
|
||||
p_display_name text,
|
||||
p_icon text
|
||||
) returns void
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = app, public
|
||||
as $$
|
||||
declare
|
||||
v_display text := nullif(btrim(coalesce(p_display_name, '')), '');
|
||||
v_icon text := nullif(btrim(coalesce(p_icon, '')), '');
|
||||
begin
|
||||
if not app.has_any_role_in_shop(p_shop, array['owner','manager']::app.business_role[]) then
|
||||
raise exception 'manager or owner role required';
|
||||
end if;
|
||||
if not exists (select 1 from app.services where code = p_service_code and is_active) then
|
||||
raise exception 'unknown or inactive service %', p_service_code;
|
||||
end if;
|
||||
if v_display is null and v_icon is null then
|
||||
delete from app.service_ui_settings
|
||||
where shop_id = p_shop and service_code = p_service_code;
|
||||
return;
|
||||
end if;
|
||||
|
||||
insert into app.service_ui_settings(shop_id, service_code, display_name, icon, updated_by)
|
||||
values (p_shop, p_service_code, v_display, v_icon, auth.uid())
|
||||
on conflict (shop_id, service_code) do update
|
||||
set display_name = excluded.display_name,
|
||||
icon = excluded.icon,
|
||||
updated_at = now(),
|
||||
updated_by = auth.uid();
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke all on function app.set_service_ui_setting(uuid, text, text, text) from public;
|
||||
grant execute on function app.set_service_ui_setting(uuid, text, text, text) to authenticated;
|
||||
|
||||
-- Recent transaction rows should use the shop's display title when set.
|
||||
drop view if exists app.v_my_recent_transactions;
|
||||
create view app.v_my_recent_transactions as
|
||||
select t.id, t.reference_no, t.shop_id, t.till_id, t.shift_id,
|
||||
t.service_code,
|
||||
coalesce(ui.display_name, s.name) as service_name,
|
||||
s.category,
|
||||
t.payment_method,
|
||||
t.gross_usd, t.gross_lbp,
|
||||
t.fee_usd + t.commission_usd as revenue_usd,
|
||||
t.fee_lbp + t.commission_lbp as revenue_lbp,
|
||||
t.external_ref, t.external_ref_provider,
|
||||
t.beneficiary_name, t.beneficiary_phone,
|
||||
t.status, t.occurred_at, t.user_id
|
||||
from app.transactions t
|
||||
join app.services s on s.code = t.service_code
|
||||
left join app.service_ui_settings ui
|
||||
on ui.shop_id = t.shop_id and ui.service_code = t.service_code
|
||||
where t.user_id = auth.uid()
|
||||
or app.has_any_role_in_shop(t.shop_id,
|
||||
array['owner','manager','auditor']::app.business_role[]);
|
||||
|
||||
grant select on app.v_my_recent_transactions to authenticated;
|
||||
@@ -0,0 +1,51 @@
|
||||
alter table auth.users
|
||||
add column if not exists is_system_admin boolean not null default false;
|
||||
|
||||
update auth.users
|
||||
set is_system_admin = true
|
||||
where id = (
|
||||
select u.id
|
||||
from auth.users u
|
||||
order by u.created_at
|
||||
limit 1
|
||||
)
|
||||
and not exists (select 1 from auth.users where is_system_admin = true);
|
||||
|
||||
drop function if exists app.me();
|
||||
|
||||
create or replace function app.me()
|
||||
returns table (
|
||||
user_id uuid,
|
||||
full_name text,
|
||||
is_active boolean,
|
||||
is_system_admin boolean,
|
||||
is_owner_anywhere boolean,
|
||||
emp_id text,
|
||||
shops jsonb
|
||||
) language sql
|
||||
security definer
|
||||
set search_path = app, public
|
||||
stable
|
||||
as $$
|
||||
select
|
||||
auth.uid() as user_id,
|
||||
coalesce(p.full_name, u.full_name, '') as full_name,
|
||||
coalesce(p.is_active, u.is_active, true) as is_active,
|
||||
coalesce(u.is_system_admin, false) as is_system_admin,
|
||||
app.is_owner_anywhere() as is_owner_anywhere,
|
||||
e.emp_id as emp_id,
|
||||
coalesce((
|
||||
select jsonb_agg(jsonb_build_object(
|
||||
'shop_id', a.shop_id, 'shop_name', s.name, 'role', a.role))
|
||||
from app.user_shop_assignments a
|
||||
join app.shops s on s.id = a.shop_id
|
||||
where a.user_id = auth.uid()
|
||||
), '[]'::jsonb) as shops
|
||||
from auth.users u
|
||||
left join app.user_profiles p on p.user_id = u.id
|
||||
left join app.employees e on lower(e.email) = lower(u.email::text)
|
||||
where u.id = auth.uid()
|
||||
limit 1;
|
||||
$$;
|
||||
revoke all on function app.me() from public;
|
||||
grant execute on function app.me() to authenticated;
|
||||
Reference in New Issue
Block a user