Files
pig/packages/db/src/schema/allocations.ts
T
2026-08-13 01:39:01 -07:00

174 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 { ALLOCATION_STATUSES, GUARANTEE_TYPES } from '@pig/core';
export {
ALLOCATION_STATUSES,
CONSUMING_ALLOCATION_STATUSES,
RESERVING_ALLOCATION_STATUSES,
} from '@pig/core';
export type { AllocationStatus } from '@pig/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', { enum: ALLOCATION_STATUSES }).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', { enum: GUARANTEE_TYPES })
.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),
],
);
export type Allocation = typeof allocations.$inferSelect;
export type NewAllocation = typeof allocations.$inferInsert;