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:
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `RepairComment` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `RepairJob` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `RepairJobPart` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropIndex
|
||||
DROP INDEX "RepairComment_repairJobId_createdAt_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "RepairJob_status_openedAt_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "RepairJob_assigneeId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "RepairJob_hostId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "RepairJob_status_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "RepairJobPart_partId_idx";
|
||||
|
||||
-- DropTable
|
||||
PRAGMA foreign_keys=off;
|
||||
DROP TABLE "RepairComment";
|
||||
PRAGMA foreign_keys=on;
|
||||
|
||||
-- DropTable
|
||||
PRAGMA foreign_keys=off;
|
||||
DROP TABLE "RepairJob";
|
||||
PRAGMA foreign_keys=on;
|
||||
|
||||
-- DropTable
|
||||
PRAGMA foreign_keys=off;
|
||||
DROP TABLE "RepairJobPart";
|
||||
PRAGMA foreign_keys=on;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "fms" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"hostId" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'OPEN',
|
||||
"problem" TEXT NOT NULL,
|
||||
"openedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"closedAt" DATETIME,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "fms_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "fm_parts" (
|
||||
"fmId" TEXT NOT NULL,
|
||||
"partId" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY ("fmId", "partId"),
|
||||
CONSTRAINT "fm_parts_fmId_fkey" FOREIGN KEY ("fmId") REFERENCES "fms" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "fm_parts_partId_fkey" FOREIGN KEY ("partId") REFERENCES "Part" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "repairs" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"hostId" TEXT NOT NULL,
|
||||
"brokenPartId" TEXT NOT NULL,
|
||||
"replacementPartId" TEXT NOT NULL,
|
||||
"performedById" TEXT NOT NULL,
|
||||
"performedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"fmId" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "repairs_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "repairs_brokenPartId_fkey" FOREIGN KEY ("brokenPartId") REFERENCES "Part" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "repairs_replacementPartId_fkey" FOREIGN KEY ("replacementPartId") REFERENCES "Part" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "repairs_performedById_fkey" FOREIGN KEY ("performedById") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "repairs_fmId_fkey" FOREIGN KEY ("fmId") REFERENCES "fms" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Part" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"serialNumber" TEXT NOT NULL,
|
||||
"partModelId" TEXT NOT NULL,
|
||||
"manufacturerId" TEXT NOT NULL,
|
||||
"price" REAL,
|
||||
"state" TEXT NOT NULL DEFAULT 'SPARE',
|
||||
"binId" TEXT,
|
||||
"categoryId" TEXT,
|
||||
"hostId" TEXT,
|
||||
"custodianId" TEXT,
|
||||
"notes" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "Part_partModelId_fkey" FOREIGN KEY ("partModelId") REFERENCES "PartModel" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "Part_manufacturerId_fkey" FOREIGN KEY ("manufacturerId") REFERENCES "Manufacturer" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "Part_binId_fkey" FOREIGN KEY ("binId") REFERENCES "Bin" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT "Part_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT "Part_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT "Part_custodianId_fkey" FOREIGN KEY ("custodianId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_Part" ("binId", "categoryId", "createdAt", "hostId", "id", "manufacturerId", "notes", "partModelId", "price", "serialNumber", "state", "updatedAt") SELECT "binId", "categoryId", "createdAt", "hostId", "id", "manufacturerId", "notes", "partModelId", "price", "serialNumber", "state", "updatedAt" FROM "Part";
|
||||
DROP TABLE "Part";
|
||||
ALTER TABLE "new_Part" RENAME TO "Part";
|
||||
CREATE UNIQUE INDEX "Part_serialNumber_key" ON "Part"("serialNumber");
|
||||
CREATE INDEX "Part_state_idx" ON "Part"("state");
|
||||
CREATE INDEX "Part_binId_idx" ON "Part"("binId");
|
||||
CREATE INDEX "Part_manufacturerId_idx" ON "Part"("manufacturerId");
|
||||
CREATE INDEX "Part_partModelId_idx" ON "Part"("partModelId");
|
||||
CREATE INDEX "Part_categoryId_idx" ON "Part"("categoryId");
|
||||
CREATE INDEX "Part_hostId_idx" ON "Part"("hostId");
|
||||
CREATE INDEX "Part_custodianId_idx" ON "Part"("custodianId");
|
||||
CREATE TABLE "new_PartModel" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"manufacturerId" TEXT NOT NULL,
|
||||
"mpn" TEXT NOT NULL,
|
||||
"eolDate" DATETIME,
|
||||
"destroyOnFail" BOOLEAN NOT NULL DEFAULT false,
|
||||
"notes" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "PartModel_manufacturerId_fkey" FOREIGN KEY ("manufacturerId") REFERENCES "Manufacturer" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_PartModel" ("createdAt", "eolDate", "id", "manufacturerId", "mpn", "notes", "updatedAt") SELECT "createdAt", "eolDate", "id", "manufacturerId", "mpn", "notes", "updatedAt" FROM "PartModel";
|
||||
DROP TABLE "PartModel";
|
||||
ALTER TABLE "new_PartModel" RENAME TO "PartModel";
|
||||
CREATE INDEX "PartModel_manufacturerId_idx" ON "PartModel"("manufacturerId");
|
||||
CREATE INDEX "PartModel_eolDate_idx" ON "PartModel"("eolDate");
|
||||
CREATE UNIQUE INDEX "PartModel_manufacturerId_mpn_key" ON "PartModel"("manufacturerId", "mpn");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "fms_status_idx" ON "fms"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "fms_hostId_idx" ON "fms"("hostId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "fms_status_openedAt_idx" ON "fms"("status", "openedAt" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "fm_parts_partId_idx" ON "fm_parts"("partId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "repairs_hostId_performedAt_idx" ON "repairs"("hostId", "performedAt" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "repairs_fmId_idx" ON "repairs"("fmId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "repairs_performedById_performedAt_idx" ON "repairs"("performedById", "performedAt" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "repairs_brokenPartId_idx" ON "repairs"("brokenPartId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "repairs_replacementPartId_idx" ON "repairs"("replacementPartId");
|
||||
Reference in New Issue
Block a user