feat: split Repairs into FM, Repair, and Custody workflows
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:
@@ -14,7 +14,7 @@ function makeTx(args: {
|
||||
createdAt: Date;
|
||||
partModelId: string;
|
||||
}[];
|
||||
openRepairs: number;
|
||||
openFms: number;
|
||||
eolPartModels: {
|
||||
id: string;
|
||||
mpn: string;
|
||||
@@ -35,8 +35,8 @@ function makeTx(args: {
|
||||
})),
|
||||
findMany: async () => args.parts,
|
||||
},
|
||||
repairJob: {
|
||||
count: async () => args.openRepairs,
|
||||
fm: {
|
||||
count: async () => args.openFms,
|
||||
},
|
||||
partModel: {
|
||||
findMany: async () => args.eolPartModels,
|
||||
@@ -52,7 +52,7 @@ const now = new Date('2026-04-16T00:00:00.000Z');
|
||||
const daysAgo = (n: number) => new Date(now.getTime() - n * 24 * 60 * 60 * 1000);
|
||||
|
||||
describe('analytics.dashboard', () => {
|
||||
it('aggregates totals, state counts and open repairs', async () => {
|
||||
it('aggregates totals, state counts and open FMs', async () => {
|
||||
const tx = makeTx({
|
||||
partCount: 5,
|
||||
stateRows: [
|
||||
@@ -60,14 +60,14 @@ describe('analytics.dashboard', () => {
|
||||
{ state: 'DEPLOYED', count: 2, totalPrice: 8000 },
|
||||
],
|
||||
parts: [],
|
||||
openRepairs: 4,
|
||||
openFms: 4,
|
||||
eolPartModels: [],
|
||||
bins: [],
|
||||
});
|
||||
|
||||
const r = await dashboard(tx);
|
||||
expect(r.totalParts).toBe(5);
|
||||
expect(r.openRepairs).toBe(4);
|
||||
expect(r.openFms).toBe(4);
|
||||
expect(r.byState).toEqual([
|
||||
{ state: 'SPARE', count: 3, totalPrice: 1500 },
|
||||
{ state: 'DEPLOYED', count: 2, totalPrice: 8000 },
|
||||
@@ -84,7 +84,7 @@ describe('analytics.dashboard', () => {
|
||||
{ id: 'p3', state: 'SPARE', binId: null, createdAt: daysAgo(400), partModelId: 'pm' },
|
||||
{ id: 'p4', state: 'SPARE', binId: null, createdAt: daysAgo(900), partModelId: 'pm' },
|
||||
],
|
||||
openRepairs: 0,
|
||||
openFms: 0,
|
||||
eolPartModels: [],
|
||||
bins: [],
|
||||
});
|
||||
@@ -109,7 +109,7 @@ describe('analytics.dashboard', () => {
|
||||
{ id: '3', state: 'SPARE', binId: 'b2', createdAt: daysAgo(1), partModelId: 'pm' },
|
||||
{ id: '4', state: 'SPARE', binId: null, createdAt: daysAgo(1), partModelId: 'pm' },
|
||||
],
|
||||
openRepairs: 0,
|
||||
openFms: 0,
|
||||
eolPartModels: [],
|
||||
bins: [
|
||||
{ id: 'b1', name: 'A1', room: { name: 'Lab', site: { name: 'HQ' } } },
|
||||
@@ -133,7 +133,7 @@ describe('analytics.dashboard', () => {
|
||||
{ id: '2', state: 'DEPLOYED', binId: null, createdAt: daysAgo(1), partModelId: 'pm1' },
|
||||
{ id: '3', state: 'DEPLOYED', binId: null, createdAt: daysAgo(1), partModelId: 'pm2' },
|
||||
],
|
||||
openRepairs: 0,
|
||||
openFms: 0,
|
||||
eolPartModels: [
|
||||
{
|
||||
id: 'pm1',
|
||||
|
||||
Reference in New Issue
Block a user