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:
@@ -1,43 +1,34 @@
|
||||
import { z } from 'zod';
|
||||
import { RepairStatus } from './enums.js';
|
||||
import { PaginationQuery } from './pagination.js';
|
||||
|
||||
export const CreateRepairJobRequest = z.object({
|
||||
hostId: z.string().uuid(),
|
||||
problem: z.string().trim().min(1, 'Problem is required').max(2000),
|
||||
problemPartIds: z.array(z.string().uuid()).max(100).optional(),
|
||||
assigneeId: z.string().uuid().optional().nullable(),
|
||||
notes: z.string().max(4096).optional().nullable(),
|
||||
});
|
||||
export type CreateRepairJobRequest = z.infer<typeof CreateRepairJobRequest>;
|
||||
|
||||
export const UpdateRepairJobRequest = z
|
||||
// Repair = a physical part-swap log entry. Tech enters host + broken serial/mpn + replacement serial.
|
||||
// If the broken part isn't in the catalog yet it gets auto-ingested (requires mpn + manufacturer).
|
||||
export const LogRepairRequest = z
|
||||
.object({
|
||||
status: RepairStatus.optional(),
|
||||
problem: z.string().trim().min(1).max(2000).optional(),
|
||||
problemPartIds: z.array(z.string().uuid()).max(100).optional(),
|
||||
assigneeId: z.string().uuid().nullable().optional(),
|
||||
notes: z.string().max(4096).nullable().optional(),
|
||||
hostId: z.string().uuid().optional(),
|
||||
assetId: z.string().trim().min(1).max(128).optional(),
|
||||
brokenSerial: z.string().trim().min(1).max(128),
|
||||
brokenMpn: z.string().trim().min(1).max(128),
|
||||
brokenManufacturerId: z.string().uuid(),
|
||||
replacementSerial: z.string().trim().min(1).max(128),
|
||||
fmId: z.string().uuid().optional(),
|
||||
})
|
||||
.refine((v) => Object.keys(v).length > 0, { message: 'At least one field required' });
|
||||
export type UpdateRepairJobRequest = z.infer<typeof UpdateRepairJobRequest>;
|
||||
.superRefine((v, ctx) => {
|
||||
const has = [v.hostId, v.assetId].filter((x) => x !== undefined && x !== '').length;
|
||||
if (has !== 1) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Provide exactly one of hostId or assetId',
|
||||
path: ['hostId'],
|
||||
});
|
||||
}
|
||||
});
|
||||
export type LogRepairRequest = z.infer<typeof LogRepairRequest>;
|
||||
|
||||
export const RepairJobListQuery = PaginationQuery.extend({
|
||||
status: RepairStatus.optional(),
|
||||
export const RepairListQuery = PaginationQuery.extend({
|
||||
hostId: z.string().uuid().optional(),
|
||||
problemPartId: z.string().uuid().optional(),
|
||||
assigneeId: z.string().uuid().optional(),
|
||||
openOnly: z
|
||||
.union([z.literal('true'), z.literal('false'), z.boolean()])
|
||||
.transform((v) => v === true || v === 'true')
|
||||
.optional(),
|
||||
performedById: z.string().uuid().optional(),
|
||||
fmId: z.string().uuid().optional(),
|
||||
since: z.string().datetime().optional(),
|
||||
});
|
||||
export type RepairJobListQuery = z.infer<typeof RepairJobListQuery>;
|
||||
|
||||
export const CreateRepairCommentRequest = z.object({
|
||||
content: z.string().trim().min(1, 'Comment cannot be empty').max(4000),
|
||||
});
|
||||
export type CreateRepairCommentRequest = z.infer<typeof CreateRepairCommentRequest>;
|
||||
|
||||
export const RepairCommentListQuery = PaginationQuery;
|
||||
export type RepairCommentListQuery = z.infer<typeof RepairCommentListQuery>;
|
||||
export type RepairListQuery = z.infer<typeof RepairListQuery>;
|
||||
|
||||
Reference in New Issue
Block a user