Add cash management schema and immediate variance alerts

This commit is contained in:
Krikorios
2026-05-06 10:51:55 +03:00
parent 1a3de58de6
commit 1896cbdd11
106 changed files with 16800 additions and 4604 deletions
+63
View File
@@ -0,0 +1,63 @@
-- =====================================================================
-- 0000 Auth shim
-- Provides `auth.users`, `auth.uid()`, `auth.role()`, `auth.jwt()` so
-- the application migrations (which were written for Supabase) run
-- unmodified. The backend sets `request.jwt.claim.sub` (and friends)
-- per request from the verified JWT, then `set local role authenticated`.
-- =====================================================================
create extension if not exists "pgcrypto";
create extension if not exists "citext";
-- Supabase ships these roles; create them if missing (e.g. plain Postgres).
do $$ begin
if not exists (select 1 from pg_roles where rolname = 'anon') then
create role anon nologin noinherit;
end if;
if not exists (select 1 from pg_roles where rolname = 'authenticated') then
create role authenticated nologin noinherit;
end if;
if not exists (select 1 from pg_roles where rolname = 'service_role') then
create role service_role nologin noinherit bypassrls;
end if;
end $$;
create schema if not exists auth;
-- Minimal `auth.users` compatible with FKs in app migrations.
create table if not exists auth.users (
id uuid primary key default gen_random_uuid(),
email citext unique,
password_hash text not null,
full_name text,
is_active boolean not null default true,
created_at timestamptz not null default now(),
last_login_at timestamptz
);
create or replace function auth.uid()
returns uuid
language sql
stable
as $$
select nullif(current_setting('request.jwt.claim.sub', true), '')::uuid
$$;
create or replace function auth.role()
returns text
language sql
stable
as $$
select coalesce(nullif(current_setting('request.jwt.claim.role', true), ''), 'anon')
$$;
create or replace function auth.jwt()
returns jsonb
language sql
stable
as $$
select coalesce(nullif(current_setting('request.jwt.claims', true), '')::jsonb, '{}'::jsonb)
$$;
grant usage on schema auth to authenticated, anon, service_role;
grant select on auth.users to authenticated, service_role;
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Run all SQL migrations from /sql/migrations/ in lexical order.
# On plain Postgres (no pg_cron extension), wrap any bare
# `create extension if not exists pg_cron;` line so it doesn't abort.
set -euo pipefail
TMPDIR_M=/tmp/migrations
mkdir -p "$TMPDIR_M"
echo ">> applying app migrations from /sql/migrations"
for f in /sql/migrations/*.sql; do
base="$(basename "$f")"
# Replace the bare pg_cron extension creation with a soft variant.
sed -E "s|^create extension if not exists pg_cron;|do \$\$ begin create extension if not exists pg_cron; exception when others then raise notice 'pg_cron unavailable, skipping schedules'; end \$\$;|i" "$f" > "$TMPDIR_M/$base"
echo ">> $base"
psql -v ON_ERROR_STOP=1 \
--username "$POSTGRES_USER" \
--dbname "$POSTGRES_DB" \
-f "$TMPDIR_M/$base"
done
echo ">> migrations complete"
+32
View File
@@ -0,0 +1,32 @@
-- =====================================================================
-- Local extension migration: simple employee payment ledger used by the
-- Employee Payment Report UI. Backed by the API; not a Supabase migration.
-- =====================================================================
create table if not exists app.employees (
id uuid primary key default gen_random_uuid(),
emp_id text not null unique,
name text not null,
email text,
department text,
location text,
created_at timestamptz not null default now()
);
create table if not exists app.employee_transactions (
id uuid primary key default gen_random_uuid(),
employee_id uuid not null references app.employees(id) on delete cascade,
transaction_date date not null,
collection_amount numeric(18,2) not null default 0,
deposit_amount numeric(18,2) not null default 0,
currency text not null check (currency in ('USD','LBP')),
created_at timestamptz not null default now()
);
create index if not exists idx_emp_tx_emp on app.employee_transactions(employee_id, transaction_date desc);
-- These tables are owned by the API; RLS off, gated at the HTTP layer.
alter table app.employees disable row level security;
alter table app.employee_transactions disable row level security;
grant select, insert, update, delete on app.employees to authenticated;
grant select, insert, update, delete on app.employee_transactions to authenticated;
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Seed (or reset) the default admin user, default shop, owner role, Till 1.
set -euo pipefail
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@local.test}"
ADMIN_PASSWORD="${ADMIN_PASSWORD:-ChangeMe123!}"
ADMIN_NAME="${ADMIN_NAME:-Local Admin}"
echo ">> seeding admin user: ${ADMIN_EMAIL}"
# Use psql -v to safely substitute values inside the DO block via :'name' --
# but :'name' only works at top level. So we generate plain SQL with the
# values inlined as quoted literals (escaping single quotes).
escape() { printf "%s" "$1" | sed "s/'/''/g"; }
EM=$(escape "$ADMIN_EMAIL")
PW=$(escape "$ADMIN_PASSWORD")
NM=$(escape "$ADMIN_NAME")
psql -v ON_ERROR_STOP=1 \
--username "$POSTGRES_USER" \
--dbname "$POSTGRES_DB" <<SQL
do \$\$
declare
v_user_id uuid;
v_shop_id uuid;
begin
insert into auth.users(email, password_hash, full_name, is_active)
values ('${EM}', crypt('${PW}', gen_salt('bf', 10)), '${NM}', true)
on conflict (email) do update
set password_hash = excluded.password_hash,
full_name = excluded.full_name,
is_active = true
returning id into v_user_id;
insert into app.user_profiles(user_id, full_name, is_active)
values (v_user_id, '${NM}', true)
on conflict (user_id) do update
set full_name = excluded.full_name,
is_active = true;
insert into app.shops(name, created_by)
values ('Default Shop', v_user_id)
on conflict do nothing;
select id into v_shop_id from app.shops where name = 'Default Shop' limit 1;
insert into app.user_shop_assignments(user_id, shop_id, role, assigned_by)
values (v_user_id, v_shop_id, 'owner', v_user_id)
on conflict (user_id, shop_id) do update set role = 'owner';
insert into app.tills(shop_id, name)
values (v_shop_id, 'Till 1')
on conflict (shop_id, name) do nothing;
end \$\$;
SQL
echo ">> admin user ensured: ${ADMIN_EMAIL}"