Add cash management schema and immediate variance alerts
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
set search_path = app, public;
|
||||
|
||||
create table if not exists app.end_of_day_reports (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
shop_id uuid not null references app.shops(id) on delete restrict,
|
||||
business_date date not null,
|
||||
submitted_at timestamptz not null default now(),
|
||||
submitted_by uuid not null references auth.users(id) on delete restrict,
|
||||
submitted_by_name text not null,
|
||||
note text,
|
||||
period_started_at timestamptz not null,
|
||||
period_ended_at timestamptz not null,
|
||||
completed_txn_count bigint not null default 0,
|
||||
voided_txn_count bigint not null default 0,
|
||||
gross_usd numeric(14,2) not null default 0,
|
||||
gross_lbp numeric(18,0) not null default 0,
|
||||
safe_drop_usd numeric(14,2) not null default 0,
|
||||
safe_drop_lbp numeric(18,0) not null default 0,
|
||||
closed_shift_count bigint not null default 0,
|
||||
total_variance_usd numeric(14,2) not null default 0,
|
||||
total_variance_lbp numeric(18,0) not null default 0,
|
||||
activity_count bigint not null default 0,
|
||||
activity_log jsonb not null default '[]'::jsonb,
|
||||
unique (shop_id, business_date)
|
||||
);
|
||||
|
||||
alter table app.end_of_day_reports enable row level security;
|
||||
alter table app.end_of_day_reports force row level security;
|
||||
revoke insert, update, delete on app.end_of_day_reports from authenticated;
|
||||
|
||||
drop policy if exists end_of_day_reports_select on app.end_of_day_reports;
|
||||
create policy end_of_day_reports_select on app.end_of_day_reports
|
||||
for select to authenticated
|
||||
using (
|
||||
app.has_any_role_in_shop(shop_id,
|
||||
array['owner','auditor']::app.business_role[])
|
||||
);
|
||||
|
||||
grant select on app.end_of_day_reports to authenticated;
|
||||
|
||||
create or replace view app.v_end_of_day_reports as
|
||||
select
|
||||
id,
|
||||
shop_id,
|
||||
business_date,
|
||||
submitted_at,
|
||||
submitted_by,
|
||||
submitted_by_name,
|
||||
note,
|
||||
period_started_at,
|
||||
period_ended_at,
|
||||
completed_txn_count,
|
||||
voided_txn_count,
|
||||
gross_usd,
|
||||
gross_lbp,
|
||||
safe_drop_usd,
|
||||
safe_drop_lbp,
|
||||
closed_shift_count,
|
||||
total_variance_usd,
|
||||
total_variance_lbp,
|
||||
activity_count,
|
||||
activity_log
|
||||
from app.end_of_day_reports;
|
||||
|
||||
grant select on app.v_end_of_day_reports to authenticated;
|
||||
|
||||
create or replace function app.submit_end_of_day(
|
||||
p_shop uuid,
|
||||
p_note text default null
|
||||
)
|
||||
returns table (
|
||||
report_id uuid,
|
||||
business_date date,
|
||||
submitted_at timestamptz,
|
||||
completed_txn_count bigint,
|
||||
gross_usd numeric,
|
||||
gross_lbp numeric,
|
||||
activity_count bigint
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = app, public
|
||||
as $$
|
||||
declare
|
||||
v_period_started_at timestamptz;
|
||||
v_period_ended_at timestamptz := now();
|
||||
v_business_date date := (now() at time zone 'Asia/Beirut')::date;
|
||||
v_open_shift_count integer;
|
||||
v_report_id uuid;
|
||||
v_submitted_by_name text;
|
||||
v_completed_txn_count bigint;
|
||||
v_voided_txn_count bigint;
|
||||
v_gross_usd numeric(14,2);
|
||||
v_gross_lbp numeric(18,0);
|
||||
v_safe_drop_usd numeric(14,2);
|
||||
v_safe_drop_lbp numeric(18,0);
|
||||
v_closed_shift_count bigint;
|
||||
v_total_variance_usd numeric(14,2);
|
||||
v_total_variance_lbp numeric(18,0);
|
||||
v_activity_count bigint;
|
||||
v_activity_log jsonb;
|
||||
begin
|
||||
if not app.has_role_in_shop(p_shop, 'owner') then
|
||||
raise exception 'owner role required';
|
||||
end if;
|
||||
|
||||
select count(*)
|
||||
into v_open_shift_count
|
||||
from app.shifts
|
||||
where shop_id = p_shop
|
||||
and status <> 'closed';
|
||||
|
||||
if v_open_shift_count > 0 then
|
||||
raise exception 'close or finalize all shifts before submitting end of day';
|
||||
end if;
|
||||
|
||||
if exists (
|
||||
select 1
|
||||
from app.end_of_day_reports eod
|
||||
where eod.shop_id = p_shop
|
||||
and eod.business_date = v_business_date
|
||||
) then
|
||||
raise exception 'end of day already submitted for this shop today';
|
||||
end if;
|
||||
|
||||
select coalesce(
|
||||
max(period_ended_at),
|
||||
date_trunc('day', now() at time zone 'Asia/Beirut') at time zone 'Asia/Beirut'
|
||||
)
|
||||
into v_period_started_at
|
||||
from app.end_of_day_reports
|
||||
where shop_id = p_shop;
|
||||
|
||||
select coalesce(full_name, email, auth.uid()::text)
|
||||
into v_submitted_by_name
|
||||
from auth.users
|
||||
where id = auth.uid();
|
||||
|
||||
select
|
||||
count(*) filter (where t.status = 'completed'),
|
||||
count(*) filter (where t.status = 'voided'),
|
||||
coalesce(sum(t.gross_usd) filter (where t.status = 'completed'), 0),
|
||||
coalesce(sum(t.gross_lbp) filter (where t.status = 'completed'), 0)
|
||||
into v_completed_txn_count, v_voided_txn_count, v_gross_usd, v_gross_lbp
|
||||
from app.transactions t
|
||||
where t.shop_id = p_shop
|
||||
and t.occurred_at >= v_period_started_at
|
||||
and t.occurred_at <= v_period_ended_at;
|
||||
|
||||
select
|
||||
coalesce(sum(case when cm.currency = 'USD' then abs(cm.amount) else 0 end), 0),
|
||||
coalesce(sum(case when cm.currency = 'LBP' then abs(cm.amount) else 0 end), 0)
|
||||
into v_safe_drop_usd, v_safe_drop_lbp
|
||||
from app.cash_movements cm
|
||||
join app.shifts sh on sh.id = cm.shift_id
|
||||
where sh.shop_id = p_shop
|
||||
and cm.type = 'drop_to_safe'
|
||||
and cm.occurred_at >= v_period_started_at
|
||||
and cm.occurred_at <= v_period_ended_at;
|
||||
|
||||
select
|
||||
count(*),
|
||||
coalesce(sum(sh.variance_usd), 0),
|
||||
coalesce(sum(sh.variance_lbp), 0)
|
||||
into v_closed_shift_count, v_total_variance_usd, v_total_variance_lbp
|
||||
from app.shifts sh
|
||||
where sh.shop_id = p_shop
|
||||
and sh.status = 'closed'
|
||||
and sh.closed_at is not null
|
||||
and sh.closed_at >= v_period_started_at
|
||||
and sh.closed_at <= v_period_ended_at;
|
||||
|
||||
select
|
||||
count(*),
|
||||
coalesce(
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'occurred_at', ae.occurred_at,
|
||||
'event_type', ae.event_type,
|
||||
'metadata', ae.metadata
|
||||
)
|
||||
order by ae.occurred_at desc
|
||||
),
|
||||
'[]'::jsonb
|
||||
)
|
||||
into v_activity_count, v_activity_log
|
||||
from app.auth_events ae
|
||||
where ae.shop_id = p_shop
|
||||
and ae.occurred_at >= v_period_started_at
|
||||
and ae.occurred_at <= v_period_ended_at
|
||||
and ae.event_type <> 'end_of_day_submitted';
|
||||
|
||||
insert into app.end_of_day_reports(
|
||||
shop_id,
|
||||
business_date,
|
||||
submitted_by,
|
||||
submitted_by_name,
|
||||
note,
|
||||
period_started_at,
|
||||
period_ended_at,
|
||||
completed_txn_count,
|
||||
voided_txn_count,
|
||||
gross_usd,
|
||||
gross_lbp,
|
||||
safe_drop_usd,
|
||||
safe_drop_lbp,
|
||||
closed_shift_count,
|
||||
total_variance_usd,
|
||||
total_variance_lbp,
|
||||
activity_count,
|
||||
activity_log
|
||||
)
|
||||
values (
|
||||
p_shop,
|
||||
v_business_date,
|
||||
auth.uid(),
|
||||
coalesce(v_submitted_by_name, auth.uid()::text),
|
||||
nullif(btrim(coalesce(p_note, '')), ''),
|
||||
v_period_started_at,
|
||||
v_period_ended_at,
|
||||
coalesce(v_completed_txn_count, 0),
|
||||
coalesce(v_voided_txn_count, 0),
|
||||
coalesce(v_gross_usd, 0),
|
||||
coalesce(v_gross_lbp, 0),
|
||||
coalesce(v_safe_drop_usd, 0),
|
||||
coalesce(v_safe_drop_lbp, 0),
|
||||
coalesce(v_closed_shift_count, 0),
|
||||
coalesce(v_total_variance_usd, 0),
|
||||
coalesce(v_total_variance_lbp, 0),
|
||||
coalesce(v_activity_count, 0),
|
||||
coalesce(v_activity_log, '[]'::jsonb)
|
||||
)
|
||||
returning id into v_report_id;
|
||||
|
||||
perform app.log_auth_event(
|
||||
'end_of_day_submitted',
|
||||
p_shop,
|
||||
null,
|
||||
jsonb_build_object(
|
||||
'report_id', v_report_id,
|
||||
'business_date', v_business_date,
|
||||
'completed_txn_count', coalesce(v_completed_txn_count, 0),
|
||||
'gross_usd', coalesce(v_gross_usd, 0),
|
||||
'gross_lbp', coalesce(v_gross_lbp, 0),
|
||||
'activity_count', coalesce(v_activity_count, 0)
|
||||
)
|
||||
);
|
||||
|
||||
return query
|
||||
select
|
||||
r.id,
|
||||
r.business_date,
|
||||
r.submitted_at,
|
||||
r.completed_txn_count,
|
||||
r.gross_usd,
|
||||
r.gross_lbp,
|
||||
r.activity_count
|
||||
from app.end_of_day_reports r
|
||||
where r.id = v_report_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke all on function app.submit_end_of_day(uuid, text) from public;
|
||||
grant execute on function app.submit_end_of_day(uuid, text) to authenticated;
|
||||
|
||||
create or replace view app.v_owner_dashboard as
|
||||
with latest_eod as (
|
||||
select distinct on (shop_id)
|
||||
shop_id,
|
||||
submitted_at
|
||||
from app.end_of_day_reports
|
||||
order by shop_id, submitted_at desc
|
||||
),
|
||||
periods as (
|
||||
select
|
||||
s.id as shop_id,
|
||||
coalesce(
|
||||
le.submitted_at,
|
||||
date_trunc('day', now() at time zone 'Asia/Beirut') at time zone 'Asia/Beirut'
|
||||
) as period_started_at,
|
||||
le.submitted_at as last_end_of_day_at
|
||||
from app.shops s
|
||||
left join latest_eod le on le.shop_id = s.id
|
||||
)
|
||||
select
|
||||
s.id as shop_id,
|
||||
s.name as shop_name,
|
||||
(select count(*) from app.shifts where shop_id = s.id and status = 'open') as open_shifts,
|
||||
(select count(*) from app.alerts where shop_id = s.id and acknowledged_at is null) as open_alerts,
|
||||
(select count(*) from app.alerts where shop_id = s.id and acknowledged_at is null and severity = 'critical') as critical_alerts,
|
||||
(select count(*)
|
||||
from app.reconciliation_exceptions e
|
||||
join app.settlements st on st.id = e.settlement_id
|
||||
where st.shop_id = s.id
|
||||
and e.resolved_at is null) as open_recon_exceptions,
|
||||
(select coalesce(sum(t.gross_usd), 0)
|
||||
from app.transactions t
|
||||
where t.shop_id = s.id
|
||||
and t.status = 'completed'
|
||||
and t.occurred_at >= p.period_started_at) as today_gross_usd,
|
||||
(select coalesce(sum(t.gross_lbp), 0)
|
||||
from app.transactions t
|
||||
where t.shop_id = s.id
|
||||
and t.status = 'completed'
|
||||
and t.occurred_at >= p.period_started_at) as today_gross_lbp,
|
||||
p.period_started_at,
|
||||
p.last_end_of_day_at
|
||||
from app.shops s
|
||||
join periods p on p.shop_id = s.id;
|
||||
Reference in New Issue
Block a user