Files
Ehsan/artifacts/api-server/src/routes/requests.ts
T

388 lines
12 KiB
TypeScript
Raw Normal View History

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,
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;
if (!donorName || !donorPhone || !amount) {
res.status(400).json({ error: "Missing donor details" });
return;
}
const donorId = uuidv4();
const existingDonor = donors.find((d) => d.phone === donorPhone);
if (existingDonor) {
existingDonor.totalDonated += Number(amount);
existingDonor.donationCount += 1;
item.donorId = existingDonor.id;
} else {
const newDonor = {
id: donorId,
name: donorName,
phone: donorPhone,
email: donorEmail || null,
totalDonated: Number(amount),
donationCount: 1,
};
donors.push(newDonor);
item.donorId = donorId;
}
item.donorName = donorName;
item.collectedAmount = Number(amount);
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;