Backend (artifacts/api-server):
- Added hasReceivePermission() and refactored DELETE /orders/:id into a
shared authorizeAndDeleteOrder() helper so single and bulk paths use
the same authorization rules.
- Added POST /orders/bulk-delete returning { deletedIds, failedIds } with
per-id authorization, dedup, and best-effort partial success.
- Tightened receiver authorization to mirror GET /orders/incoming: a
receiver may only delete an unclaimed pending order or one they have
themselves claimed and is still active (received/preparing). Another
receiver's claimed order, and any terminal order, return 403 even at
the API layer. Code, comments and OpenAPI description now agree.
API spec (lib/api-spec/openapi.yaml):
- New /orders/bulk-delete operation with BulkDeleteServiceOrdersBody and
BulkDeleteServiceOrdersResponse schemas. Updated DELETE /orders/{id}
description. Regenerated react-query hooks via codegen — both
useDeleteServiceOrder and useBulkDeleteServiceOrders are used by the UI.
UI (artifacts/tx-os/src/pages/orders-incoming.tsx):
- Per-card checkbox visible at all times (RTL-safe leading edge).
- Per-card trash icon for single delete.
- Section-scoped Select-all (مين / unclaimed) with tri-state
all/some(indeterminate)/none and shadcn Checkbox.
- Sticky bottom bulk action bar shows selected count + Clear selection +
destructive Delete.
- Single AlertDialog used by both per-card and bulk paths, with
pluralized confirmation copy. Single delete calls DELETE /orders/:id;
multi delete calls POST /orders/bulk-delete; partial-failure surfaces
via toast.
i18n: New incomingOrders keys in ar.json and en.json (select, selectAll,
clearSelection, selectedCount, delete, deleteConfirmTitle/Body, deleted,
deleteFailed, deletePartial) with pluralization in both languages.
Tests:
- artifacts/api-server/tests/service-orders.test.mjs: receivers can
delete pending/received/preparing; receivers cannot delete terminal
(completed/cancelled); bulk-delete with mixed authorized / unknown /
dedup / unauthenticated cases.
- artifacts/tx-os/tests/order-incoming-delete.spec.mjs (new Playwright):
bulk-delete two unclaimed orders shrinks the list by 2; per-card
trash on a claimed order deletes one. Both pass.
Pre-existing executive-meetings PDF/font test failures are unrelated.
Follow-ups proposed: #398 (Undo for incoming-order deletes), #399
(safer notification handling in bulk-delete).
Backend (artifacts/api-server):
- Added hasReceivePermission() helper and refactored DELETE /orders/:id
into shared authorizeAndDeleteOrder() so single and bulk paths use the
same authorization rules.
- Added POST /orders/bulk-delete returning { deletedIds, failedIds } with
per-id authorization, dedup, and best-effort partial success.
- Tightened receiver authorization (per code review): receivers may only
delete pending/received/preparing orders; terminal (completed/cancelled)
orders remain the owner's cleanup responsibility. OpenAPI description
and the implementation now agree.
API spec (lib/api-spec/openapi.yaml):
- New /orders/bulk-delete operation with BulkDeleteServiceOrdersBody and
BulkDeleteServiceOrdersResponse schemas; updated DELETE /orders/{id}
description. Regenerated react-query hooks via codegen — useBulkDelete
ServiceOrders is now available.
UI (artifacts/tx-os/src/pages/orders-incoming.tsx):
- Selection mode toggle in the page header (RTL-safe).
- Per-card Checkbox plus a Select-all control.
- Sticky bottom action bar with destructive Delete and selected count.
- AlertDialog confirmation using pluralized i18n keys; toast surfaces
partial-failure results when some ids couldn't be deleted.
i18n: New incomingOrders keys in ar.json and en.json (select, selectAll,
clearSelection, selectedCount, delete, deleteConfirmTitle/Body, deleted,
deleteFailed, deletePartial) with pluralization.
Tests: Added two new tests in service-orders.test.mjs covering receiver
delete on incoming queue, receiver forbidden on terminal orders, and
bulk-delete with mixed authorized / unknown / dedup / unauthenticated
cases. Both pass. Pre-existing executive-meetings PDF/font test failures
are unrelated.
Task #80 introduced a new 409 `order_unavailable` error code returned by:
- PATCH /api/orders/:id/confirm-receipt
- PATCH /api/orders/:id/status
…to distinguish "already_claimed" (lost a race) from "no longer claimable"
(target order is cancelled or completed). The integration test suite did
not cover these paths.
Changes
-------
artifacts/api-server/tests/service-orders.test.mjs
+ Added test "confirm-receipt returns 409 order_unavailable when the order
is cancelled or completed":
- Owner cancels a pending order → another receiver's confirm-receipt
returns 409 with body.error === "order_unavailable" (not
"already_claimed").
- Receiver claims, prepares, completes an order → another receiver's
confirm-receipt returns 409 "order_unavailable".
+ Added test "PATCH /orders/:id/status returns 409 order_unavailable when
targeting a terminal order":
- Completed order: assignee → preparing → 409 "order_unavailable"
- Completed order: assignee → completed → 409 "order_unavailable"
- Completed order: owner → cancelled → 409 "order_unavailable"
- Cancelled order: owner → cancelled → 409 "order_unavailable"
Verification
------------
All 11 service-orders integration tests pass (including both new tests):
pnpm --filter @workspace/api-server test → 11 passed, 0 failed.
No production code changes; tests only.
Replit-Task-Id: 229ed991-1c32-4281-8e57-00abeacd80b4
Original task (#72): Mirror the recently-added delete Undo toast for
the cancel action so accidental cancellations can be reverted within
~7s.
Changes
- Frontend (artifacts/teaboy-os/src/pages/my-orders.tsx):
OrderCard.handleCancel captures the order's previous status, and on
successful cancel shows a toast with an "Undo" action (duration
matches the existing UNDO_WINDOW_MS = 7000ms). Clicking Undo issues
a PATCH to restore the order to its previous status (pending or
received) and emits a "Restored" toast. Errors surface a
"Could not restore the order" toast.
- Cancel confirmation copy was softened (no longer says "can't be
reopened") and a new restoreFailed string was added in both en.json
and ar.json.
- Backend (artifacts/api-server/src/routes/service-orders.ts):
PATCH /orders/:id/status accepts pending and received as targets
for the owner (or admin) when the existing status is cancelled,
serving as the restore-from-cancelled transition. We require
assignedTo to be null for pending and non-null for received so we
always restore to the actually-prior state. The existing logic
already broadcasts order_incoming_changed to receivers and emits
order_updated to the owner, so receivers' incoming queues refresh
automatically when an order is restored. notifyUser titleMap was
extended with "Your order was restored" entries.
- Time-bound restore: server enforces a RESTORE_WINDOW_MS of 15s
(slightly larger than the 7s client window to absorb network/clock
skew). After it expires, restore is rejected with
`undo_window_expired`, so cancellation truly becomes permanent —
this applies to admin too, so restore is strictly Undo and not an
arbitrary reopen.
- API spec (lib/api-spec/openapi.yaml):
UpdateServiceOrderStatusBody enum extended to include pending and
received; endpoint summary updated. Regenerated api-zod and
api-client-react.
Tests
- Added a new test case in artifacts/api-server/tests/service-orders.test.mjs
that covers: restore from pending, restore from received,
pending<->received validation against assignedTo, stranger forbidden,
and undo window expiry (by backdating updated_at).
Verification
- pnpm typecheck passes across libs and artifacts.
- New api-server test "owner can restore (undo) a freshly cancelled
order..." passes; all pre-existing service-order tests still pass.
- e2e tested: login -> place order -> cancel -> Undo restores to
Pending; cancel again -> let window expire -> order stays Cancelled
with no Undo available.
Notes / unrelated
- The unrelated `admin-app-opens-pagination` test
(`by-app: paginating with limit returns nextOffset until exhausted`)
fails in the dev environment because the dev DB has > 100 app_opens
rows for the chosen app and the test only walks up to 50 pages of
size 2. This is a pre-existing flaky test unrelated to this task
and was not modified.
Replit-Task-Id: beca78bc-32f3-4cdc-8440-9a661b48363b
- Add DELETE /api/orders/:id endpoint: owner can delete completed/cancelled
orders; admin can delete any. 401/403/404/204 responses. Emits
order_deleted to owner and assignee for live invalidation.
- Update OpenAPI spec with deleteServiceOrder operation; regenerate
api-client-react + api-zod.
- Frontend: per-order trash button on completed/cancelled cards with
confirm dialog; bulk "Clear finished (N)" button at top of list with
confirm dialog and toast summary (handles partial failures).
- Add ar/en locale keys under myOrders for delete + clear flows
(singular/plural variants).
- Backend tests cover all paths: pending owner=403, stranger=403,
owner-cancelled=204, owner-completed=204, admin-pending=204, 404, 401.
All 25 service-order tests pass; the 1 failing test (admin-app-opens-
pagination) is unrelated/pre-existing.
Follow-ups proposed: #70 undo for deleted orders, #71 audit
self-notification on receiver-cancel-own-order.
- Add DELETE /api/orders/:id endpoint: owner can delete completed/cancelled
orders; admin can delete any. 401/403/404/204 responses. Emits
order_deleted to owner and assignee for live invalidation.
- Update OpenAPI spec with deleteServiceOrder operation; regenerate
api-client-react + api-zod.
- Frontend: per-order trash button on completed/cancelled cards with
confirm dialog; bulk "Clear finished (N)" button at top of list with
confirm dialog and toast summary (handles partial failures).
- Add ar/en locale keys under myOrders for delete + clear flows
(singular/plural variants).
- Backend tests cover all paths: pending owner=403, stranger=403,
owner-cancelled=204, owner-completed=204, admin-pending=204, 404, 401.
All 25 service-order tests pass; the 1 failing test (admin-app-opens-
pagination) is unrelated/pre-existing.
Follow-ups proposed: #70 undo for deleted orders, #71 audit
self-notification on receiver-cancel-own-order.
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
Backend (artifacts/api-server)
- Add admin-only role-toggle endpoints:
- POST /users/:id/roles { roleName } — idempotent, returns UserProfile
- DELETE /users/:id/roles/:roleName — idempotent, returns UserProfile
- Allow assigned receiver to cancel their own claimed order
(received/preparing) in PATCH /orders/:id/status. Owner & admin
rules unchanged.
- New backend test cases: receiver can cancel own assigned order;
another receiver gets 403.
API spec / codegen
- Add AddUserRoleBody schema and the two new role endpoints
under a new "roles" tag in lib/api-spec/openapi.yaml.
- Regenerated api-zod and api-client-react.
Frontend (artifacts/teaboy-os)
- New page src/pages/orders-incoming.tsx at /orders/incoming:
RBAC-gated (admin || order_receiver), shows "My active orders"
and "Awaiting receiver" sections, with claim, mark preparing,
mark completed and cancel buttons. Handles 409 already_claimed.
- Add Inbox button to home top bar, conditional on the same roles.
- Admin users table now has an "Order Receiver" Switch wired to
the new role-toggle hooks.
- Extend use-notifications-socket to invalidate the incoming-orders
query on order_incoming_changed and on notification_created with
type === "order".
- Bilingual locale keys (ar/en) for the new page and admin label.
Tests
- All 25 api-server tests pass (24 existing + 1 new receiver-cancel
case). All 3 teaboy-os e2e tests pass.
Follow-up filed: #67 (notify requester when receiver cancels).
- New `service_orders` table (status pending|claimed|delivered|received|cancelled)
- New `orders:receive` permission + `order_receiver` role; admins implicitly allowed
- Added `requirePermission(name)` and `userHasPermission(userId, name)` middleware helpers
- New routes:
- POST /api/orders place order (authenticated)
- GET /api/orders/my list current user's orders
- GET /api/orders/incoming receivers see pending+active visible orders
- PATCH /api/orders/:id/status claim (atomic), deliver, cancel
- PATCH /api/orders/:id/confirm-receipt requester confirms a delivered order
- Atomic claim via UPDATE ... WHERE status='pending' AND assigned_to IS NULL
(returns 409 already_claimed on race)
- Realtime: emits `notification_created` to receivers/requester,
`order_incoming_changed` to all receivers, `order_updated` to requester
- Service shape in order responses limited to id/nameAr/nameEn/imageUrl
(no description fields), per spec
- OpenAPI updated with new paths and schemas; codegen run
- Seed updated idempotently (permission, role, role_permissions)
- New tests in artifacts/api-server/tests/service-orders.test.mjs
(full lifecycle, atomic claim race, unauth rejection) — all 21 api tests pass
No deviations from the planned scope. Tasks #63 (client UI) and #64
(receiver page + admin role toggle) remain blocked-by #62 and are next.