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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user