Files
TX/artifacts/api-server/tests/service-orders.test.mjs
T
riyadhafraa 4e657a2d4d Task #397: Per-card and bulk select+delete on Incoming Orders
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).
2026-05-05 13:42:43 +00:00

695 lines
26 KiB
JavaScript

import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import pg from "pg";
const API_BASE = process.env.TEST_API_BASE ?? "http://localhost:8080";
const DATABASE_URL = process.env.DATABASE_URL;
if (!DATABASE_URL) throw new Error("DATABASE_URL must be set to run these tests");
const TEST_PASSWORD = "TestPass123!";
const TEST_PASSWORD_HASH =
"$2b$10$Bs636ukPMyz01nKrsi.5m.JlDXSN22AVCvn8cgPWWDbo5yJRQX2vu";
const pool = new pg.Pool({ connectionString: DATABASE_URL });
const createdUserIds = [];
const createdOrderIds = [];
async function createUser(prefix, roleName) {
const username = `${prefix}_${Date.now().toString(36)}_${Math.random()
.toString(36)
.slice(2, 8)}`;
const { rows } = await pool.query(
`INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
VALUES ($1, $2, $3, 'SO Test', 'en', true) RETURNING id`,
[username, `${username}@example.com`, TEST_PASSWORD_HASH],
);
const id = rows[0].id;
createdUserIds.push(id);
if (roleName) {
await pool.query(
`INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = $2`,
[id, roleName],
);
}
return { id, username };
}
async function login(username) {
const res = await fetch(`${API_BASE}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password: TEST_PASSWORD }),
});
assert.equal(res.status, 200);
const sc = res.headers.get("set-cookie");
return sc.split(",").map((c) => c.split(";")[0].trim()).find((c) =>
c.startsWith("connect.sid="),
);
}
async function call(cookie, method, path, body) {
return fetch(`${API_BASE}/api${path}`, {
method,
headers: { "Content-Type": "application/json", Cookie: cookie },
body: body ? JSON.stringify(body) : undefined,
});
}
let serviceId;
before(async () => {
const { rows } = await pool.query(
`SELECT id FROM services WHERE is_available = true ORDER BY id LIMIT 1`,
);
if (rows.length === 0) throw new Error("No available service to test with");
serviceId = rows[0].id;
});
after(async () => {
if (createdOrderIds.length > 0) {
await pool.query(`DELETE FROM service_orders WHERE id = ANY($1::int[])`, [
createdOrderIds,
]);
}
if (createdUserIds.length > 0) {
await pool.query(`DELETE FROM service_orders WHERE user_id = ANY($1::int[]) OR assigned_to = ANY($1::int[])`, [createdUserIds]);
await pool.query(`DELETE FROM notifications WHERE user_id = ANY($1::int[])`, [createdUserIds]);
await pool.query(`DELETE FROM user_roles WHERE user_id = ANY($1::int[])`, [createdUserIds]);
await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [createdUserIds]);
}
await pool.end();
});
test("client can place + list own orders; service summary excludes descriptions", async () => {
const requester = await createUser("req", "user");
const cookie = await login(requester.username);
let res = await call(cookie, "POST", "/orders", {
serviceId,
notes: "extra hot",
});
assert.equal(res.status, 201);
const order = await res.json();
createdOrderIds.push(order.id);
assert.equal(order.status, "pending");
assert.equal(order.userId, requester.id);
assert.ok(order.service);
assert.ok(order.service.nameAr);
assert.ok(order.service.nameEn);
assert.equal(order.service.descriptionAr, undefined);
assert.equal(order.service.descriptionEn, undefined);
res = await call(cookie, "GET", "/orders/my");
assert.equal(res.status, 200);
const list = await res.json();
const found = list.find((o) => o.id === order.id);
assert.ok(found);
assert.equal(found.service.descriptionAr, undefined);
});
test("non-receiver gets 403 from /incoming and /confirm-receipt", async () => {
const owner = await createUser("own", "user");
const cookieOwner = await login(owner.username);
let res = await call(cookieOwner, "POST", "/orders", { serviceId });
const order = await res.json();
createdOrderIds.push(order.id);
// Non-receiver listing
res = await call(cookieOwner, "GET", "/orders/incoming");
assert.equal(res.status, 403);
// Non-receiver tries to claim
res = await call(cookieOwner, "PATCH", `/orders/${order.id}/confirm-receipt`);
assert.equal(res.status, 403);
});
test("user with no roles is 403 on /incoming (middleware has no auto-bypass)", async () => {
const noRole = await createUser("noro", null);
const cookie = await login(noRole.username);
const res = await call(cookie, "GET", "/orders/incoming");
assert.equal(res.status, 403);
});
test("two parallel confirm-receipt: one wins (200), other 409 already_claimed", async () => {
const owner = await createUser("own2", "user");
const r1 = await createUser("r1", "order_receiver");
const r2 = await createUser("r2", "order_receiver");
const co = await login(owner.username);
const c1 = await login(r1.username);
const c2 = await login(r2.username);
let res = await call(co, "POST", "/orders", { serviceId });
const order = await res.json();
createdOrderIds.push(order.id);
const [a, b] = await Promise.all([
call(c1, "PATCH", `/orders/${order.id}/confirm-receipt`),
call(c2, "PATCH", `/orders/${order.id}/confirm-receipt`),
]);
const codes = [a.status, b.status].sort();
assert.deepEqual(codes, [200, 409]);
const losing = a.status === 409 ? a : b;
const body = await losing.json();
assert.equal(body.error, "already_claimed");
});
test("status transitions matrix: receiver flows + owner/admin cancel rules", async () => {
const owner = await createUser("own3", "user");
const recv = await createUser("recv3", "order_receiver");
const stranger = await createUser("strg3", "user");
const adminUser = await createUser("admn3", "admin");
const co = await login(owner.username);
const cr = await login(recv.username);
const cs = await login(stranger.username);
const ca = await login(adminUser.username);
// Order 1: receiver flow pending → received → preparing → completed
let res = await call(co, "POST", "/orders", { serviceId });
let order = await res.json();
createdOrderIds.push(order.id);
// Owner CAN cancel while pending
res = await call(co, "POST", "/orders", { serviceId });
const cancelOrder = await res.json();
createdOrderIds.push(cancelOrder.id);
res = await call(co, "PATCH", `/orders/${cancelOrder.id}/status`, {
status: "cancelled",
});
assert.equal(res.status, 200);
assert.equal((await res.json()).status, "cancelled");
// Stranger cannot move status to preparing
res = await call(cs, "PATCH", `/orders/${order.id}/status`, {
status: "preparing",
});
assert.equal(res.status, 403);
// Receiver claims first
res = await call(cr, "PATCH", `/orders/${order.id}/confirm-receipt`);
assert.equal(res.status, 200);
// Receiver moves to preparing (allowed from received)
res = await call(cr, "PATCH", `/orders/${order.id}/status`, {
status: "preparing",
});
assert.equal(res.status, 200);
assert.equal((await res.json()).status, "preparing");
// Owner cannot cancel once preparing (only pending/received)
res = await call(co, "PATCH", `/orders/${order.id}/status`, {
status: "cancelled",
});
assert.equal(res.status, 403);
// Admin can cancel anytime
// (use a NEW preparing order)
res = await call(co, "POST", "/orders", { serviceId });
const o2 = await res.json();
createdOrderIds.push(o2.id);
res = await call(cr, "PATCH", `/orders/${o2.id}/confirm-receipt`);
assert.equal(res.status, 200);
res = await call(cr, "PATCH", `/orders/${o2.id}/status`, { status: "preparing" });
assert.equal(res.status, 200);
res = await call(ca, "PATCH", `/orders/${o2.id}/status`, { status: "cancelled" });
assert.equal(res.status, 200);
// Assigned receiver CAN cancel their own assigned order while preparing
res = await call(co, "POST", "/orders", { serviceId });
const o3 = await res.json();
createdOrderIds.push(o3.id);
res = await call(cr, "PATCH", `/orders/${o3.id}/confirm-receipt`);
assert.equal(res.status, 200);
res = await call(cr, "PATCH", `/orders/${o3.id}/status`, { status: "preparing" });
assert.equal(res.status, 200);
res = await call(cr, "PATCH", `/orders/${o3.id}/status`, { status: "cancelled" });
assert.equal(res.status, 200);
assert.equal((await res.json()).status, "cancelled");
// A different receiver (not the assignee) cannot cancel
res = await call(co, "POST", "/orders", { serviceId });
const o4 = await res.json();
createdOrderIds.push(o4.id);
res = await call(cr, "PATCH", `/orders/${o4.id}/confirm-receipt`);
assert.equal(res.status, 200);
const otherRecv = await createUser("recv4", "order_receiver");
const cor = await login(otherRecv.username);
res = await call(cor, "PATCH", `/orders/${o4.id}/status`, { status: "cancelled" });
assert.equal(res.status, 403);
// Receiver completes original order
res = await call(cr, "PATCH", `/orders/${order.id}/status`, {
status: "completed",
});
assert.equal(res.status, 200);
assert.equal((await res.json()).status, "completed");
// Cannot transition completed → anything (non-admin); now 409 order_unavailable per Task #80
res = await call(cr, "PATCH", `/orders/${order.id}/status`, {
status: "preparing",
});
assert.equal(res.status, 409);
// Admin CAN cancel a completed order
res = await call(ca, "PATCH", `/orders/${order.id}/status`, {
status: "cancelled",
});
assert.equal(res.status, 200);
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("DELETE /orders/:id — owner can delete completed/cancelled, admin can delete any, others 403", async () => {
const owner = await createUser("delOwn", "user");
const recv = await createUser("delRecv", "order_receiver");
const stranger = await createUser("delStr", "user");
const adminUser = await createUser("delAdm", "admin");
const co = await login(owner.username);
const cr = await login(recv.username);
const cs = await login(stranger.username);
const ca = await login(adminUser.username);
// Helper to create an order owned by `co`
const placeOrder = async () => {
const r = await call(co, "POST", "/orders", { serviceId });
assert.equal(r.status, 201);
const o = await r.json();
createdOrderIds.push(o.id);
return o;
};
// 1) Pending order: owner cannot delete (not terminal)
const o1 = await placeOrder();
let res = await call(co, "DELETE", `/orders/${o1.id}`);
assert.equal(res.status, 403);
// 2) Stranger cannot delete a terminal order they don't own
const o2 = await placeOrder();
res = await call(co, "PATCH", `/orders/${o2.id}/status`, { status: "cancelled" });
assert.equal(res.status, 200);
res = await call(cs, "DELETE", `/orders/${o2.id}`);
assert.equal(res.status, 403);
// 3) Owner CAN delete a cancelled order they own
res = await call(co, "DELETE", `/orders/${o2.id}`);
assert.equal(res.status, 204);
// Confirm it's gone from /orders/my
res = await call(co, "GET", "/orders/my");
const list1 = await res.json();
assert.ok(!list1.find((o) => o.id === o2.id));
// 3b) Deleting the same order again returns 404 (idempotent semantics)
res = await call(co, "DELETE", `/orders/${o2.id}`);
assert.equal(res.status, 404);
// 4) Owner CAN delete a completed order
const o3 = await placeOrder();
res = await call(cr, "PATCH", `/orders/${o3.id}/confirm-receipt`);
assert.equal(res.status, 200);
res = await call(cr, "PATCH", `/orders/${o3.id}/status`, { status: "preparing" });
assert.equal(res.status, 200);
res = await call(cr, "PATCH", `/orders/${o3.id}/status`, { status: "completed" });
assert.equal(res.status, 200);
res = await call(co, "DELETE", `/orders/${o3.id}`);
assert.equal(res.status, 204);
// 5) Admin CAN delete a non-terminal (pending) order
const o4 = await placeOrder();
res = await call(ca, "DELETE", `/orders/${o4.id}`);
assert.equal(res.status, 204);
// 6) 404 for unknown id
res = await call(co, "DELETE", `/orders/9999999`);
assert.equal(res.status, 404);
// 7) Unauthenticated → 401
res = await fetch(`${API_BASE}/api/orders/1`, { method: "DELETE" });
assert.equal(res.status, 401);
});
test("DELETE /orders/:id — receivers can delete orders visible in their incoming view (unclaimed pending or own active); non-receiver, non-owner still 403", async () => {
const owner = await createUser("recvDelOwn", "user");
const recv = await createUser("recvDelRecv", "order_receiver");
const stranger = await createUser("recvDelStr", "user");
const co = await login(owner.username);
const cr = await login(recv.username);
const cs = await login(stranger.username);
const place = async () => {
const r = await call(co, "POST", "/orders", { serviceId });
assert.equal(r.status, 201);
const o = await r.json();
createdOrderIds.push(o.id);
return o;
};
// 1) Receiver can delete a pending, unclaimed order.
const o1 = await place();
let res = await call(cr, "DELETE", `/orders/${o1.id}`);
assert.equal(res.status, 204, "receiver should be able to delete pending order");
// 2) Receiver can delete a received order (claimed by them).
const o2 = await place();
res = await call(cr, "PATCH", `/orders/${o2.id}/confirm-receipt`);
assert.equal(res.status, 200);
res = await call(cr, "DELETE", `/orders/${o2.id}`);
assert.equal(res.status, 204);
// 3) Receiver can delete an order they have claimed and moved to preparing
// (their own active order — visible in their incoming view).
const o3 = await place();
res = await call(cr, "PATCH", `/orders/${o3.id}/confirm-receipt`);
assert.equal(res.status, 200);
res = await call(cr, "PATCH", `/orders/${o3.id}/status`, { status: "preparing" });
assert.equal(res.status, 200);
res = await call(cr, "DELETE", `/orders/${o3.id}`);
assert.equal(res.status, 204);
// 3b) Receiver may NOT delete an order claimed by a *different* receiver —
// it isn't in their incoming view, so the API must reject it (403).
const otherRecv = await createUser("recvDelOther", "order_receiver");
const cor = await login(otherRecv.username);
const o3b = await place();
res = await call(cor, "PATCH", `/orders/${o3b.id}/confirm-receipt`);
assert.equal(res.status, 200);
res = await call(cor, "PATCH", `/orders/${o3b.id}/status`, { status: "preparing" });
assert.equal(res.status, 200);
res = await call(cr, "DELETE", `/orders/${o3b.id}`);
assert.equal(
res.status,
403,
"receiver should NOT delete another receiver's claimed order",
);
// 4) Non-receiver, non-owner still gets 403 on any state.
const o4 = await place();
res = await call(cs, "DELETE", `/orders/${o4.id}`);
assert.equal(res.status, 403);
// 5) Receiver may NOT delete a terminal (completed/cancelled) order — that
// is the owner's cleanup responsibility, not the receiver's queue.
const o5 = await place();
res = await call(cr, "PATCH", `/orders/${o5.id}/confirm-receipt`);
assert.equal(res.status, 200);
res = await call(cr, "PATCH", `/orders/${o5.id}/status`, { status: "preparing" });
assert.equal(res.status, 200);
res = await call(cr, "PATCH", `/orders/${o5.id}/status`, { status: "completed" });
assert.equal(res.status, 200);
res = await call(cr, "DELETE", `/orders/${o5.id}`);
assert.equal(res.status, 403, "receiver should NOT delete completed orders");
const o6 = await place();
res = await call(co, "PATCH", `/orders/${o6.id}/status`, { status: "cancelled" });
assert.equal(res.status, 200);
res = await call(cr, "DELETE", `/orders/${o6.id}`);
assert.equal(res.status, 403, "receiver should NOT delete cancelled orders");
});
test("POST /orders/bulk-delete — deletes authorized ids, reports failures, dedupes", async () => {
const owner = await createUser("bulkOwn", "user");
const recv = await createUser("bulkRecv", "order_receiver");
const stranger = await createUser("bulkStr", "user");
const co = await login(owner.username);
const cr = await login(recv.username);
const cs = await login(stranger.username);
const place = async () => {
const r = await call(co, "POST", "/orders", { serviceId });
assert.equal(r.status, 201);
const o = await r.json();
createdOrderIds.push(o.id);
return o;
};
// Receiver deletes 3 ids; one is unknown → reported as failed; ids deduped.
const a = await place();
const b = await place();
const c = await place();
const unknownId = 99999991;
let res = await call(cr, "POST", "/orders/bulk-delete", {
ids: [a.id, b.id, c.id, c.id, unknownId],
});
assert.equal(res.status, 200);
let body = await res.json();
assert.deepEqual(body.deletedIds.sort(), [a.id, b.id, c.id].sort());
assert.deepEqual(body.failedIds, [unknownId]);
// Stranger tries to bulk-delete owner's pending orders → all rejected.
const d = await place();
const e = await place();
res = await call(cs, "POST", "/orders/bulk-delete", { ids: [d.id, e.id] });
assert.equal(res.status, 200);
body = await res.json();
assert.deepEqual(body.deletedIds, []);
assert.deepEqual(body.failedIds.sort(), [d.id, e.id].sort());
// Owner can bulk-delete only the cancelled one of two; pending stays.
res = await call(co, "PATCH", `/orders/${d.id}/status`, { status: "cancelled" });
assert.equal(res.status, 200);
res = await call(co, "POST", "/orders/bulk-delete", { ids: [d.id, e.id] });
assert.equal(res.status, 200);
body = await res.json();
assert.deepEqual(body.deletedIds, [d.id]);
assert.deepEqual(body.failedIds, [e.id]);
// Validation: empty ids → 400
res = await call(cr, "POST", "/orders/bulk-delete", { ids: [] });
assert.equal(res.status, 400);
// Unauthenticated → 401
res = await fetch(`${API_BASE}/api/orders/bulk-delete`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ids: [1] }),
});
assert.equal(res.status, 401);
});
test("owner can restore (undo) a freshly cancelled order to its prior status; expired window blocks restore", async () => {
const owner = await createUser("undoOwn", "user");
const recv = await createUser("undoRecv", "order_receiver");
const stranger = await createUser("undoStr", "user");
const co = await login(owner.username);
const cr = await login(recv.username);
const cs = await login(stranger.username);
// Case A: cancel from pending → restore to pending
let res = await call(co, "POST", "/orders", { serviceId });
assert.equal(res.status, 201);
const oA = await res.json();
createdOrderIds.push(oA.id);
res = await call(co, "PATCH", `/orders/${oA.id}/status`, { status: "cancelled" });
assert.equal(res.status, 200);
// Stranger cannot restore
res = await call(cs, "PATCH", `/orders/${oA.id}/status`, { status: "pending" });
assert.equal(res.status, 403);
// Owner restores within window
res = await call(co, "PATCH", `/orders/${oA.id}/status`, { status: "pending" });
assert.equal(res.status, 200);
assert.equal((await res.json()).status, "pending");
// Case B: cancel from received → restore to received
res = await call(co, "POST", "/orders", { serviceId });
const oB = await res.json();
createdOrderIds.push(oB.id);
res = await call(cr, "PATCH", `/orders/${oB.id}/confirm-receipt`);
assert.equal(res.status, 200);
res = await call(co, "PATCH", `/orders/${oB.id}/status`, { status: "cancelled" });
assert.equal(res.status, 200);
// Restoring to "pending" should fail because assignedTo is set
res = await call(co, "PATCH", `/orders/${oB.id}/status`, { status: "pending" });
assert.equal(res.status, 400);
// Correct restore is "received"
res = await call(co, "PATCH", `/orders/${oB.id}/status`, { status: "received" });
assert.equal(res.status, 200);
assert.equal((await res.json()).status, "received");
// Case C: undo window expired → restore is rejected
res = await call(co, "POST", "/orders", { serviceId });
const oC = await res.json();
createdOrderIds.push(oC.id);
res = await call(co, "PATCH", `/orders/${oC.id}/status`, { status: "cancelled" });
assert.equal(res.status, 200);
// Backdate updatedAt past the 15s server window.
await pool.query(
`UPDATE service_orders SET updated_at = NOW() - INTERVAL '60 seconds' WHERE id = $1`,
[oC.id],
);
res = await call(co, "PATCH", `/orders/${oC.id}/status`, { status: "pending" });
assert.equal(res.status, 400);
const body = await res.json();
assert.match(body.error, /undo_window_expired/);
});
test("confirm-receipt returns 409 order_unavailable when the order is cancelled or completed", async () => {
const owner = await createUser("ownU", "user");
const recv1 = await createUser("recvU1", "order_receiver");
const recv2 = await createUser("recvU2", "order_receiver");
const co = await login(owner.username);
const cr1 = await login(recv1.username);
const cr2 = await login(recv2.username);
// Case A: order cancelled before any claim → second receiver sees order_unavailable
let res = await call(co, "POST", "/orders", { serviceId });
assert.equal(res.status, 201);
const oCancelled = await res.json();
createdOrderIds.push(oCancelled.id);
res = await call(co, "PATCH", `/orders/${oCancelled.id}/status`, {
status: "cancelled",
});
assert.equal(res.status, 200);
res = await call(cr1, "PATCH", `/orders/${oCancelled.id}/confirm-receipt`);
assert.equal(res.status, 409);
let body = await res.json();
assert.equal(
body.error,
"order_unavailable",
"cancelled order should report order_unavailable, not already_claimed",
);
// Case B: order taken all the way to completed → another receiver sees order_unavailable
res = await call(co, "POST", "/orders", { serviceId });
assert.equal(res.status, 201);
const oCompleted = await res.json();
createdOrderIds.push(oCompleted.id);
res = await call(cr1, "PATCH", `/orders/${oCompleted.id}/confirm-receipt`);
assert.equal(res.status, 200);
res = await call(cr1, "PATCH", `/orders/${oCompleted.id}/status`, {
status: "preparing",
});
assert.equal(res.status, 200);
res = await call(cr1, "PATCH", `/orders/${oCompleted.id}/status`, {
status: "completed",
});
assert.equal(res.status, 200);
res = await call(cr2, "PATCH", `/orders/${oCompleted.id}/confirm-receipt`);
assert.equal(res.status, 409);
body = await res.json();
assert.equal(
body.error,
"order_unavailable",
"completed order should report order_unavailable, not already_claimed",
);
});
test("PATCH /orders/:id/status returns 409 order_unavailable when targeting a terminal order", async () => {
const owner = await createUser("ownT", "user");
const recv = await createUser("recvT", "order_receiver");
const co = await login(owner.username);
const cr = await login(recv.username);
// Build a completed order (assignee = recv)
let res = await call(co, "POST", "/orders", { serviceId });
assert.equal(res.status, 201);
const oDone = await res.json();
createdOrderIds.push(oDone.id);
res = await call(cr, "PATCH", `/orders/${oDone.id}/confirm-receipt`);
assert.equal(res.status, 200);
res = await call(cr, "PATCH", `/orders/${oDone.id}/status`, {
status: "preparing",
});
assert.equal(res.status, 200);
res = await call(cr, "PATCH", `/orders/${oDone.id}/status`, {
status: "completed",
});
assert.equal(res.status, 200);
// Assignee tries preparing on a completed order → 409 order_unavailable
res = await call(cr, "PATCH", `/orders/${oDone.id}/status`, {
status: "preparing",
});
assert.equal(res.status, 409);
let body = await res.json();
assert.equal(body.error, "order_unavailable");
// Assignee tries completed (again) on a completed order → 409 order_unavailable
res = await call(cr, "PATCH", `/orders/${oDone.id}/status`, {
status: "completed",
});
assert.equal(res.status, 409);
body = await res.json();
assert.equal(body.error, "order_unavailable");
// Owner tries to cancel a completed order → 409 order_unavailable (non-admin)
res = await call(co, "PATCH", `/orders/${oDone.id}/status`, {
status: "cancelled",
});
assert.equal(res.status, 409);
body = await res.json();
assert.equal(body.error, "order_unavailable");
// Build a cancelled order (no assignee)
res = await call(co, "POST", "/orders", { serviceId });
assert.equal(res.status, 201);
const oCxl = await res.json();
createdOrderIds.push(oCxl.id);
res = await call(co, "PATCH", `/orders/${oCxl.id}/status`, {
status: "cancelled",
});
assert.equal(res.status, 200);
// Owner tries to cancel an already-cancelled order → 409 order_unavailable (non-admin)
res = await call(co, "PATCH", `/orders/${oCxl.id}/status`, {
status: "cancelled",
});
assert.equal(res.status, 409);
body = await res.json();
assert.equal(body.error, "order_unavailable");
});
test("unauthenticated cannot place order", async () => {
const res = await fetch(`${API_BASE}/api/orders`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ serviceId }),
});
assert.equal(res.status, 401);
});