Phase F
Some checks failed
CI / Node — lint, typecheck, test, build (push) Failing after 10s
CI / Go — node-agent tests (push) Failing after 8s
CI / Go — edge-gateway build (push) Successful in 18s

This commit is contained in:
smueller
2026-06-26 15:05:01 +02:00
parent 333ad1cc7d
commit 15cf302afc
23 changed files with 2116 additions and 8 deletions

View File

@@ -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';

View File

@@ -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

View File

@@ -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;

View File

@@ -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")
}

View File

@@ -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