Hide self-notifications across order lifecycle

Task: Audit notification rules in service-orders.ts so users never
receive notifications about actions they themselves initiated. The
specific scenario: a user with the `orders.receive` permission who
cancels their own assigned order would notify themselves under the
old logic.

Approach:
- Added an optional `actorId` parameter to `notifyUser`. When the
  notification recipient equals the actor, the function returns
  early (no DB insert, no socket emit).
- Threaded `actorId` through the three notification call sites:
  1. POST /orders — placing an order no longer notifies the placer
     even if they also hold the receiver role.
  2. PATCH /orders/:id/confirm-receipt — a receiver claiming their
     own order no longer notifies themselves.
  3. PATCH /orders/:id/status — replaced the narrow
     `!initiatedByOwner` guard with the generic `actorId === userId`
     check inside `notifyUser`. This now covers owner-cancel as
     before AND owner-as-assignee transitions (preparing/completed/
     cancelled) where the owner happens to also be the assignee.

Notes / deviations:
- Kept the existing distinguishing wording for receiver-initiated
  cancels ("claimed but later cancelled by the receiver"); it's
  simply suppressed when the receiver is also the owner.
- No schema or API contract changes; purely server-side notification
  filtering. Typecheck passes.
- No follow-ups proposed: existing project tasks already cover
  automated tests for order/cancel flow, re-order from history, and
  an undo window for cancellations.

Replit-Task-Id: 65b6d89a-6882-42f8-adb7-6ac1a2560748
This commit is contained in:
riyadhafraa
2026-04-22 06:49:19 +00:00
parent d2c8e05c6c
commit bae9c26927
2 changed files with 10 additions and 3 deletions
@@ -98,7 +98,10 @@ async function notifyUser(
bodyAr: string,
bodyEn: string,
orderId: number,
actorId?: number,
) {
// Never notify users about actions they themselves initiated.
if (actorId !== undefined && actorId === userId) return;
const [notification] = await db
.insert(notificationsTable)
.values({
@@ -163,6 +166,7 @@ router.post("/orders", requireAuth, async (req, res): Promise<void> => {
service.nameAr,
service.nameEn,
order.id,
userId,
);
}
await broadcastIncomingChanged(receivers);
@@ -252,6 +256,7 @@ router.patch(
enriched.service.nameAr,
enriched.service.nameEn,
orderId,
userId,
);
await emitToUser(enriched.userId, "order_updated", enriched);
}
@@ -339,9 +344,10 @@ router.patch(
const enriched = await loadOrder(orderId);
// Notify owner on every status change initiated by a receiver/admin (not by owner cancel)
const initiatedByOwner = existing.userId === userId;
if (!initiatedByOwner && enriched) {
// Notify owner on every status change — notifyUser will skip if the
// owner themselves initiated the action (e.g. owner-cancel, or an
// assignee transition where the owner also holds the receiver role).
if (enriched) {
const titleMap: Record<string, [string, string]> = {
preparing: ["طلبك قيد التحضير", "Your order is being prepared"],
completed: ["تم إكمال طلبك", "Your order is completed"],
@@ -362,6 +368,7 @@ router.patch(
bodyAr,
bodyEn,
orderId,
userId,
);
}
if (enriched) await emitToUser(enriched.userId, "order_updated", enriched);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB