# 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.(...)` 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 0018–0026) 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.