Phase F
This commit is contained in:
@@ -243,4 +243,23 @@ export {
|
||||
type WhmcsCreateSsoResponse,
|
||||
type SsoConsumeRequest,
|
||||
type SsoConsumeResponse,
|
||||
whmcsReconcileRequestSchema,
|
||||
whmcsReconcileResponseSchema,
|
||||
whmcsListAccountsResponseSchema,
|
||||
whmcsImportServiceRequestSchema,
|
||||
whmcsEventsResponseSchema,
|
||||
whmcsEventsAckRequestSchema,
|
||||
whmcsEventsAckResponseSchema,
|
||||
whmcsUsageQuerySchema,
|
||||
whmcsUsageResponseSchema,
|
||||
whmcsUsageAdjustmentRequestSchema,
|
||||
type WhmcsReconcileRequest,
|
||||
type WhmcsReconcileResponse,
|
||||
type WhmcsListAccountsResponse,
|
||||
type WhmcsImportServiceRequest,
|
||||
type WhmcsEventsResponse,
|
||||
type WhmcsEventsAckRequest,
|
||||
type WhmcsEventsAckResponse,
|
||||
type WhmcsUsageResponse,
|
||||
type WhmcsUsageAdjustmentRequest,
|
||||
} from './whmcs';
|
||||
|
||||
@@ -108,3 +108,163 @@ export type WhmcsCreateSsoRequest = z.infer<typeof whmcsCreateSsoRequestSchema>;
|
||||
export type WhmcsCreateSsoResponse = z.infer<typeof whmcsCreateSsoResponseSchema>;
|
||||
export type SsoConsumeRequest = z.infer<typeof ssoConsumeRequestSchema>;
|
||||
export type SsoConsumeResponse = z.infer<typeof ssoConsumeResponseSchema>;
|
||||
|
||||
export const whmcsExpectedStatusSchema = z.enum([
|
||||
'Pending',
|
||||
'Active',
|
||||
'Suspended',
|
||||
'Terminated',
|
||||
'Cancelled',
|
||||
]);
|
||||
|
||||
export const whmcsReconcileRequestSchema = z.object({
|
||||
expectedWhmcsStatus: whmcsExpectedStatusSchema.optional(),
|
||||
expectedPlanSlug: z.string().max(64).optional(),
|
||||
expectedRamMb: z.number().int().min(512).max(65536).optional(),
|
||||
dryRun: z.boolean().default(false),
|
||||
autoRepair: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const reconciliationSeveritySchema = z.enum([
|
||||
'INFO',
|
||||
'WARNING',
|
||||
'BLOCKING',
|
||||
'DESTRUCTIVE',
|
||||
'SECURITY',
|
||||
]);
|
||||
|
||||
export const reconciliationIssueSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
code: z.string(),
|
||||
severity: reconciliationSeveritySchema,
|
||||
message: z.string(),
|
||||
externalServiceId: z.string().nullable(),
|
||||
serverId: z.string().uuid().nullable(),
|
||||
details: z.record(z.unknown()).optional(),
|
||||
autoRepaired: z.boolean(),
|
||||
});
|
||||
|
||||
export const whmcsReconcileResponseSchema = z.object({
|
||||
runId: z.string().uuid(),
|
||||
dryRun: z.boolean(),
|
||||
issues: z.array(reconciliationIssueSchema),
|
||||
repaired: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const whmcsListAccountsResponseSchema = z.object({
|
||||
accounts: z.array(
|
||||
z.object({
|
||||
externalServiceId: z.string(),
|
||||
externalClientId: z.string(),
|
||||
serverId: z.string().uuid(),
|
||||
domain: z.string(),
|
||||
username: z.string().nullable(),
|
||||
status: z.enum(['Active', 'Suspended', 'Terminated', 'Pending']),
|
||||
planSlug: z.string(),
|
||||
ramMb: z.number().int(),
|
||||
gameCloudStatus: z.string(),
|
||||
linkStatus: z.enum(['PENDING', 'ACTIVE', 'SUSPENDED', 'TERMINATED']),
|
||||
lastReconciledAt: z.string().datetime().nullable(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export const whmcsImportServiceRequestSchema = z.object({
|
||||
externalServiceId: z.string().min(1).max(64),
|
||||
externalClientId: z.string().min(1).max(64),
|
||||
serverId: z.string().uuid(),
|
||||
externalProductId: z.string().max(64).optional(),
|
||||
expectedPlanSlug: z.string().max(64).optional(),
|
||||
});
|
||||
|
||||
export const integrationEventSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
eventType: z.string(),
|
||||
payload: z.record(z.unknown()),
|
||||
status: z.enum(['PENDING', 'ACKNOWLEDGED', 'DEAD_LETTER']),
|
||||
createdAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const whmcsEventsResponseSchema = z.object({
|
||||
events: z.array(integrationEventSchema),
|
||||
nextCursor: z.string().uuid().nullable(),
|
||||
});
|
||||
|
||||
export const whmcsEventsAckRequestSchema = z.object({
|
||||
eventIds: z.array(z.string().uuid()).min(1).max(100),
|
||||
deadLetter: z.boolean().default(false),
|
||||
deadLetterReason: z.string().max(512).optional(),
|
||||
});
|
||||
|
||||
export const whmcsEventsAckResponseSchema = z.object({
|
||||
acknowledged: z.number().int(),
|
||||
});
|
||||
|
||||
export const whmcsUsageMetricsSchema = z.object({
|
||||
runtime_minutes: z.number(),
|
||||
memory_gib_minutes: z.number(),
|
||||
cpu_core_minutes: z.number(),
|
||||
storage_gib_days: z.number(),
|
||||
backup_storage_gib_days: z.number(),
|
||||
egress_gib: z.number(),
|
||||
extra_port_days: z.number(),
|
||||
priority_queue_minutes: z.number(),
|
||||
managed_service_units: z.number(),
|
||||
});
|
||||
|
||||
export const whmcsUsageUpdateSchema = z.object({
|
||||
diskUsageGiB: z.number(),
|
||||
bandwidthGiB: z.number(),
|
||||
});
|
||||
|
||||
export const whmcsUsageQuerySchema = z.object({
|
||||
periodStart: z.string().datetime().optional(),
|
||||
periodEnd: z.string().datetime().optional(),
|
||||
testMode: z.coerce.boolean().default(false),
|
||||
});
|
||||
|
||||
export const whmcsUsageResponseSchema = z.object({
|
||||
exportId: z.string().uuid(),
|
||||
externalServiceId: z.string(),
|
||||
serverId: z.string().uuid(),
|
||||
periodStart: z.string().datetime(),
|
||||
periodEnd: z.string().datetime(),
|
||||
metrics: whmcsUsageMetricsSchema,
|
||||
usageUpdate: whmcsUsageUpdateSchema,
|
||||
contentHash: z.string(),
|
||||
status: z.enum(['DRAFT', 'EXPORTED', 'ADJUSTED']),
|
||||
isTestMode: z.boolean(),
|
||||
adjustments: z.array(
|
||||
z.object({
|
||||
metricKey: z.string(),
|
||||
delta: z.number(),
|
||||
reason: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export const whmcsUsageAdjustmentRequestSchema = z.object({
|
||||
periodStart: z.string().datetime(),
|
||||
periodEnd: z.string().datetime(),
|
||||
adjustments: z
|
||||
.array(
|
||||
z.object({
|
||||
metricKey: z.string().min(1).max(64),
|
||||
delta: z.number(),
|
||||
reason: z.string().min(1).max(256),
|
||||
}),
|
||||
)
|
||||
.min(1)
|
||||
.max(20),
|
||||
testMode: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type WhmcsReconcileRequest = z.infer<typeof whmcsReconcileRequestSchema>;
|
||||
export type WhmcsReconcileResponse = z.infer<typeof whmcsReconcileResponseSchema>;
|
||||
export type WhmcsListAccountsResponse = z.infer<typeof whmcsListAccountsResponseSchema>;
|
||||
export type WhmcsImportServiceRequest = z.infer<typeof whmcsImportServiceRequestSchema>;
|
||||
export type WhmcsEventsResponse = z.infer<typeof whmcsEventsResponseSchema>;
|
||||
export type WhmcsEventsAckRequest = z.infer<typeof whmcsEventsAckRequestSchema>;
|
||||
export type WhmcsEventsAckResponse = z.infer<typeof whmcsEventsAckResponseSchema>;
|
||||
export type WhmcsUsageResponse = z.infer<typeof whmcsUsageResponseSchema>;
|
||||
export type WhmcsUsageAdjustmentRequest = z.infer<typeof whmcsUsageAdjustmentRequestSchema>;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,92 @@
|
||||
-- Phase E/F: WHMCS reconciliation, events, usage exports
|
||||
|
||||
CREATE TYPE "IntegrationEventStatus" AS ENUM ('PENDING', 'ACKNOWLEDGED', 'DEAD_LETTER');
|
||||
CREATE TYPE "ReconciliationSeverity" AS ENUM ('INFO', 'WARNING', 'BLOCKING', 'DESTRUCTIVE', 'SECURITY');
|
||||
CREATE TYPE "UsageExportStatus" AS ENUM ('DRAFT', 'EXPORTED', 'ADJUSTED');
|
||||
|
||||
ALTER TABLE "whmcs_service_links"
|
||||
ADD COLUMN "lastReconciledAt" TIMESTAMPTZ(3);
|
||||
|
||||
CREATE TABLE "integration_events" (
|
||||
"id" TEXT NOT NULL,
|
||||
"installationId" TEXT NOT NULL,
|
||||
"eventType" TEXT NOT NULL,
|
||||
"payload" JSONB NOT NULL,
|
||||
"dedupeKey" TEXT NOT NULL,
|
||||
"status" "IntegrationEventStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"acknowledgedAt" TIMESTAMPTZ(3),
|
||||
"deadLetteredAt" TIMESTAMPTZ(3),
|
||||
"deadLetterReason" TEXT,
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "integration_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "integration_events_dedupeKey_key" ON "integration_events"("dedupeKey");
|
||||
CREATE INDEX "integration_events_installationId_status_createdAt_idx" ON "integration_events"("installationId", "status", "createdAt");
|
||||
CREATE INDEX "integration_events_installationId_createdAt_idx" ON "integration_events"("installationId", "createdAt");
|
||||
|
||||
ALTER TABLE "integration_events"
|
||||
ADD CONSTRAINT "integration_events_installationId_fkey"
|
||||
FOREIGN KEY ("installationId") REFERENCES "whmcs_installations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
CREATE TABLE "integration_sync_cursors" (
|
||||
"id" TEXT NOT NULL,
|
||||
"installationId" TEXT NOT NULL,
|
||||
"eventsCursor" TEXT,
|
||||
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
|
||||
CONSTRAINT "integration_sync_cursors_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "integration_sync_cursors_installationId_key" ON "integration_sync_cursors"("installationId");
|
||||
|
||||
ALTER TABLE "integration_sync_cursors"
|
||||
ADD CONSTRAINT "integration_sync_cursors_installationId_fkey"
|
||||
FOREIGN KEY ("installationId") REFERENCES "whmcs_installations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
CREATE TABLE "reconciliation_issues" (
|
||||
"id" TEXT NOT NULL,
|
||||
"installationId" TEXT NOT NULL,
|
||||
"runId" TEXT NOT NULL,
|
||||
"externalServiceId" TEXT,
|
||||
"serverId" TEXT,
|
||||
"code" TEXT NOT NULL,
|
||||
"severity" "ReconciliationSeverity" NOT NULL,
|
||||
"message" TEXT NOT NULL,
|
||||
"details" JSONB,
|
||||
"autoRepaired" BOOLEAN NOT NULL DEFAULT false,
|
||||
"resolvedAt" TIMESTAMPTZ(3),
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "reconciliation_issues_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "reconciliation_issues_installationId_runId_idx" ON "reconciliation_issues"("installationId", "runId");
|
||||
CREATE INDEX "reconciliation_issues_installationId_resolvedAt_idx" ON "reconciliation_issues"("installationId", "resolvedAt");
|
||||
|
||||
ALTER TABLE "reconciliation_issues"
|
||||
ADD CONSTRAINT "reconciliation_issues_installationId_fkey"
|
||||
FOREIGN KEY ("installationId") REFERENCES "whmcs_installations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
CREATE TABLE "whmcs_usage_exports" (
|
||||
"id" TEXT NOT NULL,
|
||||
"installationId" TEXT NOT NULL,
|
||||
"externalServiceId" TEXT NOT NULL,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"periodStart" TIMESTAMPTZ(3) NOT NULL,
|
||||
"periodEnd" TIMESTAMPTZ(3) NOT NULL,
|
||||
"metrics" JSONB NOT NULL,
|
||||
"contentHash" TEXT NOT NULL,
|
||||
"status" "UsageExportStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"isTestMode" BOOLEAN NOT NULL DEFAULT false,
|
||||
"adjustmentOfId" TEXT,
|
||||
"exportedAt" TIMESTAMPTZ(3),
|
||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "whmcs_usage_exports_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "whmcs_usage_exports_installationId_externalServiceId_periodStart_periodEnd_isTestMode_key"
|
||||
ON "whmcs_usage_exports"("installationId", "externalServiceId", "periodStart", "periodEnd", "isTestMode");
|
||||
CREATE INDEX "whmcs_usage_exports_installationId_periodEnd_idx" ON "whmcs_usage_exports"("installationId", "periodEnd");
|
||||
|
||||
ALTER TABLE "whmcs_usage_exports"
|
||||
ADD CONSTRAINT "whmcs_usage_exports_installationId_fkey"
|
||||
FOREIGN KEY ("installationId") REFERENCES "whmcs_installations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -152,6 +152,26 @@ enum SsoTicketKind {
|
||||
ADMIN
|
||||
}
|
||||
|
||||
enum IntegrationEventStatus {
|
||||
PENDING
|
||||
ACKNOWLEDGED
|
||||
DEAD_LETTER
|
||||
}
|
||||
|
||||
enum ReconciliationSeverity {
|
||||
INFO
|
||||
WARNING
|
||||
BLOCKING
|
||||
DESTRUCTIVE
|
||||
SECURITY
|
||||
}
|
||||
|
||||
enum UsageExportStatus {
|
||||
DRAFT
|
||||
EXPORTED
|
||||
ADJUSTED
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
@@ -724,6 +744,10 @@ model WhmcsInstallation {
|
||||
serviceLinks WhmcsServiceLink[]
|
||||
operations IntegrationOperation[]
|
||||
ssoTickets SsoTicket[]
|
||||
events IntegrationEvent[]
|
||||
syncCursor IntegrationSyncCursor?
|
||||
reconciliationIssues ReconciliationIssue[]
|
||||
usageExports WhmcsUsageExport[]
|
||||
|
||||
@@map("whmcs_installations")
|
||||
}
|
||||
@@ -752,6 +776,7 @@ model WhmcsServiceLink {
|
||||
status WhmcsServiceStatus @default(PENDING)
|
||||
suspendedAt DateTime? @db.Timestamptz(3)
|
||||
terminatedAt DateTime? @db.Timestamptz(3)
|
||||
lastReconciledAt DateTime? @db.Timestamptz(3)
|
||||
metadata Json?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
@@ -807,3 +832,76 @@ model SsoTicket {
|
||||
@@index([installationId, createdAt])
|
||||
@@map("sso_tickets")
|
||||
}
|
||||
|
||||
model IntegrationEvent {
|
||||
id String @id @default(uuid())
|
||||
installationId String
|
||||
eventType String
|
||||
payload Json
|
||||
dedupeKey String @unique
|
||||
status IntegrationEventStatus @default(PENDING)
|
||||
acknowledgedAt DateTime? @db.Timestamptz(3)
|
||||
deadLetteredAt DateTime? @db.Timestamptz(3)
|
||||
deadLetterReason String?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
installation WhmcsInstallation @relation(fields: [installationId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([installationId, status, createdAt])
|
||||
@@index([installationId, createdAt])
|
||||
@@map("integration_events")
|
||||
}
|
||||
|
||||
model IntegrationSyncCursor {
|
||||
id String @id @default(uuid())
|
||||
installationId String @unique
|
||||
eventsCursor String?
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
installation WhmcsInstallation @relation(fields: [installationId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("integration_sync_cursors")
|
||||
}
|
||||
|
||||
model ReconciliationIssue {
|
||||
id String @id @default(uuid())
|
||||
installationId String
|
||||
runId String
|
||||
externalServiceId String?
|
||||
serverId String?
|
||||
code String
|
||||
severity ReconciliationSeverity
|
||||
message String
|
||||
details Json?
|
||||
autoRepaired Boolean @default(false)
|
||||
resolvedAt DateTime? @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
installation WhmcsInstallation @relation(fields: [installationId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([installationId, runId])
|
||||
@@index([installationId, resolvedAt])
|
||||
@@map("reconciliation_issues")
|
||||
}
|
||||
|
||||
model WhmcsUsageExport {
|
||||
id String @id @default(uuid())
|
||||
installationId String
|
||||
externalServiceId String
|
||||
serverId String
|
||||
periodStart DateTime @db.Timestamptz(3)
|
||||
periodEnd DateTime @db.Timestamptz(3)
|
||||
metrics Json
|
||||
contentHash String
|
||||
status UsageExportStatus @default(DRAFT)
|
||||
isTestMode Boolean @default(false)
|
||||
adjustmentOfId String?
|
||||
exportedAt DateTime? @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
installation WhmcsInstallation @relation(fields: [installationId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([installationId, externalServiceId, periodStart, periodEnd, isTestMode])
|
||||
@@index([installationId, periodEnd])
|
||||
@@map("whmcs_usage_exports")
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ export type {
|
||||
WhmcsServiceLink,
|
||||
IntegrationOperation,
|
||||
SsoTicket,
|
||||
IntegrationEvent,
|
||||
IntegrationSyncCursor,
|
||||
ReconciliationIssue,
|
||||
WhmcsUsageExport,
|
||||
Plan,
|
||||
FeatureFlag,
|
||||
SystemSetting,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user