Files
pig/packages/db/src/schema/demand.ts
T
karti d36762f264 Scaffold PIG and model the compute-GTM ontology
PIG is an agent-native CRM for two-sided AI-compute companies: businesses
that buy GPU capacity from providers and resell it. Their business is the
spread between two pipelines, which is precisely what a generic CRM cannot
represent.

The load-bearing decision is the `allocations` table, joining a
capacity_commitment (what we bought, at a known cost) to a demand_deal
(what we sold, at a known price). Margin, utilisation and idle capacity all
fall out of that one join. Cost is charged against the full commitment
rather than only the hours that sold, because unsold hours are already paid
for and any other treatment flatters a block that is losing money.

Domain decisions worth noting, each grounded in how this market operates:

- Demand stages put `legal` second, not last. Customers do not hand
  workloads to an infrastructure provider before paper is executed.
- Supply qualification splits technical from financial diligence, recorded
  attributably. Accepting capacity is a two-key decision.
- Capacity carries a time SHAPE (intervals + quantities), not a window.
  Commitments ramp and step down; a rectangle reports availability that
  does not exist in the month someone wants it.
- SLAs model three distinct shapes: none, a reliability tier plus credits
  policy, and a negotiated agreement. Aggregators generally cannot promise
  uptime on resold capacity, but negotiate heavyweight paper upstream.
  Remedies include fee abatement, which is materially better than a capped
  credit and is not expressible as one.
- Export control is a predicate on the allocation edge, evaluated against
  the ULTIMATE parent's jurisdiction. Country of incorporation is not a
  valid key, so this cannot live as a flag on an account.
- Agent-derived claims land in `facts` with a confidence band and evidence.
  Only verified claims self-apply; weaker ones await review.
- The API never calls the agent. It writes to a leased queue, guarded by a
  partial unique index on unfinished work.

Verified: typechecks clean, migration generates and applies to Postgres 16
(31 tables, 24 enums, 117 indexes).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 18:41:41 -07:00

