import { Router, Request, Response } from "express"; import { requests, donors, whatsappLog, checkEligibility, STATUS_STEP, DonationRequest, WhatsappLogEntry, } from "../lib/mockDb.js"; import { v4 as uuidv4 } from "uuid"; const router = Router(); // ─── GET /requests ──────────────────────────────────────────────────────────── router.get("/requests", (req: Request, res: Response): void => { let result = [...requests]; const { status, needType } = req.query; if (status) result = result.filter((r) => r.status === status); if (needType) result = result.filter((r) => r.needType === needType); res.json(result); }); // ─── GET /requests/new ─────────────────────────────────────────────────────── router.get("/requests/new", (_req: Request, res: Response): void => { res.json(requests.filter((r) => r.status === "new")); }); // ─── GET /requests/published ───────────────────────────────────────────────── router.get("/requests/published", (_req: Request, res: Response): void => { res.json(requests.filter((r) => r.status === "published")); }); // ─── POST /requests ─────────────────────────────────────────────────────────── router.post("/requests", (req: Request, res: Response): void => { const { beneficiaryName, nationalId, phone, source, sourceName, needType, requestedAmount, description, } = req.body; if ( !beneficiaryName || !nationalId || !phone || !source || !sourceName || !needType || !requestedAmount || !description ) { res.status(400).json({ error: "Missing required fields" }); return; } const { eligible } = checkEligibility(nationalId); let status: DonationRequest["status"]; if (eligible === true) { status = "verified"; } else if (eligible === false) { status = "rejected"; } else { status = "pending_review"; } const now = new Date().toISOString(); const caseNum = String(requests.length + 1).padStart(3, "0"); const newReq: DonationRequest = { id: uuidv4(), caseId: `CASE-${caseNum}`, beneficiaryName, nationalId, phone, source, sourceName, needType, requestedAmount: Number(requestedAmount), collectedAmount: 0, description, status, currentStep: STATUS_STEP[status], donorId: null, donorName: null, thankYouMessage: null, whatsappStatus: null, whatsappSentAt: null, rejectionReason: status === "rejected" ? "المستفيد غير مؤهل وفق قاعدة بيانات الاستحقاق" : null, createdAt: now, updatedAt: now, }; requests.push(newReq); res.status(201).json(newReq); }); // ─── GET /requests/:id ──────────────────────────────────────────────────────── router.get("/requests/:id", (req: Request, res: Response): void => { const item = requests.find( (r) => r.id === req.params.id || r.caseId === req.params.id ); if (!item) { res.status(404).json({ error: "Not found" }); return; } res.json(item); }); // ─── Helper: find & update ──────────────────────────────────────────────────── function findAndUpdate( id: string | string[], updater: (r: DonationRequest) => void, res: Response ): void { const item = requests.find((r) => r.id === id || r.caseId === id); if (!item) { res.status(404).json({ error: "Not found" }); return; } updater(item); item.updatedAt = new Date().toISOString(); res.json(item); } // ─── POST /requests/:id/verify ──────────────────────────────────────────────── router.post("/requests/:id/verify", (req: Request, res: Response): void => { findAndUpdate( req.params.id, (r) => { r.status = "verified"; r.currentStep = STATUS_STEP["verified"]; }, res ); }); // ─── POST /requests/:id/publish ─────────────────────────────────────────────── router.post("/requests/:id/publish", (req: Request, res: Response): void => { findAndUpdate( req.params.id, (r) => { r.status = "published"; r.currentStep = STATUS_STEP["published"]; }, res ); }); // ─── POST /requests/:id/donate ──────────────────────────────────────────────── router.post("/requests/:id/donate", (req: Request, res: Response): void => { const item = requests.find( (r) => r.id === req.params.id || r.caseId === req.params.id ); if (!item) { res.status(404).json({ error: "Not found" }); return; } const { donorName, donorPhone, donorEmail, amount } = req.body; const amt = Number(amount); if (!donorName || !donorPhone || !Number.isFinite(amt) || amt <= 0) { res.status(400).json({ error: "Invalid donor details or amount" }); return; } if (item.status !== "published") { res.status(400).json({ error: "Case is not open for donations" }); return; } const remaining = Math.max(0, item.requestedAmount - item.collectedAmount); if (remaining <= 0) { res.status(400).json({ error: "Case already fully funded" }); return; } // Clamp the donation so the collected amount never exceeds the target. const applied = Math.min(amt, remaining); const donorId = uuidv4(); const existingDonor = donors.find((d) => d.phone === donorPhone); if (existingDonor) { existingDonor.totalDonated += applied; existingDonor.donationCount += 1; item.donorId = existingDonor.id; } else { const newDonor = { id: donorId, name: donorName, phone: donorPhone, email: donorEmail || null, totalDonated: applied, donationCount: 1, }; donors.push(newDonor); item.donorId = donorId; } item.donorName = donorName; item.collectedAmount += applied; // Only advance into the closed-loop pipeline once the case is fully funded. if (item.collectedAmount >= item.requestedAmount) { item.status = "donated"; item.currentStep = STATUS_STEP["donated"]; } item.updatedAt = new Date().toISOString(); res.json(item); }); // ─── POST /requests/:id/deliver ─────────────────────────────────────────────── router.post("/requests/:id/deliver", (req: Request, res: Response): void => { findAndUpdate( req.params.id, (r) => { r.status = "delivered"; r.currentStep = STATUS_STEP["delivered"]; }, res ); }); // ─── POST /requests/:id/confirm-receipt ─────────────────────────────────────── router.post( "/requests/:id/confirm-receipt", (req: Request, res: Response): void => { findAndUpdate( req.params.id, (r) => { r.status = "receipt_confirmed"; r.currentStep = STATUS_STEP["receipt_confirmed"]; }, res ); } ); // ─── POST /requests/:id/thank-you ───────────────────────────────────────────── router.post("/requests/:id/thank-you", (req: Request, res: Response): void => { const item = requests.find( (r) => r.id === req.params.id || r.caseId === req.params.id ); if (!item) { res.status(404).json({ error: "Not found" }); return; } const { message, beneficiaryName } = req.body; if (!message) { res.status(400).json({ error: "Message is required" }); return; } // Update beneficiary name if provided if (beneficiaryName) { item.beneficiaryName = beneficiaryName; } item.thankYouMessage = message; item.status = "thank_you_submitted"; item.currentStep = STATUS_STEP["thank_you_submitted"]; item.whatsappStatus = "pending"; item.updatedAt = new Date().toISOString(); // Create whatsapp log entry const logEntry: WhatsappLogEntry = { id: uuidv4(), caseId: item.caseId, donorName: item.donorName || "متبرع", donorPhone: donors.find((d) => d.id === item.donorId)?.phone || "", beneficiaryMessage: message, whatsappMessage: `السلام عليكم، نشكركم على تبرعكم عبر منصة إحسان.\nتم إيصال الدعم للمستفيد، وهذه رسالة الشكر من المستفيد:\n"${message}"\nرقم الحالة: ${item.caseId}`, status: "pending", sentAt: null, createdAt: new Date().toISOString(), }; whatsappLog.push(logEntry); res.json(item); }); // ─── POST /requests/:id/send-whatsapp ───────────────────────────────────────── router.post( "/requests/:id/send-whatsapp", async (req: Request, res: Response): Promise => { const item = requests.find( (r) => r.id === req.params.id || r.caseId === req.params.id ); if (!item) { res.status(404).json({ error: "Not found" }); return; } const simulate = process.env.OPENCLAW_SIMULATE !== "false"; const openclawUrl = process.env.OPENCLAW_BASE_URL || "http://localhost:3100"; const logEntry = whatsappLog.find((w) => w.caseId === item.caseId); const now = new Date().toISOString(); if (simulate) { item.whatsappStatus = "sent"; item.whatsappSentAt = now; item.status = "whatsapp_sent"; item.currentStep = STATUS_STEP["whatsapp_sent"]; item.updatedAt = now; if (logEntry) { logEntry.status = "sent"; logEntry.sentAt = now; } res.json({ success: true, message: "WhatsApp sent (simulated)", simulated: true, sentAt: now, }); return; } // Live mode — call OpenClaw try { const donor = donors.find((d) => d.id === item.donorId); const payload = { to: donor?.phone || "", message: logEntry?.whatsappMessage || "", caseId: item.caseId, }; const response = await fetch(`${openclawUrl}/api/whatsapp/send`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!response.ok) { throw new Error(`OpenClaw returned ${response.status}`); } item.whatsappStatus = "sent"; item.whatsappSentAt = now; item.status = "whatsapp_sent"; item.currentStep = STATUS_STEP["whatsapp_sent"]; item.updatedAt = now; if (logEntry) { logEntry.status = "sent"; logEntry.sentAt = now; } res.json({ success: true, message: "WhatsApp sent via OpenClaw", simulated: false, sentAt: now, }); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Unknown error"; item.whatsappStatus = "failed"; item.updatedAt = now; if (logEntry) { logEntry.status = "failed"; } res.json({ success: false, message: `Failed to send via OpenClaw: ${message}`, simulated: false, sentAt: null, }); } } ); // ─── POST /requests/:id/close ───────────────────────────────────────────────── router.post("/requests/:id/close", (req: Request, res: Response): void => { findAndUpdate( req.params.id, (r) => { r.status = "closed"; r.currentStep = STATUS_STEP["closed"]; }, res ); }); // ─── POST /requests/:id/reject ──────────────────────────────────────────────── router.post("/requests/:id/reject", (req: Request, res: Response): void => { const item = requests.find( (r) => r.id === req.params.id || r.caseId === req.params.id ); if (!item) { res.status(404).json({ error: "Not found" }); return; } item.status = "rejected"; item.currentStep = STATUS_STEP["rejected"]; item.rejectionReason = req.body?.reason || "تم الرفض من قبل المشرف"; item.updatedAt = new Date().toISOString(); res.json(item); }); export default router;