407 lines
13 KiB
TypeScript
407 lines
13 KiB
TypeScript
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<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 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;
|