Files
template-app/modules/crm/contract/procedures.ts
T
2026-11-14 09:56:45 +00:00

102 lines
3.4 KiB
TypeScript

// The procedure contracts of the crm module: schemas and names, no handler and no port.
//
// The module is a client of the platform's CRM service (B-wave contract §3.14): parties and their
// contact points live there, in the deployment's organization, and every procedure is one or two
// service calls. A read reads; a change is an `effect` (it calls an external service and writes
// nothing of this app's own).
import { procedure } from "@conusai-kit/core"
import { z } from "zod"
import { CONTACT_KINDS, CONTACT_PURPOSES, CRM_PERMISSIONS } from "./constants.js"
const inProject = { org: z.string(), workspace: z.string(), project: z.string() }
/** What a form generates once per submission, so a retried request creates once. */
const requestId = z.string().uuid()
const partyId = z.string().min(1).max(64)
export const partyViewSchema = z.object({
partyId: z.string(),
kind: z.enum(["person", "organization"]),
displayName: z.string(),
legalName: z.string().nullable(),
/** ISO 3166-1 alpha-2. */
country: z.string(),
status: z.enum(["active", "archived", "merged"]),
/** Whether an identifier links the party to one of this app's people. */
linked: z.boolean(),
})
export type PartyView = z.output<typeof partyViewSchema>
export const contactViewSchema = z.object({
contactId: z.string(),
kind: z.enum(["postal", "email", "phone", "web", "peppol_endpoint"]),
/** The value as a person reads it; empty once the service erased it. */
value: z.string(),
purposes: z.array(z.enum(CONTACT_PURPOSES)),
isPrimary: z.boolean(),
erased: z.boolean(),
})
export type ContactView = z.output<typeof contactViewSchema>
export const partyList = procedure("crm.party.list")
.kind("read")
.scope("project")
.permission(CRM_PERMISSIONS.read)
.input(
z.object({
...inProject,
q: z.string().max(200).optional(),
after: z.string().max(64).optional(),
limit: z.number().int().min(1).max(200).optional(),
}),
)
.output(z.object({ rows: z.array(partyViewSchema), after: z.string().optional() }))
.contract()
export const partyGet = procedure("crm.party.get")
.kind("read")
.scope("project")
.permission(CRM_PERMISSIONS.read)
.input(z.object({ ...inProject, partyId }))
.output(z.object({ party: partyViewSchema, contacts: z.array(contactViewSchema) }))
.contract()
export const partyCreate = procedure("crm.party.create")
.kind("effect")
.tool({ approval: "human" })
.scope("project")
.permission(CRM_PERMISSIONS.write)
.input(
z.object({
...inProject,
kind: z.enum(["person", "organization"]),
displayName: z.string().trim().min(1).max(300),
legalName: z.string().trim().max(300).optional(),
country: z.string().regex(/^[A-Z]{2}$/),
/** A person who is the member creating it: the party is linked to them (an export or erasure of them reaches it). */
linkSelf: z.boolean().optional(),
requestId,
}),
)
.output(partyViewSchema)
.contract()
export const contactAdd = procedure("crm.contact.add")
.kind("effect")
.tool({ approval: "human" })
.scope("project")
.permission(CRM_PERMISSIONS.write)
.input(
z.object({
...inProject,
partyId,
kind: z.enum(CONTACT_KINDS),
value: z.string().trim().min(1).max(256),
purposes: z.array(z.enum(CONTACT_PURPOSES)).max(CONTACT_PURPOSES.length),
isPrimary: z.boolean(),
requestId,
}),
)
.output(contactViewSchema)
.contract()