From 6c4c5600de248eabe56883baf5567093baa8dbdf Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Wed, 22 Apr 2026 06:28:29 +0000 Subject: [PATCH] Notify requester distinctly when receiver cancels their claimed order (Task #67) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../api-server/src/routes/service-orders.ts | 11 +++++- .../api-server/tests/service-orders.test.mjs | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/artifacts/api-server/src/routes/service-orders.ts b/artifacts/api-server/src/routes/service-orders.ts index d1988e49..20a56aa5 100644 --- a/artifacts/api-server/src/routes/service-orders.ts +++ b/artifacts/api-server/src/routes/service-orders.ts @@ -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, ); } diff --git a/artifacts/api-server/tests/service-orders.test.mjs b/artifacts/api-server/tests/service-orders.test.mjs index 171080cf..f94a3630 100644 --- a/artifacts/api-server/tests/service-orders.test.mjs +++ b/artifacts/api-server/tests/service-orders.test.mjs @@ -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",