Skip to content

feat: work order + spare parts HTTP API#79

Merged
tekwani merged 24 commits into
tetherto:developfrom
mukama:invhm-inventory
May 22, 2026
Merged

feat: work order + spare parts HTTP API#79
tekwani merged 24 commits into
tetherto:developfrom
mukama:invhm-inventory

Conversation

@mukama
Copy link
Copy Markdown
Contributor

@mukama mukama commented May 14, 2026

Exposes the HTTP surface for Work Orders (CRUD, filtered list, audit, assign/close/cancel, work-log entries, multipart file upload/download, CSV export) and the coordinated spare-parts register/update/list/repair-history routes that pair every part move with a Work Order. Reuses the existing list-things / pushAction / actions-done plumbing throughout — clients poll /auth/actions/done/:id for async write results, paced by the expectedActionLatencyMs hint surfaced in every push response.

@mukama mukama force-pushed the invhm-inventory branch 7 times, most recently from bbe6d66 to 90f403a Compare May 14, 2026 13:05
Adds the Work Order HTTP surface as thin wrappers over the existing
things RPC plumbing:
  POST   /auth/work-orders                create  (registerThing)
  GET    /auth/work-orders                list    (listThings + type filter)
  GET    /auth/work-orders/:id            get     (listThings + id+type filter)
  PATCH  /auth/work-orders/:id            update  (updateThing)
  POST   /auth/work-orders/:id/close      close   (updateThing status=closed)
  POST   /auth/work-orders/:id/cancel     cancel  (updateThing status=cancelled)
  POST   /auth/work-orders/:id/assign     assign  (updateThing assignedTo)
  GET    /auth/work-orders/:id/audit      history (getHistoricalLogs)

Introduces a new RBAC resource work_order (r/rw) granted to
operator/admin/repair_technician roles for write and to viewer
roles for read. Strips the duplicated [HRPC_ERR]= prefix and
bubbles structured rack errors verbatim. Skips capCheck when the
gateway runs with --noauth so local curl-testing works through
permissioned routes.
@mukama mukama force-pushed the invhm-inventory branch from 90f403a to d58e755 Compare May 14, 2026 13:17
mukama added 16 commits May 14, 2026 17:06
Adds filter shortcuts (q, assignee, creator, partId, status, type,
from, to) to GET /auth/work-orders alongside the existing mingo
?query passthrough; q is a case-insensitive regex $or against code
and info.issue, the rest map to info.* paths and run on the rack
the same way every other list does. Pages now return the
{data, totalCount, offset, limit, hasMore} envelope by calling
listThings and getThingsCount in parallel, and every shortcut is
wired into the cache key so cached pages do not bleed across
filter combinations.
Adds PUT /auth/spare-parts/:id which validates the supplied
workOrderId resolves to an open or in_progress WO, pushes the
underlying updateThing on the part rack with workOrderId injected
into info, and appends a denormalised entry to the WO's
partsMoves[] so the audit trail stays on the Work Order itself.
GET /auth/spare-parts/:id/repair-history mingo-queries every WO
that ever touched the part and returns each move row hydrated with
workOrderCode in the standard {data, totalCount, offset, limit,
hasMore} envelope.
Adds POST /auth/work-orders/:id/log which forwards to the existing
saveThingComment RPC so the WO comments[] doubles as the work log.
Adds POST/GET/DELETE /auth/work-orders/:id/files routes backed by
@fastify/multipart on the gateway and the new
storeWorkOrderFile/loadWorkOrderFile/removeWorkOrderFile pass-
throughs on the ork; uploads validate mime + size, append metadata
to WO.info.files via pushAction, and reject mutations on closed or
cancelled WOs.
# Conflicts:
#	workers/lib/constants.js
#	workers/lib/server/index.js
POST /auth/spare-parts now creates the part and a paired Type-1 WO in
one call, returning {partId, workOrderId, workOrderCode}. POST
/auth/work-orders resolves the target part for both flows from
deviceIdentifier (id, code, serialNum, or macAddress) and rejects
unknown deviceTypes with ERR_INVALID_DEVICE_TYPE / unresolved parts
with ERR_PART_NOT_FOUND.
Adds an open `warranty: { vendor, fields }` shape to the create and
update body schemas; vendor-specific field validation lives in the
inventory worker so app-node stays agnostic of vendor differences.
Export accepts either the WO uuid or its IVI-* code in the path and a
required ?format=pdf|csv|docx querystring. CSV flattens one row per
parts-movement entry (WO header columns repeated). PDF is server-rendered
via PDFKit and includes header, triage, work log, parts movements, file
references, and a vendor-specific warranty section. format=docx returns
501 with ERR_EXPORT_FORMAT_NOT_IMPLEMENTED until phase 2.
Drops the pdfkit dependency and the PDF renderer. The export endpoint
now serves CSV directly; pdf and docx both 501 with
ERR_EXPORT_FORMAT_NOT_IMPLEMENTED, keeping the route shape stable for
when the frontend renders PDF client-side.
Mirrors the new MINER_LOCATIONS enum from the inventory worker.
…ters

Querystring shortcuts (location, status, q) translate to a mingo query
forwarded to the rack via listThings + getThingsCount — no app-node
post-filtering. q is a case-insensitive regex against code, info.serialNum,
and info.macAddress. WO things are excluded by default via type:{\$ne:...},
overridable by an explicit query.type. Cache key includes every filter
shortcut so combinations don't collide.
…to utils

