Files

253 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.