178 lines
6.9 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.
/**
* The demand side: what customers want, and what we have sold them.
*
* The stage set follows how enterprise AI-infrastructure deals are publicly
* described as progressing, with `legal` deliberately second — see the note on
* DEMAND_STAGES in @pig/core. A `capacityRequest` is modelled separately from
* the deal because the technical shape of what a customer needs is negotiated
* on a different clock from the commercials, and because it is the thing that
* gets matched against inventory.
*/
import {
boolean,
index,
integer,
jsonb,
numeric,
pgTable,
text,
timestamp,
uuid,
} from 'drizzle-orm/pg-core';
import {
demandStageEnum,
interconnectTypeEnum,
productLineEnum,
securityTierEnum,
} from './enums';
import { accounts, contacts } from './crm';
import { users } from './identity';
export const demandDeals = pgTable(
'demand_deals',
{
id: uuid('id').primaryKey().defaultRandom(),
accountId: uuid('account_id')
.notNull()
.references(() => accounts.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
description: text('description'),
/**
* One account routinely carries several independent opportunities across
* product lines. Collapsing everything into "GPU hours" would make
* land-and-expand — the actual motion in this market — invisible.
*/
productLine: productLineEnum('product_line').notNull().default('compute_reserved'),
stage: demandStageEnum('stage').notNull().default('qualification'),
stageChangedAt: timestamp('stage_changed_at', { withTimezone: true }).notNull().defaultNow(),
ownerUserId: uuid('owner_user_id').references(() => users.id, { onDelete: 'set null' }),
primaryContactId: uuid('primary_contact_id').references(() => contacts.id, {
onDelete: 'set null',
}),
/** Annual contract value in cents. */
acvCents: integer('acv_cents'),
/** Total contract value across the whole term, in cents. */
tcvCents: integer('tcv_cents'),
currency: text('currency').notNull().default('USD'),
termMonths: integer('term_months'),
/** Forecast confidence, 01. Distinct from stage: late deals still die. */
probability: numeric('probability', { precision: 4, scale: 3 }),
expectedCloseDate: timestamp('expected_close_date', { withTimezone: true }),
closedAt: timestamp('closed_at', { withTimezone: true }),
closedReason: text('closed_reason'),
/**
* Whether legal is cleared. Tracked as a flag as well as a stage, because a
* deal can advance past `legal` into scoping while an amendment is still
* outstanding, and shipping capacity without executed paper is the mistake
* this field exists to prevent.
*/
msaExecuted: boolean('msa_executed').notNull().default(false),
dpaExecuted: boolean('dpa_executed').notNull().default(false),
/** Set when this deal is a renewal or expansion of an earlier one. */
parentDealId: uuid('parent_deal_id'),
lastActivityAt: timestamp('last_activity_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('demand_deals_account_idx').on(t.accountId),
index('demand_deals_stage_idx').on(t.stage),
index('demand_deals_owner_idx').on(t.ownerUserId),
index('demand_deals_close_date_idx').on(t.expectedCloseDate),
],
);
/**
* The technical shape of what a customer needs.
*
* Separated from the deal because this is the object that gets matched against
* supply. A seller asking "what have we got that fits this?" is asking about
* hardware, dates and constraints — not about ACV.
*/
export const capacityRequests = pgTable(
'capacity_requests',
{
id: uuid('id').primaryKey().defaultRandom(),
demandDealId: uuid('demand_deal_id')
.notNull()
.references(() => demandDeals.id, { onDelete: 'cascade' }),
gpuType: text('gpu_type'),
/** Acceptable substitutes, in preference order. Widens the match. */
gpuTypeAlternatives: jsonb('gpu_type_alternatives').$type<string[]>().notNull().default([]),
gpuCount: integer('gpu_count').notNull(),
/**
* A hard requirement for distributed training and irrelevant for inference.
* Getting this wrong in either direction is expensive: sell Ethernet to a
* training customer and it fails; insist on InfiniBand for an inference
* customer and you lose on price.
*/
requiresHighSpeedInterconnect: boolean('requires_high_speed_interconnect')
.notNull()
.default(false),
minInterconnectType: interconnectTypeEnum('min_interconnect_type'),
minSecurityTier: securityTierEnum('min_security_tier').notNull().default('secure_cloud'),
/** Regions the workload may run in, and those it may not. */
allowedRegions: jsonb('allowed_regions').$type<string[]>().notNull().default([]),
excludedJurisdictions: jsonb('excluded_jurisdictions').$type<string[]>().notNull().default([]),
startsAt: timestamp('starts_at', { withTimezone: true }),
endsAt: timestamp('ends_at', { withTimezone: true }),
/** Total hours wanted; drives the allocation arithmetic. */
totalGpuHours: numeric('total_gpu_hours', { precision: 16, scale: 2 }),
/** Ceiling the customer will pay, in cents. The other half of the spread. */
maxPricePerGpuHourCents: integer('max_price_per_gpu_hour_cents'),
/** Compliance requirements surfaced during procurement. */
requiredCertifications: jsonb('required_certifications').$type<string[]>().notNull().default([]),
notes: text('notes'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('capacity_requests_deal_idx').on(t.demandDealId),
index('capacity_requests_gpu_type_idx').on(t.gpuType),
index('capacity_requests_window_idx').on(t.startsAt, t.endsAt),
],
);
/** Many-to-many between deals and the people involved in them. */
export const dealContacts = pgTable(
'deal_contacts',
{
id: uuid('id').primaryKey().defaultRandom(),
demandDealId: uuid('demand_deal_id')
.notNull()
.references(() => demandDeals.id, { onDelete: 'cascade' }),
contactId: uuid('contact_id')
.notNull()
.references(() => contacts.id, { onDelete: 'cascade' }),
/** economic buyer | technical evaluator | champion | procurement | legal | blocker */
role: text('role'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('deal_contacts_deal_idx').on(t.demandDealId),
index('deal_contacts_contact_idx').on(t.contactId),
],
);
export type DemandDeal = typeof demandDeals.$inferSelect;
export type NewDemandDeal = typeof demandDeals.$inferInsert;
export type CapacityRequest = typeof capacityRequests.$inferSelect;
export type NewCapacityRequest = typeof capacityRequests.$inferInsert;