Notify requester distinctly when receiver cancels their claimed order (Task #67)

Context
- After Task #64 receivers can cancel an order they have already claimed.
- Previously the requester's cancellation notification body was the same regardless of who cancelled (admin vs receiver), giving them no signal that their order had been claimed and then dropped.

Changes
- artifacts/api-server/src/routes/service-orders.ts (PATCH /orders/:id/status, cancel branch):
  - When the cancellation is initiated by the order's currently assigned receiver
    (existing.assignedTo === userId && next === "cancelled"), the requester's
    notification body now includes the bilingual phrase
    "استلم طلبك ولكن تم إلغاؤه لاحقاً" / "Your order was claimed but later
    cancelled by the receiver", appended to the service name for context.
  - Admin / non-assignee cancellations keep the existing service-name body so
    the two cases remain distinguishable in the requester's notifications list.
  - The notification is created via the existing notifyUser() helper, which
    persists to notifications and emits notification_created over Socket.IO,
    so the bell badge updates in real time.
- artifacts/api-server/tests/service-orders.test.mjs:
  - Added "requester gets a distinct notification when the receiver cancels a
    claimed order" covering the full flow: place → confirm-receipt → cancel,
    then asserts the owner has a cancellation notification whose body matches
    the new bilingual receiver-cancel copy.

Verification
- All 7 tests in service-orders.test.mjs pass locally against the running API.

Notes / deviations
- Locale JSON files (ar.json/en.json) were inspected but not modified: notification
  copy is stored server-side as titleAr/titleEn/bodyAr/bodyEn on the notifications
  row; the client just renders those strings directly.

Replit-Task-Id: 5aa480a9-c6e0-4a06-8919-b2b6784d8a98
This commit is contained in:
riyadhafraa
2026-04-22 06:28:29 +00:00
parent 0672da245a
commit 6c4c5600de
2 changed files with 47 additions and 2 deletions
@@ -348,12 +348,19 @@ router.patch(
cancelled: ["تم إلغاء طلبك", "Your order was cancelled"],
};
const [titleAr, titleEn] = titleMap[next];
let bodyAr = enriched.service.nameAr;
let bodyEn = enriched.service.nameEn;
// Distinguish receiver-initiated cancel from admin-initiated cancel
if (next === "cancelled" && existing.assignedTo === userId) {
bodyAr = `${enriched.service.nameAr} — استلم طلبك ولكن تم إلغاؤه لاحقاً`;
bodyEn = `${enriched.service.nameEn} — Your order was claimed but later cancelled by the receiver`;
}
await notifyUser(
enriched.userId,
titleAr,
titleEn,
enriched.service.nameAr,
enriched.service.nameEn,
bodyAr,
bodyEn,
orderId,
);
}
@@ -259,6 +259,44 @@ test("status transitions matrix: receiver flows + owner/admin cancel rules", asy
assert.equal((await res.json()).status, "cancelled");
});
test("requester gets a distinct notification when the receiver cancels a claimed order", async () => {
const owner = await createUser("ownN", "user");
const recv = await createUser("recvN", "order_receiver");
const co = await login(owner.username);
const cr = await login(recv.username);
let res = await call(co, "POST", "/orders", { serviceId });
assert.equal(res.status, 201);
const order = await res.json();
createdOrderIds.push(order.id);
// Receiver claims, then cancels
res = await call(cr, "PATCH", `/orders/${order.id}/confirm-receipt`);
assert.equal(res.status, 200);
res = await call(cr, "PATCH", `/orders/${order.id}/status`, {
status: "cancelled",
});
assert.equal(res.status, 200);
// Owner should have a cancellation notification with the receiver-cancel body
const { rows } = await pool.query(
`SELECT title_ar, title_en, body_ar, body_en
FROM notifications
WHERE user_id = $1 AND related_type = 'order' AND related_id = $2
ORDER BY id DESC`,
[owner.id, order.id],
);
const cancelNotif = rows.find(
(n) => n.title_en === "Your order was cancelled",
);
assert.ok(cancelNotif, "expected a cancellation notification for the owner");
assert.match(
cancelNotif.body_en,
/claimed but later cancelled by the receiver/,
);
assert.match(cancelNotif.body_ar, /استلم طلبك ولكن تم إلغاؤه لاحقاً/);
});
test("unauthenticated cannot place order", async () => {
const res = await fetch(`${API_BASE}/api/orders`, {
method: "POST",