/** * 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, 0–1. 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().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().notNull().default([]), excludedJurisdictions: jsonb('excluded_jurisdictions').$type().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().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;