8.5 KiB
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 insrc/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 inroutes/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, theSTATUS_STEPmap, andcheckEligibility.- 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)
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 buildtypechecks all packages but thevite buildstep fails locally on darwin-arm64 withCannot find module @rollup/rollup-darwin-arm64. That is by design — the workspaceoverridesinpnpm-workspace.yamlstrip every non-linux-x64-gnunative binary so the Docker (linux/amd64) build stays clean. The real build runs inside Docker; locally thetypecheckresult is the meaningful gate.
5. How it runs in production (Mac Mini, Docker)
Two services in docker-compose.yml:
api— Express, internal only, listens onPORT=8080, healthcheck/api/healthz.web— nginx that serves the built Vite SPA and reverse-proxies/api/toapi:8080. The browser always calls same-origin/api/..., so there is NO frontend API URL to configure.
Deploy / redeploy on the Mac Mini:
./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 isgitea; on this Mac Mini checkout the remote may be namedoriginbut still points at the Gitea host — verify withgit remote -v. scripts/push-to-gitea.shpushes from a dev machine;deploy.shredeploys 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 <gitea-remote> 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 /requestschecksnationalIdviacheckEligibility→ 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/<action>). donateis the heart of the closed loop: it only accepts donations while the case ispublished, clamps each donation to the remaining amount (applied = min(amount, requestedAmount − collectedAmount)), and advances the case todonatedonly oncecollectedAmount >= 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 usenode:24-bookworm-slim(glibc, not alpine) andplatform: 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
(
donatedand 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 theapiservice). Never add a separate frontend API URL. - Currency. Saudi Riyal uses the new official symbol rendered by the
<Riyal/>component (image mask), not the old "ر.س"/"SAR" text. Reuse<Riyal/>. - 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/apiin a way that escapes the base — call same-origin/api/...through the proxy. - Data is ephemeral. mockDb is in-memory; restarting the
apicontainer resets it. If you add persistence, add a database service todocker-compose.ymlaccordingly. - 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.