03 — Integration

Work Management is a member of the R9 Work & Supply network (message protocol v1). Its contract is written in schema/work_management.integration.dsl (authoring spec: docs/implementation/integration_authoring_spec.md) and turned into code by python -m codegen.cli integration work_management --network work_supply; the business logic for each message is hand-written in integration/handlers.py.

The whole network — every message with its publisher, subscribers and implementation status, generated from the contracts — is in networks/work_supply/NETWORK_MAP.md (python -m codegen.cli network-docs work_supply).

What it owns

Network entity Model Business reference
work.work_order WorkOrder work_order_number (WO-…)
work.material_requirement WorkMaterialRequirement requirement_number (MR-…)
work.removed_component / work.installed_component RemovedComponent / InstalledComponent —
work.maintenance_template MaintenanceTemplate code (found with work.search_maintenance_templates)

Every owned record has a network id (cid, assigned on create, never changes). Other applications refer to it by that id and show its business reference.

Messages

Kind Message When / what Status
publishes work.work_order_created a work order is created ✅ automatic
publishes work.material_requirement_created a material requirement is created ✅ automatic
publishes work.material_requirement_changed a requirement's status changes ✅ automatic
publishes work.work_started / work_suspended / work_closed work order status moves ✅ automatic
publishes work.late_cost_recorded a cost line added to an order that already closed (parts, bought material, contractor work or a late time entry): the order, the cost type, the amount ✅ automatic
publishes work.returned_to_service / work_verified completion records ✅ automatic
publishes work.component_removed / work.component_installed a removal / an installation, with the date and the unit's main SMU (main_smu_value, main_smu_unit, main_smu_metric_cid/ref; blank = the last known reading applies). The removal also keeps the old removal_usage_value / removal_usage_unit keys for one release ✅ automatic
publishes work.work_closed also says oil_changed (the order came from an oil_change template) with the template's SMCS code and the template reference, and the cost: actual_cost split into cost_labour, cost_parts, cost_purchased, cost_contractor (cost_currency) ✅ automatic
consumes inventory.item_reserved set the requirement reserved, fill in the reservation ✅
consumes inventory.item_issued a parts cost line (quantity x the stock's average cost) and the requirement becomes issued ✅
consumes inventory.item_unavailable set it unavailable; hold a critical order awaiting_parts ✅
consumes purchasing.delivery_commitment_changed / delivery_delayed expected date, readiness ⏳ R10
consumes warranty.candidate_detected, governance.obligation_assigned warranty flag, external dependency ⏳ later
consumes workforce.employee_registered / employee_changed create or update the read-only Person copy (same cid as the employee: name, number, position, trade, status); for a contractor's person find or create the contractor Organisation carrying the supplier number (supplier_cid / supplier_ref, code = the supplier number) ✅
consumes workforce.employee_left the Person copy becomes inactive (nothing is deleted) ✅
accepts work.create_material_requirement create a requirement (rejects closed / unknown orders) ✅
accepts work.create_work_order with a template, the work type comes from it, the priority defaults to planned, and its tasks and tools are copied ✅
accepts work.record_component_failure, work.record_component_installation a removal / an installation against the order ✅
accepts work.verify_and_close record the verification; when accepted and the closure guards hold (the order is in progress, in test, returned to service or ready for verification, and no task is in progress or failed) write the closeout and close the order ✅
accepts work.start_work start the work: in progress, start date set (publishes work_started); already started is answered as it is; awaiting parts, closed or cancelled cannot start ✅
accepts work.record_time time worked on the order, by person (employee number or name), hours, time type and trade (trade_cid or the trade's code trade_code; none: the person's trade from the copy), stamped on the entry; negative hours correct an earlier entry; on a closed order it is a late cost line; an unknown trade is rejected (unknown_trade) ✅
accepts work.reopen_work_order a closed order goes back to in progress (rejects not closed); closing it again re-costs it and Finance posts the difference as an amendment ✅
accepts work.record_job_cost one purchased or contractor cost line (Purchasing sends one per received order line); a source already recorded is not recorded twice ✅
accepts review_material_delay, update_external_constraint, return_to_service ⏳ not yet (answers handler not found)
answers work.open_work_orders orders not closed or cancelled (or in one status), oldest first, at most 200 ✅
answers work.work_order_status, work.material_requirement_status, work.search_maintenance_templates read-only status; the template lookup ✅
answers work.readiness_status, work.asset_work_history ⏳ not yet

Published events are written in the same transaction as the business change (CRUD screen or message handler alike) and delivered afterwards by the publisher, so nothing is lost or sent twice. Inbound messages are de-duplicated by message id; commands also by their idempotency key.

References to other applications

Points at Owner Fields
asset.asset (units) and asset.component (serialised components) asset register asset_cid / asset_ref; component_cid / component_ref (sent in messages as component_asset_cid / component_asset_ref) …
asset.asset_group, classification.smcs, classification.modifier, lifecycle.usage_metric_type asset register, classification, lifecycle template group and SMCS; fitment SMCS and modifier; the SMU metric (main_smu_usage_metric_type_cid, sent as main_smu_metric_cid)
catalogue.item catalogue (R10) catalogue_item_cid / catalogue_item_ref
inventory.reservation inventory inventory_reservation_cid / inventory_reservation_ref — linked ↗
purchasing.purchase_requirement purchasing (R10) purchase_requirement_cid / purchase_requirement_ref
warranty.candidate warranty (later) warranty_candidate_cid / warranty_candidate_ref
workforce.trade workforce (W1) trade_cid / trade_ref on Person (the copy), WorkAssignment and WorkTimeEntry — picked with workforce.search_trades; the code follows corrections
supplier.supplier supplier supplier_cid / supplier_ref on Organisation: the supplier number of a contractor company

People: a read-only copy of Workforce (W1)

Work's Person table is Workforce's employees, kept by the employee events: a Person's cid is the employee's cid, so every …_person_id in Work (planner, supervisor, assigned, approved by, verified by …) keeps pointing at a local row while the person is owned by Workforce. Person and Organisation are view-only on Work's screens (config/model_permissions.yaml); only the events change them. The trade a person worked in is stamped on each time entry, so a later change of trade never changes a past cost. Until W2 the labour rate is looked up in Work's Labour Rate table with the trade's code (lower case) where it used to take the labour class; the figures are unchanged.

Endpoints

Endpoint For Protection
PUT /integration/messages/<id> the orchestrator delivering messages service token
GET /resolve/<cid> people following a ↗ link from another app normal login

Settings (applications.yaml env:)

INTEGRATION_PUBLISHER=1, ORCHESTRATOR_URL=http://localhost:5089 (the R9 orchestrator); ORCHESTRATOR_SERVICE_TOKEN from .env. The publisher starts on the app's first request.