ConusAI vendor v0.1.0-beta.114
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-beta.114
2026-11-14 09:56:45 +00:00
commit 1ce8d23c35
867 changed files with 122104 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
// 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 },
}
}
+47
View File
@@ -0,0 +1,47 @@
// What the crm module does, as pure TypeScript over the port in ports.ts.
//
// Parties belong to the credential-bound app tenant and environment. Projects within
// that partition share its address book. Linked people use the actual resolved
// app member reference; another app tenant or environment has a different binding.
import type { ProjectScope } from "@conusai-kit/core"
import { type ContactView, PAGE_LIMIT, type PartyView } from "../contract/index.js"
import { type ContactActionInput, contactActionData, type PartyActionInput, partyActionData } from "./action-plan.js"
import type { CrmReads, CrmService, Page } from "./ports.js"
export function listParties(
reads: CrmReads,
query: { q?: string; after?: string; limit?: number },
): Promise<Page<PartyView>> {
const q = query.q?.trim()
return reads.listParties({
...(q ? { q } : {}),
...(query.after === undefined ? {} : { after: query.after }),
limit: query.limit ?? PAGE_LIMIT,
})
}
export async function getParty(
reads: CrmReads,
partyId: string,
): Promise<{ party: PartyView; contacts: ContactView[] }> {
const party = await reads.getParty(partyId)
const contacts = await reads.listContacts(partyId, { limit: PAGE_LIMIT })
return { party, contacts: contacts.rows }
}
export async function createParty(
service: CrmService,
scope: ProjectScope,
input: PartyActionInput,
): Promise<PartyView> {
return service.createParty(partyActionData(scope, service.bindingId, input))
}
export function addContact(
service: CrmService,
scope: ProjectScope,
input: ContactActionInput,
now: Date = new Date(),
): Promise<ContactView> {
return service.addContact(contactActionData(scope, input, now))
}
+56
View File
@@ -0,0 +1,56 @@
// The port the crm module's use cases are written against: the platform's CRM service, in this
// module's own words. adapters/service.ts is the one place that knows `@conusai-sdk/crm` exists, and
// it turns every refusal into the kit's app errors (`businessAppError` of
// `@conusai-kit/business`) before core sees it.
import { token } from "@conusai-kit/core"
import type { ContactView, PartyView } from "../contract/index.js"
export interface Page<T> {
readonly rows: T[]
readonly after?: string
}
/** What a `read` procedure may ask the service: nothing that changes a row. */
export interface CrmReads {
/** The immutable app/environment binding UUID for attributed member references. */
readonly bindingId: string
listParties(query: { q?: string; after?: string; limit: number }): Promise<Page<PartyView>>
getParty(partyId: string): Promise<PartyView>
listContacts(partyId: string, query: { after?: string; limit: number }): Promise<Page<ContactView>>
}
export interface CrmService extends CrmReads {
createParty(input: {
kind: "person" | "organization"
displayName: string
legalName?: string
country: string
identifiers: { scheme: string; value: string }[]
key: string
}): Promise<PartyView>
addContact(input: {
partyId: string
kind: "email" | "phone" | "web"
value: string
purposes: ContactView["purposes"]
isPrimary: boolean
/** `YYYY-MM-DD`: the day the contact point starts to hold. */
effectiveFrom: string
key: string
}): Promise<ContactView>
}
/** The read view a `read` or `write` handler receives: the kind is a capability, not a label. */
export const crmReadsOf = (service: CrmReads): CrmReads => ({
bindingId: service.bindingId,
listParties: (query) => service.listParties(query),
getParty: (partyId) => service.getParty(partyId),
listContacts: (partyId, query) => service.listContacts(partyId, query),
})
/**
* Bound by a composition root or a test that brings its own client (another organization, the
* business-services fake). With nothing bound, the module calls this deployment's service.
*/
export const CRM_SERVICE = token<CrmService>("crm.service", { views: { read: crmReadsOf, write: crmReadsOf } })