feat: implement problem resolution (009)

Populates the five real problem-management stubs (investigation,
root-causes, solutions, verification, resolutions -- problems is
confirmed dead/unwired scaffold and stays untouched) with doc04's
sequential workflow engine:

- investigation: version-row-per-attempt (never overwritten), with a
  customer-safe read path that always strips internalNotes.
- root-causes/solutions/verification: a strict existence chain
  (investigation -> root cause -> solution -> approval -> implementation
  -> verification), each step resolve-or-409 on its own precondition,
  matching doc06's schema field-for-field with no invented columns.
- resolutions: gated on a successfully verified solution (no stored
  solutionId FK, per doc06 -- resolved via a join at write time), moving
  the ticket to RESOLUTION_PENDING_CUSTOMER; explicit customer
  confirmation and a durable auto-close sweep (the previously-unregistered
  CLEANUP queue stub, mirroring 008's breach-detection job) both resolve
  it from there.
- reopen (ticketing/tickets): two real, separately-audited transitions
  (RESOLVED|CLOSED -> REOPENED -> IN_PROGRESS), touching no prior
  problem-resolution record and no SLARun -- closes the loop 008's own
  spec.md left open.

Verification-failure escalation reuses 003/007's existing
HUMAN_ESCALATION transition directly rather than adding an eleventh
trigger type to 008's already-shipped escalation rules.

Customer-facing confirm-resolution/reopen needed a body-shape variant of
002's inbound trust boundary that didn't previously exist:
fastify.authenticateProductIntegration hard-required a full
ticket-creation-shaped body. Extracted the shared token/scope/replay
verification into verifyIntegrationIdentity and added a narrower
authenticateProductIntegrationIdentity decorator + identityOnlyRequestSchema
on top of it -- purely additive, ticket creation's own behavior is
unchanged.

Also fixes a real test-data-hygiene bug surfaced by running this
feature's suite alongside 008's: a wildcard-scoped HierarchyNode and an
intentionally-global SLAPolicy in 008's own test fixtures were silently
affecting other test files' tickets sharing the same live Postgres.

Verified against throwaway Docker Postgres/Redis: typecheck, lint,
architecture-check all clean; full regression (tests/unit +
tests/integration together, 172 tests) passes except the 2 pre-existing
MinIO-dependent attachment failures, unrelated to this feature.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
saqib mir
2026-09-03 15:09:29 +05:30
co-authored by Claude Sonnet 5
parent aaa51ef475
commit 16daf8d32d
94 changed files with 2080 additions and 245 deletions
@@ -0,0 +1,105 @@
-- CreateTable
CREATE TABLE "investigations" (
"id" TEXT NOT NULL,
"problemId" TEXT NOT NULL,
"investigator" TEXT NOT NULL,
"findings" JSONB NOT NULL,
"evidence" JSONB,
"internalNotes" TEXT,
"status" TEXT NOT NULL DEFAULT 'open',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "investigations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "root_causes" (
"id" TEXT NOT NULL,
"problemId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"description" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "root_causes_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "solutions" (
"id" TEXT NOT NULL,
"problemId" TEXT NOT NULL,
"proposed" TEXT NOT NULL,
"approved" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "solutions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "solution_implementations" (
"id" TEXT NOT NULL,
"solutionId" TEXT NOT NULL,
"notes" TEXT,
"implementedBy" TEXT NOT NULL,
"implementedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "solution_implementations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "solution_verifications" (
"id" TEXT NOT NULL,
"solutionId" TEXT NOT NULL,
"method" TEXT NOT NULL,
"result" TEXT NOT NULL,
"evidence" JSONB,
"verifiedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "solution_verifications_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "resolutions" (
"id" TEXT NOT NULL,
"ticketId" TEXT NOT NULL,
"outcome" TEXT NOT NULL,
"resolvedBy" TEXT NOT NULL,
"resolvedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "resolutions_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "investigations_problemId_createdAt_idx" ON "investigations"("problemId", "createdAt");
-- CreateIndex
CREATE INDEX "root_causes_problemId_createdAt_idx" ON "root_causes"("problemId", "createdAt");
-- CreateIndex
CREATE INDEX "solutions_problemId_createdAt_idx" ON "solutions"("problemId", "createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "solution_implementations_solutionId_key" ON "solution_implementations"("solutionId");
-- CreateIndex
CREATE UNIQUE INDEX "solution_verifications_solutionId_key" ON "solution_verifications"("solutionId");
-- CreateIndex
CREATE UNIQUE INDEX "resolutions_ticketId_key" ON "resolutions"("ticketId");
-- AddForeignKey
ALTER TABLE "investigations" ADD CONSTRAINT "investigations_problemId_fkey" FOREIGN KEY ("problemId") REFERENCES "problems"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "root_causes" ADD CONSTRAINT "root_causes_problemId_fkey" FOREIGN KEY ("problemId") REFERENCES "problems"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "solutions" ADD CONSTRAINT "solutions_problemId_fkey" FOREIGN KEY ("problemId") REFERENCES "problems"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "solution_implementations" ADD CONSTRAINT "solution_implementations_solutionId_fkey" FOREIGN KEY ("solutionId") REFERENCES "solutions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "solution_verifications" ADD CONSTRAINT "solution_verifications_solutionId_fkey" FOREIGN KEY ("solutionId") REFERENCES "solutions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "resolutions" ADD CONSTRAINT "resolutions_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+83
View File
@@ -117,6 +117,10 @@ model Problem {
category Category? @relation(fields: [categoryId], references: [id])
tickets Ticket[]
investigations Investigation[]
rootCauses RootCause[]
solutions Solution[]
@@map("problems")
}
@@ -150,6 +154,7 @@ model Ticket {
assignmentHistory AssignmentHistory[]
slaRun SLARun?
escalationEvents EscalationEvent[]
resolution Resolution?
@@unique([productId, idempotencyKey])
@@index([productId, status])
@@ -637,3 +642,81 @@ model EscalationEvent {
@@index([ticketId, createdAt])
@@map("escalation_events")
}
model Investigation {
id String @id @default(cuid())
problemId String
problem Problem @relation(fields: [problemId], references: [id])
investigator String
findings Json
evidence Json?
internalNotes String? // never exposed on a customer-facing read — see
// specs/009-problem-resolution/spec.md FR-003
status String @default("open") // open | complete
createdAt DateTime @default(now())
@@index([problemId, createdAt])
@@map("investigations")
}
model RootCause {
id String @id @default(cuid())
problemId String
problem Problem @relation(fields: [problemId], references: [id])
type String // technical | configuration | external_dependency | business |
// contributing_factor
description String
createdAt DateTime @default(now())
@@index([problemId, createdAt])
@@map("root_causes")
}
model Solution {
id String @id @default(cuid())
problemId String
problem Problem @relation(fields: [problemId], references: [id])
proposed String
approved Boolean @default(false)
createdAt DateTime @default(now())
implementation SolutionImplementation?
verification SolutionVerification?
@@index([problemId, createdAt])
@@map("solutions")
}
model SolutionImplementation {
id String @id @default(cuid())
solutionId String @unique
solution Solution @relation(fields: [solutionId], references: [id])
notes String?
implementedBy String
implementedAt DateTime @default(now())
@@map("solution_implementations")
}
model SolutionVerification {
id String @id @default(cuid())
solutionId String @unique
solution Solution @relation(fields: [solutionId], references: [id])
method String // automated | technical_test | customer_confirmation | agent_confirmation
result String // success | failed
evidence Json?
verifiedAt DateTime @default(now())
@@map("solution_verifications")
}
model Resolution {
id String @id @default(cuid())
ticketId String @unique
ticket Ticket @relation(fields: [ticketId], references: [id])
outcome String
resolvedBy String // "ai" | agentId — see specs/009-problem-resolution/data-model.md
resolvedAt DateTime @default(now())
@@map("resolutions")
}