feat: split Repairs into FM, Repair, and Custody workflows
CI / Lint · Typecheck · Test · Build (push) Successful in 44s
CI / Playwright (smoke) (push) Has been skipped
CI / Build & push images (push) Successful in 1m0s

The old Repairs module had grown ticketing-system features (status lifecycle,
comments, assignee, notes) that duplicate what the external ticketing tool
already owns. Vector only needs to track whether maintenance is open or closed.

- Rename RepairJob -> Fm (OPEN/CLOSED only), drop RepairComment, assignee, notes
- New Repair table: persistent log of physical part swaps, with ingest on
  unknown broken MPN via partModels.upsertByMpn
- New custody model: PENDING_DROP_IN_CUSTODY / PENDING_DESTRUCTION_IN_CUSTODY
  states + Part.custodianId, with a "My Custody" page for drop-off
- PartModel.destroyOnFail routes broken parts to the destruction path
- Host lookup on /fms and /repairs accepts hostId XOR assetId
- Wire the dormant webhook emitter: fm.opened, fm.closed, repair.logged
- Single fresh Prisma migration (dev DB was wiped, no backfill)

Tests: 60 passing (custody transitions in parts.test.ts; new fms.test.ts,
repairs.test.ts, custody.test.ts covering happy paths, validation failures,
webhook emissions, and ingest-on-unknown-MPN).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 12:22:56 -04:00
parent 6690d8a5dd
commit 3d77f2846d
54 changed files with 3304 additions and 1287 deletions
+16 -10
View File
@@ -1,6 +1,13 @@
import { z } from 'zod';
export const PartState = z.enum(['SPARE', 'DEPLOYED', 'BROKEN', 'PENDING_DESTRUCTION']);
export const PartState = z.enum([
'SPARE',
'DEPLOYED',
'BROKEN',
'PENDING_DESTRUCTION',
'PENDING_DROP_IN_CUSTODY',
'PENDING_DESTRUCTION_IN_CUSTODY',
]);
export type PartState = z.infer<typeof PartState>;
export const Role = z.enum(['ADMIN', 'TECHNICIAN']);
@@ -11,17 +18,16 @@ export const PartEventType = z.enum([
'STATE_CHANGED',
'LOCATION_CHANGED',
'FIELD_UPDATED',
'REPAIR_STARTED',
'REPAIR_COMPLETED',
'REPAIR_CANCELLED',
'REPAIR_COMMENTED',
'FM_OPENED',
'FM_CLOSED',
'PART_SWAPPED',
'TAG_ADDED',
'TAG_REMOVED',
]);
export type PartEventType = z.infer<typeof PartEventType>;
export const RepairStatus = z.enum(['PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED']);
export type RepairStatus = z.infer<typeof RepairStatus>;
export const FmStatus = z.enum(['OPEN', 'CLOSED']);
export type FmStatus = z.infer<typeof FmStatus>;
export const CsvImportStatus = z.enum([
'PENDING',
@@ -39,9 +45,9 @@ export const WebhookEventName = z.enum([
'part.deleted',
'part.state_changed',
'part.location_changed',
'repair.started',
'repair.completed',
'repair.cancelled',
'fm.opened',
'fm.closed',
'repair.logged',
'tag.assigned',
'tag.removed',
]);