25 KiB
Task 005: Repair Parts Tracking
Status
Approved implementation specification. Not yet implemented.
This task introduces a minimal durable component registry only to support
tracked-component references in repair-part history. It does not implement the
broader component inventory, lifecycle, installation, location, or warehouse
model described as future direction in
docs/design/COMPONENT_DOMAIN_MODEL.md.
Objective
Allow authorized operators to record what was used during an active repair as exactly one of:
- a quantity-based consumable; or
- one tracked component identified by a durable component UUID.
Repair-part history is immutable. Corrections use an explicit void action and, when necessary, a new replacement entry. Original entries and events are never edited or physically deleted.
Product Intent
A technician should be able to determine which consumables and tracked components were used during a repair. Consumable records preserve their description, quantity, and unit. Tracked-component records preserve both the live component UUID and an immutable display snapshot captured when the entry is recorded.
All authoritative actors come from authentication. All authoritative timestamps and repair-event sequences are server or database controlled.
Authoritative Boundaries
Task 004 remains authoritative for:
- control-board replacement;
- MAC ownership;
- control-board installation and removal history;
- current control-board and MAC projections;
CONTROL_BOARD_REPLACEDevents; and- the
/api/miners/{miner_id}/change-maccompatibility alias.
Task 005 records repair usage only. It must not install, remove, replace, transfer, relocate, or change ownership of a tracked component. In particular, recording a control-board component must not:
- replace a control board or transfer its MAC address;
- modify
miners.control_board_snorminers.current_mac; - modify
miner_network_identities,miner_mac_ownerships, orminer_control_board_installations; - append
CONTROL_BOARD_REPLACED; or - substitute for Task 004's explicit replacement workflow.
The existing miner_control_board_installations.id identifies an installation
history row, not a durable component. The existing miner_hashboards.id
identifies a miner-slot projection, not a component that retains identity
through movement. Task 005 must not repurpose either UUID as a component UUID or
infer component identity from a serial number, MAC address, miner, slot, or
installation.
Scope
- a minimal durable component registry;
- narrow component creation and detail reads;
- focused repair-part list, record, and void routes;
- capability-based permissions;
- mutually exclusive consumable and tracked-component request shapes;
- fixed-precision quantities and controlled units;
- immutable tracked-component display snapshots;
- explicit void and replacement-entry corrections;
- immutable, explicitly sequenced repair events;
- atomic transactions and concurrency control;
- PostgreSQL constraints, foreign keys, and immutability protections; and
- focused, migration, regression, architecture, and route tests.
Out Of Scope
- generalized component inventory or generic component CRUD;
- component update, delete, list, search, merge, or deduplication APIs;
- component locations, movements, installations, or removals;
- component lifecycle or condition workflows;
- component ownership transfer;
- warehouse inventory and transfers;
- stock levels, allocation, or automatic stock deduction;
- purchasing, suppliers, vendors, pricing, invoices, and cost accounting;
- warranties and RMA workflows;
- reorder automation;
- automatic creation of components while recording repair parts;
- automatic installation, removal, ownership transfer, or location tracking;
- specialized component tables or attributes;
- generalized bills of materials;
- bulk import;
- photos and work orders; and
- redesign or migration of Task 004 structures.
Terminology
Component: A minimally registered physical part with an immutable UUID and constrained type. Task 005 records identity and basic display metadata only.
Repair-part entry: An immutable record associating one consumable use or one tracked component with a repair.
Consumable: A normalized description, positive quantity, controlled unit, and optional notes. It has no component UUID.
Tracked component: One existing component referenced by UUID. Its repair-part entry includes a display snapshot and has no consumable quantity or unit.
Display snapshot: Immutable component type, Asset ID, manufacturer, model, and manufacturer serial values copied from the component when the repair-part entry is recorded. Nullable source values remain null.
Void: A one-way audited correction that preserves the original entry. A corrected value is represented by voiding the incorrect entry and recording a new entry.
Minimal Component Registry
Component Types
The initial controlled component types are:
control_board;hashboard;power_supply; andfan.
Do not accept other or arbitrary free-text types. Expanding the vocabulary
requires a reviewed migration and corresponding model and test updates.
Component Schema
Create the focused components table with:
idUUID primary key with a database default;component_typeconstrained text, required;asset_idtext, optional;manufacturertext, optional;modeltext, optional;manufacturer_serialtext, optional;created_attimezone-aware timestamp, required and server controlled; andcreated_byauthenticated user UUID, required.
Rules:
id,component_type,created_at, andcreated_byare immutable;- optional text is trimmed, nonblank when present, and limited to 255 characters;
- Asset IDs are case-insensitively unique among components after trimming;
- component Asset IDs do not share a database uniqueness namespace with miner Asset IDs in this task;
- manufacturer serials are not assumed globally unique;
created_byreferences the existing user identity withRESTRICTdeletion behavior;- no client may supply
id,created_at, orcreated_by; - no component is created implicitly from a repair-part request; and
- Task 005 provides no component metadata update or deletion workflow.
The UUID is the authoritative component identity. Asset ID and manufacturer metadata do not replace it. A later explicit task may evolve mutable display metadata while preserving the UUID and history.
API
Component Routes
Task 005 adds exactly these narrow component routes:
POST /api/components
GET /api/components/{component_id}
POST /api/components creates one component and returns 201 Created.
GET /api/components/{component_id} returns one component and 200 OK.
Unknown component UUIDs return 404. Malformed UUIDs use the existing 422
validation response.
Do not add component list, search, update, patch, delete, install, remove, move, or lifecycle routes.
Repair-Part Routes
Task 005 adds exactly these repair-part routes:
GET /api/repairs/{repair_id}/parts
POST /api/repairs/{repair_id}/parts
POST /api/repairs/{repair_id}/parts/{repair_part_id}/void
The list returns 200 OK. Recording returns 201 Created. A successful void
returns 200 OK with the voided repair-part representation. No route physically
deletes or destructively edits a repair-part entry.
Register and test these routes without changing the precedence or behavior of
legacy static routes, including /api/repairs/parts-summary.
Response Envelopes
Use the existing typed success envelope containing data, meta, and error.
Component responses expose only the approved component fields. Repair-part
responses expose the stored consumable fields or stored component snapshot,
plus component UUID, notes, recording metadata, and void metadata.
A tracked repair-part response must use its immutable snapshot for historical
display. It may expose component_id so a caller can separately request the
live component. It must not silently substitute current component metadata for
the snapshot.
List Ordering
Return repair-part entries in ascending recorded repair-event sequence. The explicit database sequence, not timestamps, determines order. Void state is shown on the original entry; void events remain separately visible in the existing repair timeline.
No pagination or filtering is added because the collection is bounded to one repair. A later change requires compatibility review.
Permissions
Introduce these proposed capabilities:
components.readfor component detail reads;components.createfor component creation; andrepairs.parts.managefor recording and voiding repair-part entries.
Reuse implemented repairs.read for the repair-part list.
Role grants:
| Capability | Admin | Manager | Technician | Read Only |
|---|---|---|---|---|
components.read |
yes | yes | yes | yes |
components.create |
yes | yes | yes | no |
repairs.read |
yes | yes | yes | yes |
repairs.parts.manage |
yes | yes | yes | no |
Routers check capabilities, never role names. Every route requires authentication and focused unauthenticated, unauthorized, and authorized-role tests.
Request Models
Reject unknown fields in every request. Actor UUIDs, timestamps, sequences, event payloads, snapshots, and persistence identifiers are never accepted from clients.
Create Component
{
"component_type": "hashboard",
"asset_id": "HB-0042",
"manufacturer": "MicroBT",
"model": "M50 Hashboard",
"manufacturer_serial": "HB-SN-12345"
}
Only component_type is required. Optional text values are normalized by
trimming surrounding whitespace and are rejected when blank after trimming.
Duplicate normalized Asset IDs return 409 Conflict without leaking a database
exception.
Record Consumable
{
"consumable": {
"description": "Thermal paste",
"quantity": 1,
"unit": "gram"
},
"tracked_component_uuid": null,
"notes": "Applied during board service."
}
Record Tracked Component
{
"consumable": null,
"tracked_component_uuid": "c79f9f36-1e89-4dbc-8a84-9ff3167614be",
"notes": "Used during this repair."
}
Exactly one of consumable and tracked_component_uuid must be non-null. A
tracked-component request refers to one existing component and never creates,
installs, removes, transfers, or mutates it.
Void Repair Part
{
"reason": "Recorded against the wrong repair."
}
The reason is required, trimmed, nonblank, and limited to 1000 characters. The actor and timestamp come from authentication and the server.
Consumable Rules
A consumable requires:
- normalized, nonblank
description, limited to 255 characters; quantityrepresented asNUMERIC(12,3)or the repository-equivalent fixed precision;- quantity greater than zero;
- one controlled
unit; and - optional shared notes.
The initial controlled units are:
each;gram;millilitre;metre; andcentimetre.
Do not accept aliases or arbitrary free-text units. The service and request model reject more than three fractional digits or values outside the fixed precision range.
When unit is each, quantity must be a whole number. Measurement units permit
fractional quantities to three decimal places. Quantity describes repair usage
only and never changes stock.
Repair-Part Schema
Create the focused repair_parts table with:
idUUID primary key with a database default;repair_idUUID, required;consumable_descriptiontext, nullable;quantityNUMERIC(12,3), nullable;unitconstrained text, nullable;component_idUUID, nullable;component_type_snapshotconstrained text, nullable;component_asset_id_snapshottext, nullable;component_manufacturer_snapshottext, nullable;component_model_snapshottext, nullable;component_manufacturer_serial_snapshottext, nullable;notestext, nullable;recorded_byauthenticated user UUID, required;recorded_attimezone-aware timestamp, required and server controlled;recorded_event_idrepair-event UUID, required;voided_byauthenticated user UUID, nullable;voided_attimezone-aware timestamp, nullable;void_reasontext, nullable;void_event_idrepair-event UUID, nullable.
Foreign keys use RESTRICT deletion behavior for repairs, users, components,
and events so authoritative history cannot disappear through cascades.
The repository captures tracked-component snapshots from the locked component row in the same transaction as the repair-part insert and event. Client values cannot populate snapshot columns. Nullable component metadata remains null in the snapshot rather than being fabricated.
The original entry shape, consumable values, component UUID and snapshot, notes, recording actor, timestamp, and recorded event data are immutable. A void adds only the write-once void fields and immutable void event.
PostgreSQL Invariants
PostgreSQL must enforce the complete XOR shape:
- consumable branch:
consumable_description,quantity, andunitare all present;component_idand every component snapshot field are null; or
- tracked-component branch:
component_idandcomponent_type_snapshotare present;consumable_description,quantity, andunitare all null;- optional snapshot fields match the nullable values captured at record time.
Additional constraints and protections:
- quantity is greater than zero when present;
- quantity uses at most three fractional digits;
eachrequiresquantity = trunc(quantity);- unit is one of
each,gram,millilitre,metre, orcentimetre; - component and snapshot types use the approved component-type constraint;
- descriptions and snapshot text are trimmed and nonblank when present;
- notes are null or trimmed, nonblank, and limited to 4000 characters;
- recording actor, timestamp, and event UUID are required;
- component UUID references an existing component;
- recorded and void event UUIDs are unique and reference repair events;
- void actor, timestamp, reason, and event UUID are either all null or all present;
- void reason is trimmed, nonblank, and limited to 1000 characters;
- void timestamp is not earlier than recorded timestamp;
- a repair part cannot be associated with another repair through its event references;
- original entry fields cannot be updated;
- void fields transition only once from all-null to all-present; and
- repair-part and repair-event rows cannot be physically deleted.
Use PostgreSQL constraints, foreign keys, indexes, and narrowly scoped triggers where a declarative constraint cannot enforce cross-row immutability or event ownership. Service validation provides clear domain errors but does not replace database integrity.
Repair State Rules
- component creation and detail reads do not change repair state;
- record repair-part entries only while the repair is active;
- void repair-part entries only while the repair is active;
- list entries and void state after repair completion;
- completed and cancelled repairs are read-only for Task 005;
- no unrestricted editing is allowed in any repair state; and
- Task 005 never changes repair status or miner lifecycle.
Begin Repair, Repair Timeline, Return to Service, and Task 004 behavior must remain unchanged.
Events
Add these immutable repair event types:
REPAIR_PART_RECORDED; andREPAIR_PART_VOIDED.
REPAIR_PART_RECORDED contains:
- repair-part UUID;
- entry kind:
consumableortracked_component; - the stored description, quantity, and unit for a consumable; or
- the stored component UUID and complete component display snapshot for a tracked component; and
- optional notes.
It must not claim that a tracked component was installed, removed, replaced, transferred, relocated, or changed.
REPAIR_PART_VOIDED contains:
- repair-part UUID; and
- required correction reason.
The event actor, timestamp, and sequence use existing repair-event columns and
are server controlled. Clients request domain actions, not raw events. Events
are append-only and ordered only by the repair's explicit database sequence.
The repair_parts table is the queryable record and does not replace Task 004
history.
Transactions And Concurrency
Component Creation
Component creation is one transaction. The database generates its UUID and timestamp, the authenticated actor is required, and the normalized Asset ID constraint resolves concurrent duplicates. No repair or component event is invented for this minimal identity registration.
Record Repair Part
Resolve the repair's miner UUID, then lock in this order:
Miner -> Repair -> Component (tracked entries only)
After locking, revalidate the miner and active repair state. For a tracked
entry, lock and load the component, capture its snapshot, insert the repair-part
row, allocate the next repair-event sequence, and append exactly one
REPAIR_PART_RECORDED event in one transaction.
For a consumable, perform the same workflow without a component lock. Any validation, insert, snapshot, sequence, or event failure rolls back the complete action.
Void Repair Part
Resolve the repair's miner UUID, then lock in this order:
Miner -> Repair -> Repair Part
Revalidate the active repair and route ownership after locking. Set the
write-once void fields and append exactly one REPAIR_PART_VOIDED event in one
transaction. Concurrent void requests produce one success and one 409
conflict, with exactly one void event.
The Miner lock and Repair lock serialize record and void actions with Return to
Service and Task 004. If repair completion wins, the later Task 005 action sees
a terminal repair and fails with 409. If Task 005 wins, its complete entry and
event commit before completion proceeds.
Architecture
Preserve:
Router
-> Service
-> Repository
-> PostgreSQL
Routers own authentication, permission checks, request validation, HTTP status mapping, and response envelopes. Services own normalization, component and repair-part rules, workflow coordination, and domain exceptions. Repositories own parameterized SQL, locks, snapshots, persistence, and PostgreSQL integrity translation.
Routers contain no SQL, database-driver imports, transaction handling, or business rules. Services and repositories contain no FastAPI imports.
Error Behavior
- unauthenticated:
401; - missing permission:
403; - unknown component, repair, or repair part:
404; - malformed UUID, unknown field, invalid type, invalid unit, invalid shape,
invalid precision, or invalid text:
422; - duplicate normalized component Asset ID:
409; - inactive repair, already-voided entry, or concurrent state conflict:
409; and - persistence failures: translated without exposing SQL or driver details.
A repair-part UUID that does not belong to the repair in the route is treated as
not found and returns 404.
Migration
Create additive Alembic revision 0014_repair_parts_tracking with parent
0013_control_board_replacement.
The migration must:
- create
componentswith its type, text, actor, timestamp, Asset ID uniqueness, immutability, and deletion protections; - create
repair_partswith XOR, quantity, unit, snapshot, actor, timestamp, event, void, immutability, and deletion protections; - add
components.read,components.create, andrepairs.parts.managewith the approved role assignments; - extend the repair-event type constraint with
REPAIR_PART_RECORDEDandREPAIR_PART_VOIDED; - add only indexes justified by component lookup, normalized Asset ID uniqueness, repair-part listing, foreign keys, and event relationships;
- preserve all existing data without inventing components, repair parts, actors, timestamps, snapshots, or events; and
- leave Task 004 tables, constraints, projections, routes, and history intact.
There is no legacy backfill. Generic maintenance events, telemetry, serial numbers, miner hashboard projections, and Task 004 installation rows are not sufficient evidence for durable component identity or repair-part usage.
Downgrade is permitted only when no component, repair-part, or Task 005 repair event rows exist. Otherwise it must refuse with a clear actionable error rather than delete authoritative identity or history. An empty safe downgrade removes Task 005 constraints, permissions, tables, and event values in dependency order, then supports re-upgrade.
Validate previous-head upgrade, preflight behavior, safe downgrade, downgrade refusal, re-upgrade, and an empty database from base through head.
Tests
Component Registry
- create and detail-read authorization for every role;
- successful creation with required fields only and with all metadata;
- UUID, actor, and timestamp authority;
- all approved component types and rejection of unknown types;
- whitespace normalization, text limits, and blank optional values;
- case-insensitive normalized Asset ID uniqueness and concurrent duplicates;
- nullable manufacturer metadata and nonunique manufacturer serials;
- immutable identity, type, actor, and timestamp;
- no implicit component creation from a repair-part request;
- no list, search, update, delete, lifecycle, location, or installation routes; and
- missing and malformed component UUID behavior.
Repair-Part Requests And Reads
- consumable and tracked-component happy paths;
- controlled units and rejection of aliases or free text;
NUMERIC(12,3)boundaries and rejection of excess precision;- positive fractional measurement quantities;
- whole-number
eachquantities and rejection of fractionaleach; - both entry branches supplied and neither branch supplied;
- incomplete consumable shapes;
- unknown, malformed, and client-created component UUIDs;
- optional notes and blank or overlength text;
- server-controlled actors, timestamps, sequences, events, and snapshots;
- stable list ordering by recorded event sequence;
- response envelopes and historical reads after completion;
- component metadata changed after recording without changing the stored snapshot or historical display;
- no N+1 component or actor queries; and
- route ownership for nested repair-part UUIDs.
Workflow And Integrity
- add and void authorization for every role;
- active repair enforcement;
- explicit void and void-plus-replacement correction;
- already-voided and cross-repair behavior;
- concurrent record versus Return to Service;
- concurrent void requests;
- rollback on component lookup, snapshot, part insert, sequence, or event failure;
- direct XOR, type, quantity, unit, text, foreign-key, event, and void constraints;
- original-field, event, and physical-deletion protections;
- confirmation that tracked-component recording changes no component, miner, lifecycle, installation, ownership, MAC, location, or condition state;
- confirmation that a control-board reference cannot bypass or duplicate Task 004's replacement behavior; and
- lock-order and transaction-boundary tests.
Regression And Validation
- Begin Repair tests;
- Repair Timeline tests;
- Return to Service tests;
- Task 004 control-board replacement and compatibility-alias tests;
- Inventory lifecycle and permission regressions;
- legacy static repair-route precedence tests;
- migration upgrade, downgrade, refusal, re-upgrade, previous-head, and empty database tests;
- full backend suite;
- disposable development baseline;
- Python compilation and shell syntax checks;
- router SQL, driver, transaction, and business-rule audit;
- service and repository FastAPI audit;
- exact route-count audit; and
git diff --check.
Acceptance Criteria
- Components receive immutable UUIDs and only the minimal approved identity and display metadata.
- Component types and consumable units use the approved controlled values.
- Every repair-part entry represents exactly one complete consumable or one existing tracked component, never both or neither.
- Consumable quantities use fixed precision, remain positive, and enforce
whole numbers for
each. - Tracked entries preserve a live component foreign key and immutable display snapshot.
- Historical display does not change when live component metadata later changes.
- Recording a tracked component does not install, remove, replace, transfer, relocate, create, or merge a component or miner.
- Original entries and events are immutable; corrections use an audited void and optional replacement entry without physical deletion.
- Actors, timestamps, snapshots, and event sequences are authoritative and server controlled.
- Record and void actions are atomic and concurrency safe.
- Task 004 remains authoritative for control-board and MAC behavior.
- No generalized inventory, warehouse, purchasing, supplier, stock, location, lifecycle, warranty, RMA, or cost system is introduced.
- Existing repair workflows and static route precedence remain unchanged.
- Migration lifecycle, focused tests, regressions, architecture audits, and the disposable baseline pass.
Completion Report
Report files, migration revision and parent, routes and exact route count, permissions and role grants, component registry schema, repair-part schema, request and response models, constraints, snapshots, event payloads and sequencing, transaction and lock strategy, downgrade behavior, tests and counts, regression results, architecture and route audits, known limitations, and final status.
Do not commit. Do not push.