Service Orders — backend foundation (Task #62)

- 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.
This commit is contained in:
riyadhafraa
2026-04-21 18:24:20 +00:00
parent de8333e659
commit cdf5bf4d33
11 changed files with 1713 additions and 11 deletions
+36 -10
View File
@@ -37,31 +37,57 @@ async function main() {
{ name: "services:manage", descriptionAr: "إدارة الخدمات", descriptionEn: "Manage Services" },
{ name: "users:manage", descriptionAr: "إدارة المستخدمين", descriptionEn: "Manage Users" },
{ name: "chat:access", descriptionAr: "الوصول للمحادثات", descriptionEn: "Access Chat" },
{ name: "orders:receive", descriptionAr: "استقبال الطلبات", descriptionEn: "Receive Service Orders" },
];
const insertedPerms = await db
.insert(permissionsTable)
.values(permissions)
.onConflictDoNothing()
.returning();
await db.insert(permissionsTable).values(permissions).onConflictDoNothing();
const allInsertedPerms = await db.select().from(permissionsTable);
console.log("Permissions created");
// Create order_receiver role
const [orderReceiverRole] = await db
.insert(rolesTable)
.values({
name: "order_receiver",
descriptionAr: "مستقبل الطلبات",
descriptionEn: "Service Order Receiver",
})
.onConflictDoNothing()
.returning();
const allRoles = await db.select().from(rolesTable);
const adminRoleResolved = adminRole ?? allRoles.find((r) => r.name === "admin");
const userRoleResolved = userRole ?? allRoles.find((r) => r.name === "user");
const orderReceiverResolved =
orderReceiverRole ?? allRoles.find((r) => r.name === "order_receiver");
// Assign all permissions to admin role
if (adminRole && insertedPerms.length > 0) {
if (adminRoleResolved && allInsertedPerms.length > 0) {
await db
.insert(rolePermissionsTable)
.values(insertedPerms.map((p) => ({ roleId: adminRole.id, permissionId: p.id })))
.values(allInsertedPerms.map((p) => ({ roleId: adminRoleResolved.id, permissionId: p.id })))
.onConflictDoNothing();
}
// Assign chat permission to user role
if (userRole && insertedPerms.length > 0) {
const chatPerm = insertedPerms.find((p) => p.name === "chat:access");
if (userRoleResolved && allInsertedPerms.length > 0) {
const chatPerm = allInsertedPerms.find((p) => p.name === "chat:access");
if (chatPerm) {
await db
.insert(rolePermissionsTable)
.values({ roleId: userRole.id, permissionId: chatPerm.id })
.values({ roleId: userRoleResolved.id, permissionId: chatPerm.id })
.onConflictDoNothing();
}
}
// Assign orders:receive permission to order_receiver role
if (orderReceiverResolved && allInsertedPerms.length > 0) {
const ordersPerm = allInsertedPerms.find((p) => p.name === "orders:receive");
if (ordersPerm) {
await db
.insert(rolePermissionsTable)
.values({ roleId: orderReceiverResolved.id, permissionId: ordersPerm.id })
.onConflictDoNothing();
}
}