Files
OMT-SM/supabase/migrations/0031_end_of_day_reports.sql
T

310 lines
9.1 KiB
PL/PgSQL

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;