You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Purpose: Complete REST contract for every module. Base path: /api/v1. All endpoints except POST /auth/signup, POST /auth/login, and POST /auth/forgot-password require a valid Authorization: Bearer <token> header. Every endpoint additionally enforces the role matrix from the master plan (§2) via a route guard — the specific required role(s) is noted per endpoint as Roles:.
Server-side enforcement: the DTO/validation schema for this endpoint literally has no role field defined — even if a client sends one, deserialization drops it before it reaches the service layer. This is the technical implementation of "no self-elevation."
Admin, Asset Manager (read-only), Department Head (dept-scoped)
List/search/filter by department, role, status.
GET
/org/employees/:id
Self, Admin, relevant Dept Head
Detail.
PATCH
/org/employees/:id
Admin
Edit name/department/status.
POST
/org/employees/:id/promote
Admin only
{ newRole: "DEPARTMENT_HEAD" | "ASSET_MANAGER" }. This is the only endpoint in the entire system capable of changing an employee's role. Writes an activity_log entry with action='ROLE_PROMOTED' including old and new role in metadata.
3. Asset Module (/assets)
Method
Path
Roles
Description
GET
/assets
All authenticated
Search/filter: ?q=&category=&status=&department=&location=&bookable= — q hits the GIN full-text index on tag/name/serial.
GET
/assets/:id
All authenticated
Full detail.
GET
/assets/:id/history
All authenticated
Merged, time-sorted allocation + maintenance history.
GET
/assets/tag/:assetTag
All authenticated
Lookup by scanned QR/tag (fast path for audit mobile flow).
POST
/assets
Admin, Asset Manager
Create. Server generates assetTag and qrCodeUrl; body carries name/category/serial/etc. Status defaults AVAILABLE.
PATCH
/assets/:id
Admin, Asset Manager
Edit descriptive fields only — status is intentionally not editable through this endpoint (see §5).
POST
/assets/:id/photos
Admin, Asset Manager
Upload photo(s), returns updated photoUrls.
Why status is excluded from the generic PATCH: if a generic edit endpoint could set status, someone could set Under Maintenance → Available bypassing the maintenance-resolution workflow, or Available → Disposed skipping the lifecycle rules. Status changes only happen as side effects of the specific workflow endpoints below (allocate, return, maintenance approve/resolve, audit close) or via a dedicated guarded endpoint:
| POST | /assets/:id/transition | Admin, Asset Manager | { toStatus, reason } — manual override path, validated against the state-transition table (Spec 03 §1), logged with action='MANUAL_STATUS_OVERRIDE'. |
4. Allocation & Transfer Module (/allocations, /transfers)
Method
Path
Roles
Description
GET
/allocations
Scoped (self / dept / all)
Filter by asset, employee, department, status, overdue=true.
POST
/allocations
Admin, Asset Manager, Department Head (own dept)
{ assetId, employeeId?, departmentId?, expectedReturnDate? }. Returns 409 Conflict with { code: "ALLOCATION_CONFLICT", currentHolder: {...} } if the asset already has an active allocation — this is the exact trigger for the UI's "currently held by X, request transfer?" prompt.
POST
/allocations/:id/return
Admin, Asset Manager, holder themself
{ conditionNote }. Closes allocation, reverts asset to AVAILABLE.
POST
/transfers
Any authenticated (on an asset allocated to them or visible to them)
{ allocationId, toEmployeeId? , toDepartmentId? }. Status starts REQUESTED.
GET
/transfers
Scoped
List pending transfers relevant to the caller's approval scope.
POST
/transfers/:id/approve
Asset Manager, Department Head (own dept)
Executes the transaction in Spec 01 §5.2.
POST
/transfers/:id/reject
Asset Manager, Department Head (own dept)
{ reason }.
Allocation conflict response example:
409Conflict
{
"success": false,
"error": {
"code": "ALLOCATION_CONFLICT",
"message": "Asset AF-0114 is currently held by Priya Shah.",
"details": { "currentHolder": { "employeeId": "...", "name": "Priya Shah" }, "allocationId": "..." }
}
}
5. Booking Module (/bookings)
Method
Path
Roles
Description
GET
/bookings
All authenticated
Filter by resource, requester, status, date range — powers calendar view.
GET
/bookings/resource/:assetId/calendar
All authenticated
Returns bookings for one bookable resource in a date window.
POST
/bookings
All authenticated
{ resourceId, startTime, endTime, bookedForDepartmentId? }. Returns 409 Conflict with { code: "BOOKING_OVERLAP" } if it collides with the DB exclusion constraint (Spec 01 §5.3).
POST
/bookings/:id/cancel
Booking owner, Admin, Asset Manager
{ reason? }.
PATCH
/bookings/:id/reschedule
Booking owner
{ startTime, endTime } — implemented as cancel + re-create inside one transaction, so it goes through the same overlap check.
Booking overlap response example:
409Conflict
{
"success": false,
"error": {
"code": "BOOKING_OVERLAP",
"message": "Room B2 is already booked from 09:00 to 10:00, which overlaps your requested slot."
}
}
6. Maintenance Module (/maintenance)
Method
Path
Roles
Description
GET
/maintenance
Scoped
Filter by asset, status, priority.
POST
/maintenance
All authenticated (typically current holder)
{ assetId, issueDescription, priority, photoUrl? }. Status starts PENDING.
POST
/maintenance/:id/approve
Asset Manager
Side effect: asset.status = UNDER_MAINTENANCE (same transaction).
POST
/maintenance/:id/reject
Asset Manager
{ reason }. No asset status change.
POST
/maintenance/:id/assign-technician
Asset Manager
{ technicianName }. Status → TECHNICIAN_ASSIGNED.
POST
/maintenance/:id/start
Asset Manager
Status → IN_PROGRESS.
POST
/maintenance/:id/resolve
Asset Manager
Side effect: asset.status = AVAILABLE, resolved_at = now() (same transaction).
7. Audit Module (/audits)
Method
Path
Roles
Description
POST
/audits
Admin, Asset Manager
{ scopeDepartmentId?, scopeLocation?, startDate, endDate, auditorIds: [...] } — creates cycle + assignments + one PENDINGaudit_result row per in-scope asset (server computes scope at creation time and snapshots the asset list).
{ assetsAvailable, assetsAllocated, maintenanceToday, activeBookings, pendingTransfers, upcomingReturns, overdueReturns }. Each computed as its own scoped query, not derived client-side.
GET
/reports/utilization
Admin, Asset Manager
Most-used vs idle assets over a date range.
GET
/reports/maintenance-frequency
Admin, Asset Manager
Grouped by asset/category.
GET
/reports/upcoming-lifecycle
Admin, Asset Manager
Assets due for maintenance / nearing retirement (age/condition threshold).
Admin (all), Asset Manager/Dept Head (scoped), Employee (own actions only)
Filter by entity type/id, actor, date range.
10. Cross-cutting: Rate Limiting
Endpoint group
Limit
/auth/login
5 attempts / 15 min per IP+email combo, exponential backoff after
/auth/forgot-password
3 requests / hour per email
All other endpoints
Standard per-user throttle (e.g. 100 req/min) to prevent scripReact abuse
11. Standard Error Codes
Code
HTTP Status
Meaning
VALIDATION_ERROR
400
Request body failed schema validation
UNAUTHORIZED
401
Missing/invalid/expired token
FORBIDDEN
403
Valid token, insufficient role/scope
NOT_FOUND
404
Entity doesn't exist or isn't visible to caller
ALLOCATION_CONFLICT
409
Asset already actively allocated
BOOKING_OVERLAP
409
Time slot collides with existing booking
INVALID_STATE_TRANSITION
409
Requested status change isn't allowed from current state
AUDIT_CYCLE_CLOSED
409
Attempted edit on a closed cycle
RATE_LIMITED
429
Too many requests
INTERNAL_ERROR
500
Unhandled server error
This error-code vocabulary should be treated as the canonical list; frontend error handling and any AI agent building against this API should switch on error.code, not on parsing error.message strings.