Both list handlers (work-orders + spare-parts) hand-rolled the same
regex-escape + listThings/getThingsCount/pagination skeleton; collapses
to a single utils helper used by both. Cache keys for the two GET list
routes now canonicalize their JSON inputs via stableJsonString so
semantically-equal queries share a cache slot instead of missing each
other on key-order differences.
…o polling

App-node was polling listThings after each pushAction to surface the
rack-assigned thing code in the response — exactly the
'submit-and-poll-in-app-node' anti-pattern the codebase had already
rejected. registerSparePart now pre-generates partId/woId, fires both
registerThing pushActions in parallel, and returns
{partId, workOrderId, partActionId, workOrderActionId, errors}. Clients
read the eventually-assigned codes from GET /auth/actions/done/:id like
every other write in the app.

Removes waitForThing + sleep from utils and the two timeout constants
that only existed to tune it.
…to#10 tetherto#12 tetherto#20

tetherto#3 (atomicity) — updateSparePart now checks the part pushAction's
   errors[] before firing the WO partsMoves append. If the part write
   was rejected at push time, throws ERR_PART_UPDATE_PUSH_FAILED:<msg>
   with a 502 instead of silently enqueueing the WO append. Response
   shape also gains partActionId / workOrderActionId / workOrderAppendErrors
   so the FE can detect a post-push WO failure and trigger a manual
   reconcile.
tetherto#6 (status casing) — WO status enum normalised to 'inProgress' so
   every info.* enum value follows the camelCase convention adopted
   for MINER_LOCATIONS.
tetherto#8 (registerSparePart field sprawl) — drops the
   info.model→info.deviceModel→'unknown' fallback chain and the
   serialNum→macAddress→partId fallback. deviceModel and serialNum
   are now both required up front with explicit ERR_*_REQUIRED errors.
tetherto#10 (action latency) — register/update responses include
   expectedActionLatencyMs sourced from ctx.conf.expectedActionLatencyMs
   (default 1000) so the FE can pace its /auth/actions/done/:id polling
   instead of guessing.
tetherto#12 (export 501) — error message now carries the requested format
   (ERR_EXPORT_FORMAT_NOT_IMPLEMENTED:pdf / :docx) so the FE can tell
   which format was rejected without re-reading the request.
tetherto#20 (pagination) — listThingsWithCount now slices the dedupe'd union
   down to the requested limit so multi-ork fan-out can't return more
   rows than the caller asked for. hasMore is now derived from
   offset + limit rather than offset + data.length. Multi-rack
   offset-pagination remains best-effort because each rack applies
   the offset locally; documented inline.
Mirrors the inventory rack revert — keeps existing 'in_progress' WO
records valid for transitions.
Mirrors the inventory rack revert.
@mukama mukama changed the title feat: expose Work Order HTTP API + RBAC feat: work order + spare parts HTTP API May 18, 2026
mukama added 2 commits May 20, 2026 15:17
Updates the work-order file handlers to use the renamed generic file
RPCs and tags each call with FILE_TYPES.WORK_ORDER so the rack can
dispatch on it.
loadFile / removeFile are now called with { workOrderId, fileId } so the
rack resolves the blob descriptor from the work order it belongs to —
app-node no longer forwards a raw blobRef. deleteWorkOrderFile surfaces
the rack's { cleared } result as blobCleared so the HTTP caller can tell
whether the underlying blob was actually removed.
Comment thread config/common.json.example Outdated
Comment thread workers/lib/server/handlers/spareParts.handlers.js Outdated
Comment thread workers/lib/server/handlers/workOrderFiles.handlers.js Outdated
Comment thread workers/lib/server/lib/workOrderExport.js Outdated
Comment thread workers/lib/utils.js Outdated
mukama added 5 commits May 21, 2026 22:11
- resolve the work_order rack from the ork rack registry via listRacks
  (getWorkOrderRackId, cached on ctx) and drop the workOrderRackId config
  key — the rack id is discovered, not deployment config
- relocate submitWorkOrderAction out of generic utils into the new
  server/lib/workOrders module alongside getWorkOrderRackId
- add WORK_ORDER_TERMINAL_STATUSES constant, replacing the duplicated
  ['closed', 'cancelled'] literals across the WO handlers
- workOrderExport: derive the CSV header and rows from one [name,
  extractor] schema instead of two hand-synced lists
- rename pushOne -> pushSingleAction in registerSparePart
renderWorkOrderCsv builds the header from the work order's own properties
— top-level code plus every info field, then each parts-movement entry's
keys — so the CSV tracks the worker response schema with no hand-kept
column list. Drops the CSV_HEADERS / WO_COLUMNS / MOVE_COLUMNS definitions.
Align the work order and spare parts file names with the repo convention
(cooling.system.routes.js, energy.system.routes.js, etc.): camelCase
basenames become dot-separated — spareParts.handlers.js ->
spare.parts.handlers.js, workOrders.js -> work.orders.js,
workOrderExport.js -> work.order.export.js. Updates every require() path,
route registration in index.js, and test references.
@tekwani tekwani merged commit 7970688 into tetherto:develop May 22, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants