Files
minerops/docs/tasks/005-repair-parts-tracking.md
T

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:

  1. a quantity-based consumable; or
  2. 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_REPLACED events; and
  • the /api/miners/{miner_id}/change-mac compatibility 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_sn or miners.current_mac;
  • modify miner_network_identities, miner_mac_ownerships, or miner_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; and
  • fan.

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:

  • id UUID primary key with a database default;
  • component_type constrained text, required;
  • asset_id text, optional;
  • manufacturer text, optional;
  • model text, optional;
  • manufacturer_serial text, optional;
  • created_at timezone-aware timestamp, required and server controlled; and
  • created_by authenticated user UUID, required.

Rules:

  • id, component_type, created_at, and created_by are 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_by references the existing user identity with RESTRICT deletion behavior;
  • no client may supply id, created_at, or created_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.read for component detail reads;
  • components.create for component creation; and
  • repairs.parts.manage for 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;
  • quantity represented as NUMERIC(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; and
  • centimetre.

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:

  • id UUID primary key with a database default;
  • repair_id UUID, required;
  • consumable_description text, nullable;
  • quantity NUMERIC(12,3), nullable;
  • unit constrained text, nullable;
  • component_id UUID, nullable;
  • component_type_snapshot constrained text, nullable;
  • component_asset_id_snapshot text, nullable;
  • component_manufacturer_snapshot text, nullable;
  • component_model_snapshot text, nullable;
  • component_manufacturer_serial_snapshot text, nullable;
  • notes text, nullable;
  • recorded_by authenticated user UUID, required;
  • recorded_at timezone-aware timestamp, required and server controlled;
  • recorded_event_id repair-event UUID, required;
  • voided_by authenticated user UUID, nullable;
  • voided_at timezone-aware timestamp, nullable;
  • void_reason text, nullable;
  • void_event_id repair-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, and unit are all present;
    • component_id and every component snapshot field are null; or
  • tracked-component branch:
    • component_id and component_type_snapshot are present;
    • consumable_description, quantity, and unit are 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;
  • each requires quantity = trunc(quantity);
  • unit is one of each, gram, millilitre, metre, or centimetre;
  • 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; and
  • REPAIR_PART_VOIDED.

REPAIR_PART_RECORDED contains:

  • repair-part UUID;
  • entry kind: consumable or tracked_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 components with its type, text, actor, timestamp, Asset ID uniqueness, immutability, and deletion protections;
  • create repair_parts with XOR, quantity, unit, snapshot, actor, timestamp, event, void, immutability, and deletion protections;
  • add components.read, components.create, and repairs.parts.manage with the approved role assignments;
  • extend the repair-event type constraint with REPAIR_PART_RECORDED and REPAIR_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 each quantities and rejection of fractional each;
  • 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

  1. Components receive immutable UUIDs and only the minimal approved identity and display metadata.
  2. Component types and consumable units use the approved controlled values.
  3. Every repair-part entry represents exactly one complete consumable or one existing tracked component, never both or neither.
  4. Consumable quantities use fixed precision, remain positive, and enforce whole numbers for each.
  5. Tracked entries preserve a live component foreign key and immutable display snapshot.
  6. Historical display does not change when live component metadata later changes.
  7. Recording a tracked component does not install, remove, replace, transfer, relocate, create, or merge a component or miner.
  8. Original entries and events are immutable; corrections use an audited void and optional replacement entry without physical deletion.
  9. Actors, timestamps, snapshots, and event sequences are authoritative and server controlled.
  10. Record and void actions are atomic and concurrency safe.
  11. Task 004 remains authoritative for control-board and MAC behavior.
  12. No generalized inventory, warehouse, purchasing, supplier, stock, location, lifecycle, warranty, RMA, or cost system is introduced.
  13. Existing repair workflows and static route precedence remain unchanged.
  14. 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.