Build the agent-native compute CRM platform
CI / verify (push) Successful in 3m6s

This commit is contained in:
2026-08-13 01:39:01 -07:00
parent bfd2f8d95a
commit 853bde2265
160 changed files with 61812 additions and 483 deletions
+711
View File
@@ -0,0 +1,711 @@
import {
ACCOUNT_SIDES,
AFFILIATION_KINDS,
CUSTOMER_SEGMENTS,
DEMAND_STAGES,
INTERCONNECT_TYPES,
PRODUCT_LINES,
SUPPLIER_TYPES,
SUPPLY_STAGES,
permissionGranted,
type AccountSide,
type Team,
} from '@pig/core';
import type { Database } from '@pig/db';
import { accounts, agentTasks, contacts, demandDeals, sites, supplyDeals } from '@pig/db';
import { desc, eq } from 'drizzle-orm';
import { Hono } from 'hono';
import { z } from 'zod';
import { AuthError, effectivePermissions, type Principal } from '../lib/auth';
import { MutationError, mutation, type ApiEnv, type MutationDefinition } from '../lib/mutation';
import type { NotificationOutbox } from '../services/notification-outbox';
type Transaction = Parameters<Parameters<Database['transaction']>[0]>[0];
const requiredText = (maximum: number) => z.string().trim().min(1).max(maximum);
const nullableText = (maximum: number) => z.string().trim().max(maximum).nullable().optional();
const nullableUrl = z.string().trim().url().max(400).nullable().optional();
const nullableUuid = z.string().uuid().nullable().optional();
const nullableDate = z
.union([z.string().date(), z.string().datetime()])
.nullable()
.optional()
.transform((value) => {
if (!value) return value === null ? null : undefined;
return new Date(value.length === 10 ? `${value}T12:00:00.000Z` : value);
});
const nullablePositiveInteger = z.number().int().positive().nullable().optional();
const nullableNonnegativeInteger = z.number().int().nonnegative().nullable().optional();
const accountFields = {
name: requiredText(200),
domain: nullableText(200).transform((value) => value?.toLowerCase() || value),
website: nullableUrl,
description: nullableText(4000),
side: z.enum(ACCOUNT_SIDES),
supplierType: z.enum(SUPPLIER_TYPES).nullable().optional(),
customerSegment: z.enum(CUSTOMER_SEGMENTS).nullable().optional(),
country: nullableText(100),
region: nullableText(100),
jurisdiction: nullableText(100),
ultimateParentName: nullableText(200),
ultimateParentCountry: nullableText(100),
};
const accountCreateSchema = z.object(accountFields).strict();
const accountUpdateSchema = z.object(accountFields).partial().strict();
const contactFields = {
accountId: z.string().uuid(),
fullName: requiredText(200),
firstName: nullableText(100),
lastName: nullableText(100),
title: nullableText(200),
email: z.string().trim().email().max(320).nullable().optional(),
phone: nullableText(80),
linkedinUrl: nullableUrl,
twitterHandle: nullableText(100),
githubHandle: nullableText(100),
websiteUrl: nullableUrl,
affiliation: z.enum(AFFILIATION_KINDS),
isDecisionMaker: z.boolean(),
confidenceNote: nullableText(2000),
};
const contactCreateSchema = z.object(contactFields).strict();
const contactUpdateSchema = z.object(contactFields).partial().strict();
const demandDealFields = {
accountId: z.string().uuid(),
name: requiredText(240),
description: nullableText(4000),
productLine: z.enum(PRODUCT_LINES),
stage: z.enum(DEMAND_STAGES),
primaryContactId: nullableUuid,
acvCents: nullableNonnegativeInteger,
tcvCents: nullableNonnegativeInteger,
currency: z.string().trim().toUpperCase().regex(/^[A-Z]{3}$/),
termMonths: nullablePositiveInteger,
probability: z.number().min(0).max(1).nullable().optional(),
expectedCloseDate: nullableDate,
closedReason: nullableText(2000),
msaExecuted: z.boolean(),
dpaExecuted: z.boolean(),
parentDealId: nullableUuid,
};
const demandDealCreateSchema = z.object(demandDealFields).strict();
const demandDealUpdateSchema = z.object(demandDealFields).partial().strict();
const supplyDealFields = {
accountId: z.string().uuid(),
siteId: nullableUuid,
name: requiredText(240),
stage: z.enum(SUPPLY_STAGES),
primaryContactId: nullableUuid,
gpuType: nullableText(100),
gpuCount: nullablePositiveInteger,
interconnectType: z.enum(INTERCONNECT_TYPES).nullable().optional(),
targetCostPerGpuHourCents: nullableNonnegativeInteger,
termMonths: nullablePositiveInteger,
availableFrom: nullableDate,
technicalVerdict: nullableText(500),
technicalNotes: nullableText(4000),
financialVerdict: nullableText(500),
financialNotes: nullableText(4000),
rejectionReason: nullableText(2000),
};
const supplyDealCreateSchema = z.object(supplyDealFields).strict();
const supplyDealUpdateSchema = z.object(supplyDealFields).partial().strict();
export function accountSupportsTeam(side: AccountSide, team: Team): boolean {
return team !== 'research' && (side === 'both' || side === team);
}
function hasDealPermission(principal: Principal, team: 'supply' | 'demand'): boolean {
return permissionGranted(effectivePermissions(principal), 'deal:write', team);
}
function requireAnyDealPermission(principal: Principal): void {
if (hasDealPermission(principal, 'supply') || hasDealPermission(principal, 'demand')) return;
throw new AuthError(
"This principal lacks the 'deal:write' capability for supply or demand.",
403,
'insufficient_permission',
);
}
function requireAccountPermission(principal: Principal, side: AccountSide): void {
if (
(accountSupportsTeam(side, 'supply') && hasDealPermission(principal, 'supply')) ||
(accountSupportsTeam(side, 'demand') && hasDealPermission(principal, 'demand'))
) {
return;
}
throw new AuthError(
`This principal cannot write ${side}-side records.`,
403,
'insufficient_permission',
);
}
async function findAccount(tx: Transaction, id: string) {
const [account] = await tx.select().from(accounts).where(eq(accounts.id, id)).limit(1);
if (!account) throw MutationError.notFound('Account');
return account;
}
async function requireAccountForTeam(tx: Transaction, id: string, team: 'supply' | 'demand') {
const account = await findAccount(tx, id);
if (!accountSupportsTeam(account.side, team)) {
throw new MutationError(
'relationship_mismatch',
`The selected account is not on the ${team} side.`,
409,
);
}
return account;
}
async function requireContactForAccount(
tx: Transaction,
contactId: string | null | undefined,
accountId: string,
): Promise<void> {
if (!contactId) return;
const [contact] = await tx.select().from(contacts).where(eq(contacts.id, contactId)).limit(1);
if (!contact) throw MutationError.notFound('Contact');
if (contact.accountId !== accountId) {
throw new MutationError(
'relationship_mismatch',
'The primary contact must belong to the selected account.',
409,
);
}
}
async function touchAccount(tx: Transaction, accountId: string, now: Date): Promise<void> {
await tx
.update(accounts)
.set({ lastActivityAt: now, updatedAt: now })
.where(eq(accounts.id, accountId));
}
const anyDealPermission = { authorize: requireAnyDealPermission } as const;
export function createAccountMutationDefinition(): MutationDefinition<
typeof accountCreateSchema,
typeof accounts.$inferSelect
> {
return {
schema: accountCreateSchema,
permission: anyDealPermission,
invalidMessage: 'Invalid account.',
async mutate({ input, principal, tx, now }) {
requireAccountPermission(principal, input.side);
const [created] = await tx
.insert(accounts)
.values({
...input,
ownerUserId: principal.userId,
source: 'manual',
lastActivityAt: now,
updatedAt: now,
})
.returning();
if (!created) throw new MutationError('write_failed', 'Account was not created.', 409);
await tx.insert(agentTasks).values({
kind: 'enrich_account',
subject: created.id,
reason: `New account created by ${principal.name}`,
requestedByUserId: principal.userId,
});
return {
data: created,
activity: {
type: 'note',
subject: `Created account — ${created.name}`,
accountId: created.id,
meta: { action: 'created', recordType: 'account', side: created.side },
},
};
},
};
}
function updateAccountMutationDefinition(): MutationDefinition<
typeof accountUpdateSchema,
typeof accounts.$inferSelect
> {
return {
schema: accountUpdateSchema,
permission: anyDealPermission,
invalidMessage: 'Invalid account update.',
async mutate({ input, principal, params, tx, now }) {
if (!params.id) throw MutationError.notFound('Account');
const before = await findAccount(tx, params.id);
const nextSide = input.side ?? before.side;
// Both checks matter when a record crosses sides: access to the source
// record must not grant permission to move it into another team's book.
requireAccountPermission(principal, before.side);
requireAccountPermission(principal, nextSide);
const [updated] = await tx
.update(accounts)
.set({ ...input, updatedAt: now, lastActivityAt: now })
.where(eq(accounts.id, before.id))
.returning();
if (!updated) throw MutationError.notFound('Account');
return {
data: updated,
activity: {
type: 'note',
subject: `Updated account — ${updated.name}`,
accountId: updated.id,
meta: { action: 'updated', recordType: 'account', fromSide: before.side, side: updated.side },
},
};
},
};
}
function createContactMutationDefinition(): MutationDefinition<
typeof contactCreateSchema,
typeof contacts.$inferSelect
> {
return {
schema: contactCreateSchema,
permission: anyDealPermission,
invalidMessage: 'Invalid contact.',
async mutate({ input, principal, tx, now }) {
const account = await findAccount(tx, input.accountId);
requireAccountPermission(principal, account.side);
const [created] = await tx
.insert(contacts)
.values({
...input,
ownerUserId: principal.userId,
source: 'manual',
lastActivityAt: now,
updatedAt: now,
})
.returning();
if (!created) throw new MutationError('write_failed', 'Contact was not created.', 409);
await touchAccount(tx, account.id, now);
await tx.insert(agentTasks).values({
kind: 'enrich_contact',
subject: created.id,
reason: `New contact created by ${principal.name}`,
requestedByUserId: principal.userId,
});
return {
data: created,
activity: {
type: 'note',
subject: `Created contact — ${created.fullName}`,
accountId: account.id,
contactId: created.id,
meta: { action: 'created', recordType: 'contact', side: account.side },
},
};
},
};
}
function updateContactMutationDefinition(): MutationDefinition<
typeof contactUpdateSchema,
typeof contacts.$inferSelect
> {
return {
schema: contactUpdateSchema,
permission: anyDealPermission,
invalidMessage: 'Invalid contact update.',
async mutate({ input, principal, params, tx, now }) {
if (!params.id) throw MutationError.notFound('Contact');
const [before] = await tx.select().from(contacts).where(eq(contacts.id, params.id)).limit(1);
if (!before) throw MutationError.notFound('Contact');
if (!before.accountId && !input.accountId) {
throw new MutationError(
'relationship_required',
'Select an account before editing this contact.',
409,
);
}
if (before.accountId) {
const beforeAccount = await findAccount(tx, before.accountId);
requireAccountPermission(principal, beforeAccount.side);
}
const account = await findAccount(tx, input.accountId ?? before.accountId!);
requireAccountPermission(principal, account.side);
const [updated] = await tx
.update(contacts)
.set({ ...input, updatedAt: now, lastActivityAt: now })
.where(eq(contacts.id, before.id))
.returning();
if (!updated) throw MutationError.notFound('Contact');
await touchAccount(tx, account.id, now);
return {
data: updated,
activity: {
type: 'note',
subject: `Updated contact — ${updated.fullName}`,
accountId: account.id,
contactId: updated.id,
meta: { action: 'updated', recordType: 'contact', side: account.side },
},
};
},
};
}
export function createDemandDealMutationDefinition(): MutationDefinition<
typeof demandDealCreateSchema,
typeof demandDeals.$inferSelect
> {
return {
schema: demandDealCreateSchema,
permission: { capability: 'deal:write', team: 'demand' },
invalidMessage: 'Invalid demand deal.',
async mutate({ input, principal, tx, now }) {
const account = await requireAccountForTeam(tx, input.accountId, 'demand');
await requireContactForAccount(tx, input.primaryContactId, account.id);
if (input.parentDealId) {
const [parent] = await tx
.select()
.from(demandDeals)
.where(eq(demandDeals.id, input.parentDealId))
.limit(1);
if (!parent) throw MutationError.notFound('Parent demand deal');
if (parent.accountId !== account.id) {
throw new MutationError(
'relationship_mismatch',
'A parent deal must belong to the selected account.',
409,
);
}
}
const closedAt = ['closed_won', 'closed_lost'].includes(input.stage) ? now : null;
const probability =
input.probability === undefined
? undefined
: input.probability === null
? null
: String(input.probability);
const [created] = await tx
.insert(demandDeals)
.values({
...input,
probability,
ownerUserId: principal.userId,
stageChangedAt: now,
closedAt,
lastActivityAt: now,
updatedAt: now,
})
.returning();
if (!created) throw new MutationError('write_failed', 'Demand deal was not created.', 409);
await touchAccount(tx, account.id, now);
return {
data: created,
activity: {
type: 'note',
subject: `Created demand deal — ${created.name}`,
accountId: account.id,
demandDealId: created.id,
meta: { action: 'created', recordType: 'demand_deal', stage: created.stage },
},
};
},
};
}
function updateDemandDealMutationDefinition(notifications?: NotificationOutbox): MutationDefinition<
typeof demandDealUpdateSchema,
typeof demandDeals.$inferSelect
> {
return {
schema: demandDealUpdateSchema,
permission: { capability: 'deal:write', team: 'demand' },
invalidMessage: 'Invalid demand deal update.',
async mutate({ input, params, tx, now }) {
if (!params.id) throw MutationError.notFound('Demand deal');
const [before] = await tx
.select()
.from(demandDeals)
.where(eq(demandDeals.id, params.id))
.limit(1);
if (!before) throw MutationError.notFound('Demand deal');
const accountId = input.accountId ?? before.accountId;
const account = await requireAccountForTeam(tx, accountId, 'demand');
const contactId = input.primaryContactId === undefined
? before.primaryContactId
: input.primaryContactId;
await requireContactForAccount(tx, contactId, account.id);
const parentId = input.parentDealId === undefined ? before.parentDealId : input.parentDealId;
if (parentId) {
if (parentId === before.id) {
throw new MutationError('relationship_mismatch', 'A deal cannot be its own parent.', 409);
}
const [parent] = await tx.select().from(demandDeals).where(eq(demandDeals.id, parentId)).limit(1);
if (!parent) throw MutationError.notFound('Parent demand deal');
if (parent.accountId !== account.id) {
throw new MutationError(
'relationship_mismatch',
'A parent deal must belong to the selected account.',
409,
);
}
}
const stage = input.stage ?? before.stage;
const stageChanged = stage !== before.stage;
const probability =
input.probability === undefined
? undefined
: input.probability === null
? null
: String(input.probability);
const closedAt = stageChanged
? ['closed_won', 'closed_lost'].includes(stage)
? now
: null
: before.closedAt;
const [updated] = await tx
.update(demandDeals)
.set({
...input,
probability,
stageChangedAt: stageChanged ? now : before.stageChangedAt,
closedAt,
updatedAt: now,
lastActivityAt: now,
})
.where(eq(demandDeals.id, before.id))
.returning();
if (!updated) throw MutationError.notFound('Demand deal');
await touchAccount(tx, account.id, now);
if (stageChanged) {
await notifications?.enqueueStageChange(tx, {
accountId: account.id,
dealId: updated.id,
dealSide: 'demand',
dealName: updated.name,
fromStage: before.stage,
toStage: updated.stage,
changedAt: now.toISOString(),
});
}
return {
data: updated,
activity: {
type: stageChanged ? 'stage_change' : 'note',
subject: stageChanged
? `${before.stage}${updated.stage}`
: `Updated demand deal — ${updated.name}`,
accountId: account.id,
demandDealId: updated.id,
meta: {
action: 'updated',
recordType: 'demand_deal',
fromStage: before.stage,
stage: updated.stage,
},
},
};
},
};
}
function createSupplyDealMutationDefinition(): MutationDefinition<
typeof supplyDealCreateSchema,
typeof supplyDeals.$inferSelect
> {
return {
schema: supplyDealCreateSchema,
permission: { capability: 'deal:write', team: 'supply' },
invalidMessage: 'Invalid supply deal.',
async mutate({ input, principal, tx, now }) {
const account = await requireAccountForTeam(tx, input.accountId, 'supply');
await requireContactForAccount(tx, input.primaryContactId, account.id);
if (input.siteId) {
const [site] = await tx.select().from(sites).where(eq(sites.id, input.siteId)).limit(1);
if (!site) throw MutationError.notFound('Site');
if (site.accountId !== account.id) {
throw new MutationError(
'relationship_mismatch',
'The selected site must belong to the selected account.',
409,
);
}
}
const closedAt = ['live', 'churned', 'rejected'].includes(input.stage) ? now : null;
const [created] = await tx
.insert(supplyDeals)
.values({
...input,
ownerUserId: principal.userId,
stageChangedAt: now,
closedAt,
lastActivityAt: now,
updatedAt: now,
})
.returning();
if (!created) throw new MutationError('write_failed', 'Supply deal was not created.', 409);
await touchAccount(tx, account.id, now);
return {
data: created,
activity: {
type: 'note',
subject: `Created supply deal — ${created.name}`,
accountId: account.id,
supplyDealId: created.id,
meta: { action: 'created', recordType: 'supply_deal', stage: created.stage },
},
};
},
};
}
function updateSupplyDealMutationDefinition(notifications?: NotificationOutbox): MutationDefinition<
typeof supplyDealUpdateSchema,
typeof supplyDeals.$inferSelect
> {
return {
schema: supplyDealUpdateSchema,
permission: { capability: 'deal:write', team: 'supply' },
invalidMessage: 'Invalid supply deal update.',
async mutate({ input, principal, params, tx, now }) {
if (!params.id) throw MutationError.notFound('Supply deal');
const [before] = await tx.select().from(supplyDeals).where(eq(supplyDeals.id, params.id)).limit(1);
if (!before) throw MutationError.notFound('Supply deal');
const accountId = input.accountId ?? before.accountId;
const account = await requireAccountForTeam(tx, accountId, 'supply');
const contactId = input.primaryContactId === undefined
? before.primaryContactId
: input.primaryContactId;
await requireContactForAccount(tx, contactId, account.id);
const siteId = input.siteId === undefined ? before.siteId : input.siteId;
if (siteId) {
const [site] = await tx.select().from(sites).where(eq(sites.id, siteId)).limit(1);
if (!site) throw MutationError.notFound('Site');
if (site.accountId !== account.id) {
throw new MutationError(
'relationship_mismatch',
'The selected site must belong to the selected account.',
409,
);
}
}
const stage = input.stage ?? before.stage;
const stageChanged = stage !== before.stage;
const closedAt = stageChanged
? ['live', 'churned', 'rejected'].includes(stage)
? now
: null
: before.closedAt;
const technicalVerdictChanged =
input.technicalVerdict !== undefined &&
input.technicalVerdict !== before.technicalVerdict;
const financialVerdictChanged =
input.financialVerdict !== undefined &&
input.financialVerdict !== before.financialVerdict;
const verdictUpdate = {
technicalVerdictBy: technicalVerdictChanged
? input.technicalVerdict === null
? null
: principal.userId
: undefined,
technicalVerdictAt: technicalVerdictChanged
? input.technicalVerdict === null
? null
: now
: undefined,
financialVerdictBy: financialVerdictChanged
? input.financialVerdict === null
? null
: principal.userId
: undefined,
financialVerdictAt: financialVerdictChanged
? input.financialVerdict === null
? null
: now
: undefined,
};
const [updated] = await tx
.update(supplyDeals)
.set({
...input,
...verdictUpdate,
stageChangedAt: stageChanged ? now : before.stageChangedAt,
closedAt,
updatedAt: now,
lastActivityAt: now,
})
.where(eq(supplyDeals.id, before.id))
.returning();
if (!updated) throw MutationError.notFound('Supply deal');
await touchAccount(tx, account.id, now);
if (stageChanged) {
await notifications?.enqueueStageChange(tx, {
accountId: account.id,
dealId: updated.id,
dealSide: 'supply',
dealName: updated.name,
fromStage: before.stage,
toStage: updated.stage,
changedAt: now.toISOString(),
});
}
return {
data: updated,
activity: {
type: stageChanged ? 'stage_change' : 'note',
subject: stageChanged
? `${before.stage}${updated.stage}`
: `Updated supply deal — ${updated.name}`,
accountId: account.id,
supplyDealId: updated.id,
meta: {
action: 'updated',
recordType: 'supply_deal',
fromStage: before.stage,
stage: updated.stage,
},
},
};
},
};
}
export function createRecordRoutes(db: Database, notifications?: NotificationOutbox): Hono<ApiEnv> {
const app = new Hono<ApiEnv>();
app.get('/api/contacts', async (c) => {
const accountId = c.req.query('accountId');
const rows = await db
.select({ contact: contacts, accountName: accounts.name, accountSide: accounts.side })
.from(contacts)
.leftJoin(accounts, eq(accounts.id, contacts.accountId))
.where(accountId ? eq(contacts.accountId, accountId) : undefined)
.orderBy(desc(contacts.lastActivityAt), contacts.fullName)
.limit(500);
return c.json(rows);
});
app.post('/api/accounts', mutation(db, createAccountMutationDefinition()));
app.patch('/api/accounts/:id', mutation(db, updateAccountMutationDefinition()));
app.post('/api/contacts', mutation(db, createContactMutationDefinition()));
app.patch('/api/contacts/:id', mutation(db, updateContactMutationDefinition()));
app.post('/api/deals/demand', mutation(db, createDemandDealMutationDefinition()));
app.patch('/api/deals/demand/:id', mutation(db, updateDemandDealMutationDefinition(notifications)));
app.post('/api/deals/supply', mutation(db, createSupplyDealMutationDefinition()));
app.patch('/api/deals/supply/:id', mutation(db, updateSupplyDealMutationDefinition(notifications)));
return app;
}