diff --git a/packages/iios-service/prisma/migrations/20260701080858_adapter_init/migration.sql b/packages/iios-service/prisma/migrations/20260701080858_adapter_init/migration.sql new file mode 100644 index 0000000..d06657b --- /dev/null +++ b/packages/iios-service/prisma/migrations/20260701080858_adapter_init/migration.sql @@ -0,0 +1,72 @@ +-- CreateTable +CREATE TABLE "IiosInboundRawEvent" ( + "id" TEXT NOT NULL, + "channelType" TEXT NOT NULL, + "externalEventId" TEXT, + "signatureStatus" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'RECEIVED', + "headers" JSONB, + "payload" JSONB NOT NULL, + "traceId" TEXT, + "interactionId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "IiosInboundRawEvent_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "IiosOutboundCommand" ( + "id" TEXT NOT NULL, + "channelType" TEXT NOT NULL, + "target" TEXT NOT NULL, + "payload" JSONB NOT NULL, + "status" TEXT NOT NULL DEFAULT 'PENDING', + "idempotencyKey" TEXT NOT NULL, + "providerRef" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "IiosOutboundCommand_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "IiosDeliveryAttempt" ( + "id" TEXT NOT NULL, + "commandId" TEXT NOT NULL, + "attemptNo" INTEGER NOT NULL, + "status" TEXT NOT NULL, + "providerRef" TEXT, + "latencyMs" INTEGER, + "errorCode" TEXT, + "at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "IiosDeliveryAttempt_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "IiosRateLimitBucket" ( + "channelType" TEXT NOT NULL, + "bucketKey" TEXT NOT NULL, + "windowStart" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "count" INTEGER NOT NULL DEFAULT 0, + "resetAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "IiosRateLimitBucket_pkey" PRIMARY KEY ("channelType","bucketKey") +); + +-- CreateIndex +CREATE INDEX "IiosInboundRawEvent_status_idx" ON "IiosInboundRawEvent"("status"); + +-- CreateIndex +CREATE UNIQUE INDEX "IiosInboundRawEvent_channelType_externalEventId_key" ON "IiosInboundRawEvent"("channelType", "externalEventId"); + +-- CreateIndex +CREATE UNIQUE INDEX "IiosOutboundCommand_idempotencyKey_key" ON "IiosOutboundCommand"("idempotencyKey"); + +-- CreateIndex +CREATE INDEX "IiosOutboundCommand_channelType_status_idx" ON "IiosOutboundCommand"("channelType", "status"); + +-- CreateIndex +CREATE INDEX "IiosDeliveryAttempt_commandId_idx" ON "IiosDeliveryAttempt"("commandId"); + +-- AddForeignKey +ALTER TABLE "IiosDeliveryAttempt" ADD CONSTRAINT "IiosDeliveryAttempt_commandId_fkey" FOREIGN KEY ("commandId") REFERENCES "IiosOutboundCommand"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/iios-service/prisma/schema.prisma b/packages/iios-service/prisma/schema.prisma index bfb4a58..cebc88a 100644 --- a/packages/iios-service/prisma/schema.prisma +++ b/packages/iios-service/prisma/schema.prisma @@ -517,3 +517,63 @@ model IiosCallbackRequest { @@index([scopeId, status]) } + +// ─── P5: Adapters (anti-corruption layer, simulation mode) ──────── + +/// Raw inbound provider payload (claim-check / replay). Verified before storing. +model IiosInboundRawEvent { + id String @id @default(cuid()) + channelType String + externalEventId String? + signatureStatus String // VERIFIED | INVALID | UNSIGNED + status String @default("RECEIVED") // RECEIVED | NORMALIZED | REJECTED | FAILED + headers Json? + payload Json + traceId String? + interactionId String? + createdAt DateTime @default(now()) + + @@unique([channelType, externalEventId]) + @@index([status]) +} + +/// A signed command to send on an external channel — executed by the sandbox sink. +model IiosOutboundCommand { + id String @id @default(cuid()) + channelType String + target String + payload Json + status String @default("PENDING") // PENDING | SENT | FAILED | RATE_LIMITED + idempotencyKey String @unique + providerRef String? + createdAt DateTime @default(now()) + + attempts IiosDeliveryAttempt[] + + @@index([channelType, status]) +} + +model IiosDeliveryAttempt { + id String @id @default(cuid()) + commandId String + command IiosOutboundCommand @relation(fields: [commandId], references: [id], onDelete: Cascade) + attemptNo Int + status String + providerRef String? + latencyMs Int? + errorCode String? + at DateTime @default(now()) + + @@index([commandId]) +} + +/// Per-(channelType, key) token/window bucket for outbound throttling. +model IiosRateLimitBucket { + channelType String + bucketKey String + windowStart DateTime @default(now()) + count Int @default(0) + resetAt DateTime + + @@id([channelType, bucketKey]) +} diff --git a/packages/iios-service/src/test-utils/reset-db.ts b/packages/iios-service/src/test-utils/reset-db.ts index 385af04..e51045e 100644 --- a/packages/iios-service/src/test-utils/reset-db.ts +++ b/packages/iios-service/src/test-utils/reset-db.ts @@ -8,6 +8,9 @@ import type { PrismaClient } from '@prisma/client'; export async function resetDb(prisma: PrismaClient): Promise { await prisma.$executeRawUnsafe( `TRUNCATE TABLE + "IiosInboundRawEvent","IiosDeliveryAttempt","IiosOutboundCommand","IiosRateLimitBucket", + "IiosTicketStateHistory","IiosTicketThreadLink","IiosCallbackRequest","IiosTicket", + "IiosSupportTeamMember","IiosSupportQueue", "IiosInboxItemStateHistory","IiosInboxItem","IiosUnreadCounter","IiosMessageReceipt", "IiosOutboxEvent","IiosProcessedEvent","IiosMessagePart","IiosInteraction", "IiosThreadParticipant","IiosThread","IiosActorRef","IiosSourceHandle","IiosChannel","IiosScope"