Files

13 KiB
Raw Permalink Blame History

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/, replayed by server/db/init/01_run_migrations.sh.
  • Auth shim: 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 replaces PostgREST + GoTrue. It issues JWTs via bcrypt + jsonwebtoken, then on every request opens a pooled connection and runs:
    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 is no longer the real Supabase JS client — it is a drop-in shim backed by 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.

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-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.