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
+232
View File
@@ -16,6 +16,8 @@ tags:
description: App management
- name: services
description: Internal services (khidmati)
- name: orders
description: Service orders workflow
- name: conversations
description: Chat conversations
- name: messages
@@ -588,6 +590,123 @@ paths:
"204":
description: Deleted
/orders:
post:
operationId: createServiceOrder
tags: [orders]
summary: Place a service order
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateServiceOrderBody"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/ServiceOrder"
"400":
description: Validation error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/orders/my:
get:
operationId: listMyServiceOrders
tags: [orders]
summary: List orders placed by the current user
responses:
"200":
description: My orders
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ServiceOrder"
/orders/incoming:
get:
operationId: listIncomingServiceOrders
tags: [orders]
summary: List active orders for receivers (requires orders:receive)
responses:
"200":
description: Incoming orders
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ServiceOrder"
"403":
description: Forbidden
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/orders/{id}/status:
patch:
operationId: updateServiceOrderStatus
tags: [orders]
summary: Receiver updates order status (claim/deliver/cancel)
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateServiceOrderStatusBody"
responses:
"200":
description: Updated order
content:
application/json:
schema:
$ref: "#/components/schemas/ServiceOrder"
"409":
description: Conflict (already claimed or invalid transition)
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/orders/{id}/confirm-receipt:
patch:
operationId: confirmServiceOrderReceipt
tags: [orders]
summary: Requester confirms they received their order
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Order received
content:
application/json:
schema:
$ref: "#/components/schemas/ServiceOrder"
"409":
description: Invalid transition
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/service-categories:
get:
operationId: listServiceCategories
@@ -1628,6 +1747,119 @@ components:
- sortOrder
- createdAt
ServiceOrderUser:
type: object
properties:
id:
type: integer
username:
type: string
displayNameAr:
type: ["string", "null"]
displayNameEn:
type: ["string", "null"]
avatarUrl:
type: ["string", "null"]
required:
- id
- username
ServiceOrderService:
type: object
properties:
id:
type: integer
nameAr:
type: string
nameEn:
type: string
imageUrl:
type: ["string", "null"]
required:
- id
- nameAr
- nameEn
ServiceOrderStatus:
type: string
enum: [pending, claimed, delivered, received, cancelled]
ServiceOrder:
type: object
properties:
id:
type: integer
serviceId:
type: integer
requestedBy:
type: integer
assignedTo:
type: ["integer", "null"]
quantity:
type: integer
notes:
type: ["string", "null"]
status:
$ref: "#/components/schemas/ServiceOrderStatus"
createdAt:
type: string
format: date-time
claimedAt:
type: ["string", "null"]
format: date-time
deliveredAt:
type: ["string", "null"]
format: date-time
receivedAt:
type: ["string", "null"]
format: date-time
cancelledAt:
type: ["string", "null"]
format: date-time
service:
$ref: "#/components/schemas/ServiceOrderService"
requester:
oneOf:
- $ref: "#/components/schemas/ServiceOrderUser"
- type: "null"
assignee:
oneOf:
- $ref: "#/components/schemas/ServiceOrderUser"
- type: "null"
required:
- id
- serviceId
- requestedBy
- quantity
- status
- createdAt
- service
CreateServiceOrderBody:
type: object
properties:
serviceId:
type: integer
quantity:
type: integer
minimum: 1
maximum: 50
default: 1
notes:
type: ["string", "null"]
maxLength: 500
required:
- serviceId
UpdateServiceOrderStatusBody:
type: object
properties:
status:
type: string
enum: [claimed, delivered, cancelled]
required:
- status
ConversationWithDetails:
type: object
properties: