Files
template-app/modules/crm/core/action-plan.ts
T
2026-11-14 09:56:45 +00:00

109 lines
3.6 KiB
TypeScript

// Shared pure transformations used by the actual CRM core AND its approval
// inspector. No database, network, permission or Root authority is established.
import { actorRef } from "@conusai-kit/business"
import { forbidden, type ProjectScope } from "@conusai-kit/core"
import { type ContactView, SUBJECT_SCHEME } from "../contract/index.js"
import type { CrmService } from "./ports.js"
export interface PartyActionInput {
kind: "person" | "organization"
displayName: string
legalName?: string | undefined
country: string
linkSelf?: boolean | undefined
requestId: string
}
export interface ContactActionInput {
partyId: string
kind: "email" | "phone" | "web"
value: string
purposes: ContactView["purposes"]
isPrimary: boolean
requestId: string
}
export const requestKey = (scope: Pick<ProjectScope, "projectId">, requestId: string): string =>
`app-project:${scope.projectId}:${requestId}`
export const dayOf = (at: Date): string => at.toISOString().slice(0, 10)
export function partyActionData(
scope: ProjectScope,
bindingId: string,
input: PartyActionInput,
): Parameters<CrmService["createParty"]>[0] {
const identifiers: { scheme: string; value: string }[] = []
if (input.linkSelf) {
if (input.kind !== "person") throw forbidden("only a person can be linked to a member")
if (scope.actor.system) throw forbidden("a system actor is nobody's party")
identifiers.push({ scheme: SUBJECT_SCHEME, value: actorRef(bindingId, scope.actor.userId) })
}
const legalName = input.legalName?.trim()
return {
kind: input.kind,
displayName: input.displayName.trim(),
...(legalName ? { legalName } : {}),
country: input.country,
identifiers,
key: requestKey(scope, input.requestId),
}
}
export function contactActionData(
scope: ProjectScope,
input: ContactActionInput,
now: Date,
): Parameters<CrmService["addContact"]>[0] {
return {
partyId: input.partyId,
kind: input.kind,
value: input.value.trim(),
purposes: [...new Set(input.purposes)],
isPrimary: input.isPrimary,
effectiveFrom: dayOf(now),
key: requestKey(scope, input.requestId),
}
}
export function partyRequestBody(input: Parameters<CrmService["createParty"]>[0]) {
return {
data: {
kind: input.kind,
display_name: input.displayName,
country: input.country,
...(input.legalName === undefined ? {} : { legal_name: input.legalName }),
...(input.identifiers.length === 0 ? {} : { identifiers: input.identifiers }),
},
idempotency_key: input.key,
}
}
export function contactRequestBody(input: Parameters<CrmService["addContact"]>[0]) {
return {
data: {
kind: input.kind,
value: input.value,
purposes: input.purposes,
is_primary: input.isPrimary,
effective: { from: input.effectiveFrom },
},
idempotency_key: input.key,
}
}
export function partyEncodedRequest(orgSlug: string, input: Parameters<CrmService["createParty"]>[0]) {
const body = partyRequestBody(input)
return {
service: "crm",
operation: "crm_create_party",
method: "POST" as const,
path: `/v1/orgs/${encodeURIComponent(orgSlug)}/crm/parties`,
body,
arguments: { body },
}
}
export function contactEncodedRequest(orgSlug: string, input: Parameters<CrmService["addContact"]>[0]) {
const body = contactRequestBody(input)
return {
service: "crm",
operation: "crm_add_contact_point",
method: "POST" as const,
path: `/v1/orgs/${encodeURIComponent(orgSlug)}/crm/parties/${encodeURIComponent(input.partyId)}/contacts`,
body,
arguments: { id: input.partyId, body },
}
}