253 lines
13 KiB
Markdown
253 lines
13 KiB
Markdown
# 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 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.
|