import { CONSUMING_ALLOCATION_STATUSES, GPU_SOCKETS, GUARANTEE_TYPES, INTERCONNECT_TYPES, SECURITY_TIERS, } from '@pig/core'; import type { Allocation, CapacityCommitment, Database } from '@pig/db'; import { Hono } from 'hono'; import { z } from 'zod'; import type { ApiEnv } from '../lib/mutation'; import type { MutationDefinition } from '../lib/mutation'; import { MutationError, mutation } from '../lib/mutation'; import { CapacityWriteService, type CapacityWriteTransaction, } from '../services/capacity-writes'; const uuid = z.string().uuid(); const cents = z.number().int().nonnegative(); const percentage = z.number().min(0).max(100); const gpuHours = z.number().positive().refine( (value) => Math.abs(value * 100 - Math.round(value * 100)) < 1e-7, 'GPU-hours may have at most two decimal places.', ); const currency = z.string().regex(/^[A-Z]{3}$/); const shape = z .object({ intervals: z.array(z.string().datetime()).min(2), quantities: z.array(z.number().int().nonnegative()).min(1), }) .strict(); const commitmentFields = { accountId: uuid, siteId: uuid.nullable().optional(), supplyDealId: uuid.nullable().optional(), name: z.string().trim().min(1).max(200), gpuType: z.string().trim().min(1).max(100), socket: z.enum(GPU_SOCKETS).nullable().optional(), gpuCount: z.number().int().positive(), interconnectType: z.enum(INTERCONNECT_TYPES).optional(), securityTier: z.enum(SECURITY_TIERS).optional(), startsAt: z.string().datetime(), endsAt: z.string().datetime(), totalGpuHours: gpuHours, costPerGpuHourCents: cents, currency: currency.optional(), shape: shape.nullable().optional(), colocateWith: z.array(uuid).max(100).optional(), isContiguous: z.boolean().optional(), minimumSpendCents: cents.nullable().optional(), isAutoRenew: z.boolean().optional(), noticeDays: z.number().int().nonnegative().nullable().optional(), takeOrPayFloorPct: percentage.nullable().optional(), prepaidPct: percentage.nullable().optional(), prepaidAmountCents: cents.nullable().optional(), usefulLifeYears: z.number().positive().max(100).nullable().optional(), salvageValuePct: percentage.nullable().optional(), depreciationStartAt: z.string().datetime().nullable().optional(), costOfCapitalBps: z.number().int().nonnegative().nullable().optional(), financingInstrument: z.string().trim().min(1).max(200).nullable().optional(), oversubscriptionPct: z.number().min(0).max(1000).optional(), notes: z.string().max(10_000).nullable().optional(), }; const createCommitmentSchema = z.object(commitmentFields).strict(); const updateCommitmentSchema = z .object({ ...commitmentFields, terminatedAt: z.string().datetime().nullable().optional(), }) .partial() .strict() .refine((input) => Object.keys(input).length > 0, 'At least one change is required.'); const allocationFields = { capacityCommitmentId: uuid, demandDealId: uuid, gpuHours, pricePerGpuHourCents: cents, currency: currency.optional(), startsAt: z.string().datetime(), endsAt: z.string().datetime(), guaranteeType: z.enum(GUARANTEE_TYPES).optional(), priority: z.number().int().nonnegative().optional(), complianceDecisionId: uuid.nullable().optional(), notes: z.string().max(10_000).nullable().optional(), }; const createAllocationSchema = z .object({ ...allocationFields, status: z.enum(CONSUMING_ALLOCATION_STATUSES), }) .strict(); const createHoldSchema = z .object({ ...allocationFields, pricePerGpuHourCents: cents.optional(), holdExpiresAt: z.string().datetime(), holdOpportunityCostCents: cents.nullable().optional(), }) .strict(); const releaseSchema = z .object({ reason: z.string().trim().min(1).max(1_000).optional() }) .strict(); type CapacityWriteOperations = Pick< CapacityWriteService, | 'createCommitment' | 'updateCommitment' | 'createAllocation' | 'createHold' | 'releaseAllocation' >; type ServiceFactory = (tx: CapacityWriteTransaction) => CapacityWriteOperations; const service: ServiceFactory = (tx) => new CapacityWriteService(tx); export function createCommitmentMutationDefinition( makeService: ServiceFactory = service, ): MutationDefinition { return { schema: createCommitmentSchema, permission: { capability: 'commitment:write' as const, team: 'supply' as const }, invalidMessage: 'Invalid capacity commitment.', async mutate({ input, tx, now }) { const result = await makeService(tx).createCommitment(input, now); return { data: result.commitment, activity: { type: 'note' as const, subject: `Created capacity commitment: ${result.commitment.name}`, accountId: result.commitment.accountId, supplyDealId: result.commitment.supplyDealId ?? undefined, meta: { capacityCommitmentId: result.commitment.id }, }, }; }, }; } export function updateCommitmentMutationDefinition( makeService: ServiceFactory = service, ): MutationDefinition { return { schema: updateCommitmentSchema, permission: { capability: 'commitment:write' as const, team: 'supply' as const }, invalidMessage: 'Invalid capacity commitment update.', async mutate({ input, params, tx, now }) { if (!params.id) throw MutationError.notFound('Capacity commitment'); const result = await makeService(tx).updateCommitment(params.id, input, now); return { data: result.commitment, activity: { type: 'note' as const, subject: `Updated capacity commitment: ${result.commitment.name}`, accountId: result.commitment.accountId, supplyDealId: result.commitment.supplyDealId ?? undefined, meta: { capacityCommitmentId: result.commitment.id, changedFields: Object.keys(input), ...(input.terminatedAt !== undefined ? { terminatedAt: result.commitment.terminatedAt?.toISOString() ?? null } : {}), }, }, }; }, }; } export function createAllocationMutationDefinition( makeService: ServiceFactory = service, ): MutationDefinition { return { schema: createAllocationSchema, permission: { capability: 'deal:write' as const, team: 'demand' as const }, invalidMessage: 'Invalid allocation.', async mutate({ input, principal, tx, now }) { const result = await makeService(tx).createAllocation(input, principal, now); return { data: result.allocation, activity: { type: 'note' as const, subject: `Allocated ${input.gpuHours} GPU-hours from ${result.commitment.name}`, accountId: result.deal.accountId, demandDealId: result.deal.id, meta: { allocationId: result.allocation.id, capacityCommitmentId: result.commitment.id, status: result.allocation.status, pricePerGpuHourCents: result.allocation.pricePerGpuHourCents, }, }, }; }, }; } export function createHoldMutationDefinition( makeService: ServiceFactory = service, ): MutationDefinition { return { schema: createHoldSchema, permission: { capability: 'deal:write' as const, team: 'demand' as const }, invalidMessage: 'Invalid capacity hold.', async mutate({ input, principal, tx, now }) { const result = await makeService(tx).createHold(input, principal, now); return { data: result.allocation, activity: { type: 'note' as const, subject: `Held ${input.gpuHours} GPU-hours from ${result.commitment.name}`, accountId: result.deal.accountId, demandDealId: result.deal.id, meta: { allocationId: result.allocation.id, capacityCommitmentId: result.commitment.id, holdExpiresAt: input.holdExpiresAt, }, }, }; }, }; } export function releaseAllocationMutationDefinition( makeService: ServiceFactory = service, ): MutationDefinition { return { schema: releaseSchema, permission: { capability: 'deal:write' as const, team: 'demand' as const }, invalidMessage: 'Invalid allocation release.', async mutate({ input, params, tx, now }) { if (!params.id) throw MutationError.notFound('Allocation'); const result = await makeService(tx).releaseAllocation(params.id, input, now); return { data: result.allocation, activity: { type: 'note' as const, subject: `Released allocation ${result.allocation.id}`, accountId: result.accountId, demandDealId: result.allocation.demandDealId ?? undefined, meta: { allocationId: result.allocation.id, capacityCommitmentId: result.allocation.capacityCommitmentId, from: result.previousStatus, to: 'released', reason: input.reason, }, }, }; }, }; } export function createCapacityWriteRoutes(db: Database): Hono { const routes = new Hono(); routes.post('/api/commitments', mutation(db, createCommitmentMutationDefinition())); routes.patch('/api/commitments/:id', mutation(db, updateCommitmentMutationDefinition())); routes.post('/api/allocations', mutation(db, createAllocationMutationDefinition())); routes.post('/api/allocations/holds', mutation(db, createHoldMutationDefinition())); routes.post( '/api/allocations/:id/release', mutation(db, releaseAllocationMutationDefinition()), ); return routes; }