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
+252
View File
@@ -0,0 +1,252 @@
# CLAUDE.md
Persistent context for Claude / Copilot sessions on this repository.
Read this **first** before making changes.
---
## 1. Project Idea
**CRM OMT — Cash Collection Management System** is a multi-shop POS / cash
control / reconciliation web app aimed at the typical Lebanese cell-phone /
mixed-retail shop.
### The real-world problem we're solving
Most cell shops in Lebanon don't just sell phones and do repairs — they also
operate an **OMT counter and/or a Whish counter** (plus Alfa/Touch recharge,
EDL bill payments, FX swap between USD and LBP, etc.) as a side service for
walk-in customers. That side service moves a lot of cash through the till
every day, in two currencies, across multiple employees and shifts.
What owners actually struggle with:
- **Cash shortages / "incompatible cash" at end of shift** — the drawer doesn't
match what the system says it should hold.
- **No clear accountability per cashier / per shift** — when money is missing,
it's not obvious *whose* shift it disappeared on.
- **Mixed streams in one drawer** — repair income, item sales, OMT send/receive,
Whish in/out, recharges, bill payments, FX swaps — all flowing through the
same physical cash, in USD *and* LBP, with no separation.
- **Carry-forward shortfalls** — a cashier comes up short one day; the owner
needs that shortfall to roll forward and be cleared by future deposits, not
silently forgotten.
- **Owner has no live visibility** — they want to know, at any moment, how much
cash *should* be in each till and in the safe, who owes what, and where the
variance is.
This app addresses exactly that: it gives the **shop owner** a tool to track
every money movement (repair, sale, OMT, Whish, recharge, bill, FX, deposit,
withdrawal, refund, void) per **employee** and per **shift**, enforce the
accounting rules in the database (atomic sale coupling, sign guards, void
reverses movements, idempotency), and surface variance + outstanding balances
so losses are caught the same day instead of weeks later.
### Feature areas
- **Shift control** — open/close shifts, opening floats, midday drops, end-of-day variance.
- **POS / Transaction entry** — record service sales, fees, FX swaps, customer KYC.
- **Cashier tools** — deposits, withdrawals, refunds, voids, overrides.
- **Manager console** — approvals, fee schedule, fx rates, safe & bank ledger, seed data.
- **User management** — roles (admin / manager / cashier), assignments per shop & shift.
- **Reporting** — outstanding employee balances (carry-forward shortfall logic),
detailed employee payment reports, reconciliation views.
The accounting model is enforced server-side in PostgreSQL: atomic sale coupling,
cash movement sign guard, void reverses movements, idempotency keys, RLS by
shop/role, etc. (See `supabase/migrations/00xx_*.sql`.)
---
## 2. Origin Note — Supabase ➜ self-hosted Postgres
> The project was originally scaffolded on **Supabase** (Lovable / `vite_react_shadcn_ts`
> template, `@supabase/supabase-js` client, `auth.users`, RLS using `request.jwt.claim.*`).
>
> It has since been **transformed into a self-hosted PostgreSQL stack**:
>
> - **DB:** local `postgres:16-alpine` via `docker-compose.yml`, data in named volume `dbdata`.
> Schema is the original Supabase migrations under [`supabase/migrations/`](supabase/migrations/),
> replayed by [`server/db/init/01_run_migrations.sh`](server/db/init/01_run_migrations.sh).
> - **Auth shim:** [`server/db/init/00_auth_shim.sql`](server/db/init/00_auth_shim.sql)
> recreates the `auth.users` table + `auth.uid()` / `auth.role()` / `auth.jwt()` SQL
> helpers that the migrations expect, so the original RLS policies keep working.
> - **API:** a small Express server at [`server/src/index.js`](server/src/index.js)
> replaces PostgREST + GoTrue. It issues JWTs via `bcrypt` + `jsonwebtoken`,
> then on every request opens a pooled connection and runs:
> ```sql
> SELECT set_config('request.jwt.claim.sub', $user_id, true);
> SELECT set_config('request.jwt.claim.role', $role, true);
> SELECT set_config('request.jwt.claims', $claims, true);
> SET LOCAL ROLE authenticated;
> ```
> so RLS continues to evaluate exactly as it did on Supabase.
> - **Frontend shim:** [`src/integrations/supabase/client.ts`](src/integrations/supabase/client.ts)
> is no longer the real Supabase JS client — it is a **drop-in shim** backed by
> [`src/lib/api.ts`](src/lib/api.ts) that exposes the same surface
> (`supabase.auth.*`, `supabase.rpc(...)`, `supabase.from(view).select().eq(...)`).
> This is why existing components keep importing `@/integrations/supabase/client`
> even though there is no Supabase anymore.
- **`@supabase/supabase-js`** has been removed from the project. The shim at
`@/integrations/supabase/client` is now the only "supabase" surface.
> - **Seed admin** is created by [`server/db/init/99_seed_admin.sh`](server/db/init/99_seed_admin.sh)
> from `ADMIN_EMAIL` / `ADMIN_PASSWORD` / `ADMIN_NAME` env vars in `docker-compose.yml`.
**Implication for any future work:** treat `supabase/*` as the **source of truth
for the schema only**. Do not reintroduce calls to a hosted Supabase. New
endpoints must be added to `server/src/index.js` (and, if used as RPCs, exposed
via `app.<fn>(...)` SQL functions so they go through the generic `/rpc/:fn` route).
---
## 3. Repository Layout
```
cash-collection-management-system/
├── docker-compose.yml # postgres:16-alpine + volume + init scripts
├── index.html # Vite entry
├── vite.config.ts
├── package.json # frontend (Vite + React 18 + TS + shadcn/ui + tailwind)
├── server/
│ ├── package.json # express, pg, bcrypt, jsonwebtoken, cors, dotenv
│ ├── .env(.example) # DATABASE_URL, JWT_SECRET, CORS_ORIGIN, PORT
│ ├── src/index.js # the entire Express API (auth, /rpc/:fn, /from/:view, employees…)
│ └── db/init/ # postgres docker-entrypoint-initdb.d
│ ├── 00_auth_shim.sql # recreates auth.users + auth.* helpers
│ ├── 01_run_migrations.sh # replays /sql/migrations/*.sql in order
│ ├── 50_employee_payments.sql # extra app-layer tables for the payment report
│ └── 99_seed_admin.sh # creates first admin from env vars
├── supabase/
│ ├── config.toml # legacy, unused at runtime
│ └── migrations/ # 0001…0026 — schema + RLS + RPCs (source of truth)
├── src/
│ ├── main.tsx, App.tsx, index.css
│ ├── pages/ # Index.tsx (tabbed shell), NotFound.tsx
│ ├── components/ # Feature components (see §4)
│ │ └── ui/ # shadcn primitives — only the ones actually used
│ ├── hooks/
│ │ ├── useAuth.tsx
│ │ ├── useSupabaseEmployeeData.ts # primary data hook (reads/writes via api shim)
│ │ ├── useEmployeeData.ts # legacy adapter, kept for EmployeePaymentReport
│ │ └── use-toast.ts
│ ├── integrations/supabase/
│ │ └── client.ts # SHIM over src/lib/api.ts — NOT real supabase-js
│ └── lib/
│ ├── api.ts # fetch wrapper around the Express server
│ ├── services.ts # POS service catalogue (OMT_SEND, WHISH_SEND, …)
│ ├── currency.ts # USD/LBP conversion + formatting
│ └── utils.ts # cn() helper
└── docs/
└── THREAT_MODEL.md
```
---
## 4. Feature Components → DB
| Component | Talks to |
| ------------------------------------- | ---------------------------------------------------------- |
| `LoginPage` | `POST /auth/login` (Express) → JWT in localStorage |
| `OwnerOverview` | `v_owner_dashboard`, `v_z_report`, `v_employee_scorecard_30d`, `alerts` (read), `ack_alert` RPC |
| `ShiftControl` | `supabase.rpc(...)` shift open/declare/finalize + Expected/Counted/Δ panel from `finalize_close` |
| `TransactionEntry` | `supabase.rpc(...)` atomic sale + cash movement RPCs |
| `CashierTools` | `supabase.rpc(...)` deposits / withdrawals / refunds |
| `ManagerConsole` | RPCs for fee schedule, fx rates, safe/bank ledger, seeds |
| `UserManagement` | `useSupabaseEmployeeData` + admin RPCs (`get_shop_users`) |
| `OutstandingReportDashboard` | `useSupabaseEmployeeData` (`/employees`, `/employee_transactions`) |
| `DetailedEmployeePaymentReport` | same |
| `EmployeePaymentReport` | `useEmployeeData` (legacy adapter over the same data) |
| `AdminDataEntryModal` | `useSupabaseEmployeeData.addTransaction(...)` |
---
## 5. How to Run
```bash
# one-time
cp server/.env.example server/.env # edit JWT_SECRET if exposed
npm install
npm --prefix server install
# day-to-day (DB + API + Web all together)
npm run dev:all
# DB → docker container crm_omt_db on :5432
# API → node server on :4000
# WEB → vite on :5173 (or :8080)
# build frontend
npm run build
# wipe & rebuild DB (re-runs all migrations + seeds admin)
npm run db:reset
```
Default seed admin (override via env in `docker-compose.yml`):
- email: `admin@local.test`
- password: `ChangeMe123!`
---
## 6. Conventions / Gotchas
- **Don't import `@supabase/supabase-js` directly.** Use `@/integrations/supabase/client`
(the shim) or `@/lib/api` (raw). The package will be removed.
- **Don't put business logic in the Express server.** All money-touching logic
must live in SQL functions under the `app.*` schema (see migrations 00180026)
and be invoked through `/rpc/:fn`. The Express layer only authenticates and
forwards args.
- **RLS depends on JWT claims being set per-connection.** Any new endpoint that
hits a tenant-scoped table must use `withUserClient(req, ...)` in
`server/src/index.js`, not `pool.query` directly.
- **New views exposed via `supabase.from(view)`** must be added to `ALLOWED_VIEWS`
in `server/src/index.js`.
- **shadcn/ui:** only the components actually imported by feature code live in
`src/components/ui/`. If you need another primitive, add it back from
https://ui.shadcn.com — don't restore a kitchen-sink set.
- **Currencies:** USD is the canonical store; LBP is derived via
`getUsdToLbpRate()` in `src/lib/currency.ts`.
---
## 7. Recent Cleanup (2026-05)
**Cleanup pass:**
- Root one-off codegen scripts: `rewrite_pos.py`, `update_assign_shift.py`,
`update_shift_control.py`, `update_shiftcontrol_rpc.py`.
- `bun.lockb` (project uses npm).
- `src/App.css` (Vite template leftover, not imported).
- `src/integrations/supabase/types.ts` (Supabase-generated types, unused since the shim).
- `src/hooks/use-mobile.tsx` (only consumed by the now-removed `sidebar` UI).
- Unused shadcn primitives in `src/components/ui/`:
`accordion, alert, alert-dialog, aspect-ratio, avatar, badge, breadcrumb,
carousel, chart, checkbox, collapsible, command, context-menu, drawer,
dropdown-menu, form, hover-card, input-otp, menubar, navigation-menu,
pagination, progress, radio-group, resizable, scroll-area, separator,
sheet, sidebar, skeleton, slider, switch, toggle, toggle-group`.
- `@supabase/supabase-js` uninstalled.
**Owner-facing additions (against the stated problem):**
- `ShiftControl` now displays an **Expected / Counted / Δ** panel right after
finalize-close, in USD and LBP, color-coded by short / over / match.
- New **`OwnerOverview`** component, wired as the default tab for `admin`
users. Reads:
- `app.v_owner_dashboard` — per-shop open shifts, open alerts, today's gross.
- `app.v_z_report` — recent closed-shift variances.
- `app.v_employee_scorecard_30d` — 30-day per-cashier variance + voids.
- `app.alerts` — open alerts with one-click acknowledge via `app.ack_alert`.
- Exposed those views/tables in `ALLOWED_VIEWS` in `server/src/index.js`.
RLS continues to scope rows.
**Known follow-ups still on the list:**
- Add `WHISH_RECEIVE` (needs new migration: service row + record-receive RPC
+ details table or a generic receive path). Today only `WHISH_SEND` is wired.
- Roll real shift variance into the per-cashier outstanding/carry-forward
ledger so the Outstanding Report reflects POS reality, not just manual entries.
- Cashier-side "live drawer" widget while a shift is open
(expected-vs-recorded by stream). Needs a small `app.live_drawer(p_shift_id)` RPC.
- Inventory + repair-ticket UI (DB seeds exist via `0014_product_catalog.sql`).
`npm run build` is green.