/** * Allocations — the margin ledger. * * This is the table PIG exists for. Everything else is scaffolding around it. * * A `capacity_commitment` is a block of GPU-hours bought from a provider at a * known cost. A `demand_deal` is an agreement to sell compute to a customer at * a known price. An allocation records that some of a specific block was sold * to a specific customer, at a specific price, for a specific window. * * From that single join, everything the business runs on falls out: * * margin = Σ(allocated hours × price) − (committed hours × cost) * utilisation = Σ(allocated hours) ÷ committed hours * idle capacity = committed hours − Σ(allocated hours) * * No generic CRM can compute these, because none of them has a concept of a * cost-bearing commitment sitting behind the pipeline. That is the entire * argument for building this rather than configuring HubSpot. * * Note the deliberate asymmetry in the margin formula: cost is charged against * the FULL commitment, not merely the hours that sold. Unsold hours on a * commitment are already paid for. Charging only the allocated share would * report a healthy margin on a block that is bleeding money, which is precisely * the failure this system is meant to make impossible. */ import { index, integer, numeric, pgTable, text, timestamp, uuid, } from 'drizzle-orm/pg-core'; import { capacityCommitments } from './supply'; import { demandDeals } from './demand'; import { users } from './identity'; export const allocations = pgTable( 'allocations', { id: uuid('id').primaryKey().defaultRandom(), /** The block being drawn from. Restricted: never orphan a cost record. */ capacityCommitmentId: uuid('capacity_commitment_id') .notNull() .references(() => capacityCommitments.id, { onDelete: 'restrict' }), /** * Who it was sold to. Nullable, because internal research consumption is a * real and important allocation with no deal and no revenue behind it. * Leaving research burn out of the ledger overstates available capacity and * understates true cost — the two mistakes this table prevents. */ demandDealId: uuid('demand_deal_id').references(() => demandDeals.id, { onDelete: 'set null', }), /** * Set when the consumer is an internal team rather than a customer. * Mutually exclusive with `demandDealId` in practice; enforced in the * service layer rather than by constraint, because a research allocation * occasionally converts into a customer one and the transition should not * require deleting the row and losing its history. */ internalTeam: text('internal_team'), /** Hours drawn from the block. */ gpuHours: numeric('gpu_hours', { precision: 16, scale: 2 }).notNull(), /** * Sell price in cents per GPU-hour. Zero for internal research * consumption — which is meaningful, not missing: the hours cost real money * and earn none. */ pricePerGpuHourCents: integer('price_per_gpu_hour_cents').notNull().default(0), currency: text('currency').notNull().default('USD'), /** * The window this allocation occupies. Must sit inside the commitment's own * window; capacity cannot be sold before it exists or after it lapses. */ startsAt: timestamp('starts_at', { withTimezone: true }).notNull(), endsAt: timestamp('ends_at', { withTimezone: true }).notNull(), /** * `planned` — pencilled in against a deal that has not closed * `committed` — contractually promised to the customer * `active` — running now * `completed` — finished and billable * `released` — given back; the hours return to available inventory * * Only `committed`, `active` and `completed` consume capacity. `planned` * allocations are shown separately so a seller can see what the pipeline * would do to utilisation without letting forecasts pollute the actuals. */ status: text('status').notNull().default('planned'), /** * A capacity HOLD with an expiry. * * When a seller reserves inventory against a deal that has not closed, * that capacity must disappear from everyone else's availability * immediately — otherwise two sellers promise the same GPUs and one of * them is wrong. Holds expire on a timer so that a stalled deal releases * inventory automatically rather than stranding it indefinitely. * * This single pair of fields is the clearest thing a generic CRM cannot * do: it will happily let you create an opportunity for any amount, and * nothing anywhere checks whether you can deliver it. */ holdExpiresAt: timestamp('hold_expires_at', { withTimezone: true }), /** What else we turned away to keep this hold, in cents. Makes holds honest. */ holdOpportunityCostCents: integer('hold_opportunity_cost_cents'), /** * Priority ladder, borrowed from how ad servers arbitrate guaranteed * against opportunistic demand — the closest published analogue to * arbitrating reserved against spot against internal burn. * * `guaranteed` Contractually reserved; displaces everything below * `committed` Reserved share, but not a fixed block * `on_demand` Priced opportunistically * `preemptible` Spot; yields to anything above it * `internal` Research burn; the first thing displaced * * Lower `priority` integers win. Making this explicit is what lets the * system answer "can I actually sell this?" rather than "is something * technically free?" */ guaranteeType: text('guarantee_type').notNull().default('committed'), priority: integer('priority').notNull().default(100), /** * The compliance gate. An allocation is a specific buyer against specific * capacity in a specific jurisdiction, which is exactly the granularity at * which export control actually applies — see compliance.ts. Null means * unevaluated, which the service layer treats as blocking rather than * permissive for any cross-border match. */ complianceDecisionId: uuid('compliance_decision_id'), createdByUserId: uuid('created_by_user_id').references(() => users.id, { onDelete: 'set null', }), notes: text('notes'), releasedAt: timestamp('released_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }, (t) => [ index('allocations_commitment_idx').on(t.capacityCommitmentId), index('allocations_deal_idx').on(t.demandDealId), index('allocations_status_idx').on(t.status), index('allocations_window_idx').on(t.startsAt, t.endsAt), /** Sweeping expired holds back into available inventory. */ index('allocations_hold_expiry_idx').on(t.holdExpiresAt), ], ); /** * Statuses that actually consume committed capacity. * * Kept here beside the column it describes so the definition cannot drift away * from the schema. Utilisation and idle-capacity figures filter on this; * including `planned` would let optimistic forecasting hide idle hardware. */ export const CONSUMING_ALLOCATION_STATUSES = ['committed', 'active', 'completed'] as const; /** * Statuses that block the capacity from being sold to somebody else. * * Deliberately WIDER than the set that counts toward utilisation and margin. * A live hold must remove inventory from availability — otherwise two sellers * promise the same GPUs — while not yet counting as sold, because it has not * been. Conflating "cannot be offered to anyone else" with "earning revenue" * is how a pipeline of optimistic holds comes to look like a full book. */ export const RESERVING_ALLOCATION_STATUSES = [ 'planned', ...CONSUMING_ALLOCATION_STATUSES, ] as const; export const ALLOCATION_STATUSES = [ 'planned', ...CONSUMING_ALLOCATION_STATUSES, 'released', ] as const; export type AllocationStatus = (typeof ALLOCATION_STATUSES)[number]; export type Allocation = typeof allocations.$inferSelect; export type NewAllocation = typeof allocations.$inferInsert;