# Hammam Dev — EHSAN "Closed Donation Loop" (POC) > Onboarding brief for any developer or AI coding agent (e.g. OpenClaw) taking over > this project. Read it fully before editing. The repo is hosted on a self-hosted > **Gitea** server and runs on a **Mac Mini** via Docker. > > **تنويه:** مشروع **تجريبي** مستوحى من فكرة منصة «إحسان»، وليس منصة إحسان الرسمية > ولا تابعاً لها. ## 1. What this product is A bilingual (Arabic / English, full RTL + LTR) **proof-of-concept charity donation web app** inspired by the Saudi "EHSAN" (إحسان) platform. Its core idea is a **Closed Donation Loop**: - Beneficiaries submit support requests (housing, food, electricity, water, health, court-ordered debt, appliances like A/C & refrigerator, etc.). - Each request/opportunity has a **funding target**. Donations from multiple donors **accumulate and are clamped to the target** — a case can never be over-funded. - A case only **enters the "closed loop" fulfillment pipeline once it is fully funded**. This is the central business rule. Do not break it. - After funding, the flow continues to confirmation / tracking, and a simulated WhatsApp notification log records donor/beneficiary messaging. This is a POC/demo: data is **in-memory mock data** (no real database, no real payments). ## 2. Tech stack - Monorepo: **pnpm workspaces**, Node.js 24, TypeScript 5.9. - Web app (`artifacts/ehsan-poc`): React + Vite + **wouter** (routing) + **TanStack Query** (data) + **Tailwind CSS** + shadcn/ui components. - API (`artifacts/api-server`): **Express 5**, all routes mounted under `/api`, health at `/api/healthz`. Data lives in `src/lib/mockDb.ts` (in-memory; resets on restart). - Shared libs under `lib/`: `api-spec` (OpenAPI source of truth), `api-client-react` (generated React Query hooks), `api-zod` (Zod schemas), `db` (Drizzle schema, not active at runtime since data is in-memory). - All workspace libs export TypeScript source directly (`./src/index.ts`) — no lib pre-build step; Vite/esbuild consume the source. ## 3. Repo map - `artifacts/ehsan-poc/src/pages/` — screens: home, about, waqf, baraem, request, opportunities, donate/:id, cart, login, admin, track/:id, thank-you/:id, whatsapp-log, not-found. - `artifacts/ehsan-poc/src/contexts/` — `LanguageContext` (ar/en + RTL), `CartContext` (multi-case donation cart), `AuthContext` (mock admin login). - `artifacts/ehsan-poc/src/components/` — `Riyal.tsx` (renders the NEW Saudi Riyal symbol via an image mask), `layout/` (Header, AppLayout), `ui/` (shadcn components). - `artifacts/ehsan-poc/src/App.tsx` — UI root: routing (wouter) + all context providers. - `artifacts/api-server/src/routes/` — `health`, `requests`, `donors`, `stats`, `whatsappLog`. Mounted in `routes/index.ts`. - `artifacts/api-server/src/routes/requests.ts` — **the closed-loop logic and every status transition** live here. - `artifacts/api-server/src/lib/mockDb.ts` — types, in-memory seed data, the `STATUS_STEP` map, and `checkEligibility`. - Root deploy files: `Dockerfile.web`, `Dockerfile.api`, `docker/nginx.conf`, `docker-compose.yml`, `deploy.sh`, `scripts/push-to-gitea.sh`, `DEPLOYMENT.md`. ## 4. How to run locally (development) ```bash pnpm install pnpm --filter @workspace/api-server run dev # API (port 5000 in dev) pnpm --filter @workspace/ehsan-poc run dev # Web (needs PORT and BASE_PATH env) pnpm run typecheck # full typecheck pnpm run build # typecheck + build everything ``` Note: `vite.config.ts` REQUIRES `PORT` and `BASE_PATH` env vars even for `build` (it throws otherwise), e.g. `PORT=8080 BASE_PATH=/ pnpm --filter @workspace/ehsan-poc run build`. > **Apple Silicon caveat:** a full `pnpm run build` typechecks all packages but the > `vite build` step fails locally on darwin-arm64 with > `Cannot find module @rollup/rollup-darwin-arm64`. That is **by design** — the > workspace `overrides` in `pnpm-workspace.yaml` strip every non-`linux-x64-gnu` > native binary so the Docker (`linux/amd64`) build stays clean. The real build runs > inside Docker; locally the `typecheck` result is the meaningful gate. ## 5. How it runs in production (Mac Mini, Docker) Two services in `docker-compose.yml`: - `api` — Express, internal only, listens on `PORT=8080`, healthcheck `/api/healthz`. - `web` — nginx that serves the built Vite SPA **and reverse-proxies `/api/` to `api:8080`**. The browser always calls **same-origin `/api/...`**, so there is NO frontend API URL to configure. Deploy / redeploy on the Mac Mini: ```bash ./deploy.sh # git pull gitea main → docker compose down → build → up -d ``` App is then served on the Mac Mini at `http://localhost:8080` (override with `WEB_PORT`). ## 6. Deployment flow (how code travels) ``` Edit code → commit → push to Gitea (branch: main) → on Mac Mini run ./deploy.sh → Docker rebuilds & restarts ``` - Central repo is **Gitea** (no GitHub), branch `main`. The documented remote name is `gitea`; on this Mac Mini checkout the remote may be named `origin` but still points at the Gitea host — verify with `git remote -v`. - `scripts/push-to-gitea.sh` pushes from a dev machine; `deploy.sh` redeploys on the Mac Mini. - An AI agent working directly on the Mac Mini clone should use this loop: edit → test → `pnpm run build` → `git commit` → `git push main` → `./deploy.sh`. ## 7. Status lifecycle (create → close) The case `status` (`RequestStatus`) and its step number (`STATUS_STEP` in `mockDb.ts`): ``` new (1) → pending_review (2) → verified (3) → published (4) → donated (5) ← reached ONLY when fully funded → delivered (6) → receipt_confirmed (7) → thank_you_submitted (8) → whatsapp_sent (9) → closed (10) rejected (side path, step 2) ``` - **Eligibility on creation:** `POST /requests` checks `nationalId` via `checkEligibility` → eligible ⇒ `verified`, not eligible ⇒ `rejected`, unknown ⇒ `pending_review`. - **API transition endpoints:** `verify`, `publish`, `donate`, `deliver`, `confirm-receipt`, `thank-you`, `send-whatsapp`, `close`, `reject` (`POST /requests/:id/`). - **`donate` is the heart of the closed loop:** it only accepts donations while the case is `published`, clamps each donation to the remaining amount (`applied = min(amount, requestedAmount − collectedAmount)`), and advances the case to `donated` only once `collectedAmount >= requestedAmount`. ## 8. Hard rules / gotchas — do NOT break these - **amd64 / glibc ONLY.** The pnpm workspace strips every native binary that is not `linux-x64-gnu`. Docker build stages MUST use `node:24-bookworm-slim` (glibc, not alpine) and `platform: linux/amd64` (runs under Rosetta on Apple Silicon). Do not switch the build base to alpine or arm64 — rollup / tailwind-oxide / lightningcss will fail to find native binaries. - **Funding rule (Closed Donation Loop).** Donations accumulate and clamp to the target; a case can never be over-funded and enters the fulfillment pipeline (`donated` and beyond) only when fully funded. Preserve this. - **Bilingual + RTL.** Every user-facing string must exist in both Arabic and English via `LanguageContext`. Don't hardcode single-language text. Keep RTL layout working. - **Same-origin API.** The browser calls `/api/...` on the same domain (nginx proxies it to the `api` service). Never add a separate frontend API URL. - **Currency.** Saudi Riyal uses the new official symbol rendered by the `` component (image mask), not the old "ر.س"/"SAR" text. Reuse ``. - **Routing base.** The app is mounted under a base path via `import.meta.env.BASE_URL`. Use it for routes/links; never hardcode root-relative `/api` in a way that escapes the base — call same-origin `/api/...` through the proxy. - **Data is ephemeral.** mockDb is in-memory; restarting the `api` container resets it. If you add persistence, add a database service to `docker-compose.yml` accordingly. - **HMR quirk (dev):** if you see "useLanguage must be used within a LanguageProvider" while the code is correct, it's stale Fast Refresh state — restart the web dev server. - Always run `pnpm run build` (typecheck + build) before pushing to catch type errors. ## 9. Suggested first task for a new agent Read `DEPLOYMENT.md`, `artifacts/ehsan-poc/src/App.tsx`, and `artifacts/api-server/src/routes/index.ts` to confirm the routes and data model, then summarize your understanding of the funding / closed-loop flow before making any change.