13 KiB
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_tstemplate,@supabase/supabase-jsclient,auth.users, RLS usingrequest.jwt.claim.*).It has since been transformed into a self-hosted PostgreSQL stack:
- DB: local
postgres:16-alpineviadocker-compose.yml, data in named volumedbdata. Schema is the original Supabase migrations undersupabase/migrations/, replayed byserver/db/init/01_run_migrations.sh.- Auth shim:
server/db/init/00_auth_shim.sqlrecreates theauth.userstable +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.jsreplaces PostgREST + GoTrue. It issues JWTs viabcrypt+jsonwebtoken, then on every request opens a pooled connection and runs:so RLS continues to evaluate exactly as it did on Supabase.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;- Frontend shim:
src/integrations/supabase/client.tsis no longer the real Supabase JS client — it is a drop-in shim backed bysrc/lib/api.tsthat exposes the same surface (supabase.auth.*,supabase.rpc(...),supabase.from(view).select().eq(...)). This is why existing components keep importing@/integrations/supabase/clienteven though there is no Supabase anymore.
@supabase/supabase-jshas been removed from the project. The shim at@/integrations/supabase/clientis now the only "supabase" surface.
- Seed admin is created by
server/db/init/99_seed_admin.shfromADMIN_EMAIL/ADMIN_PASSWORD/ADMIN_NAMEenv vars indocker-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
# 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-jsdirectly. 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, ...)inserver/src/index.js, notpool.querydirectly. - New views exposed via
supabase.from(view)must be added toALLOWED_VIEWSinserver/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()insrc/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-removedsidebarUI).- 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-jsuninstalled.
Owner-facing additions (against the stated problem):
ShiftControlnow displays an Expected / Counted / Δ panel right after finalize-close, in USD and LBP, color-coded by short / over / match.- New
OwnerOverviewcomponent, wired as the default tab foradminusers. 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 viaapp.ack_alert.
- Exposed those views/tables in
ALLOWED_VIEWSinserver/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_SENDis wired.
- details table or a generic receive path). Today only
- 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.