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;