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

163 lines
4.5 KiB
TypeScript

import { dayOf, requestKey } from "../core/action-plan.js"
// The crm use cases over a recording port: what each asks the service, with no client and no network.
import { actorRef } from "@conusai-kit/business"
import type { ProjectScope } from "@conusai-kit/core"
import { describe, expect, it } from "vitest"
import type { PartyView } from "../contract/index.js"
import { addContact, createParty, listParties } from "../core/crm.js"
import { type CrmService, crmReadsOf } from "../core/ports.js"
const scope: ProjectScope = {
level: "project",
actor: { userId: "01900000-0000-7000-8000-000000000002" },
orgId: "o1",
workspaceId: "w1",
projectId: "p1",
}
const party: PartyView = {
partyId: "x",
kind: "person",
displayName: "Ada",
legalName: null,
country: "LT",
status: "active",
linked: false,
}
function recording() {
const asked: Array<[string, unknown]> = []
const service: CrmService = {
bindingId: "01900000-0000-7000-8000-000000000001",
listParties: async (query) => {
asked.push(["listParties", query])
return { rows: [] }
},
getParty: async () => party,
listContacts: async () => ({ rows: [] }),
createParty: async (input) => {
asked.push(["createParty", input])
return party
},
addContact: async (input) => {
asked.push(["addContact", input])
return {
contactId: "c",
kind: input.kind,
value: input.value,
purposes: input.purposes,
isPrimary: input.isPrimary,
erased: false,
}
},
}
return { service, asked }
}
describe("the crm use cases", () => {
it("list parties with a trimmed search and the module's page size", async () => {
const { service, asked } = recording()
await listParties(service, { q: " acme " })
expect(asked).toEqual([["listParties", { q: "acme", limit: 50 }]])
})
it("create a party with the project's request key and no identifier unless asked", async () => {
const { service, asked } = recording()
await createParty(service, scope, {
kind: "organization",
displayName: " Acme ",
legalName: " ",
country: "LT",
requestId: "r1",
})
expect(asked).toEqual([
[
"createParty",
{ kind: "organization", displayName: "Acme", country: "LT", identifiers: [], key: requestKey(scope, "r1") },
],
])
})
it("link a person to the member with the member's subject reference, and only a person, and only a member", async () => {
const { service, asked } = recording()
await createParty(service, scope, {
kind: "person",
displayName: "Ada",
country: "LT",
linkSelf: true,
requestId: "r2",
})
expect(asked[0]?.[1]).toMatchObject({
identifiers: [
{
scheme: "conusai.subject",
value: actorRef("01900000-0000-7000-8000-000000000001", "01900000-0000-7000-8000-000000000002"),
},
],
})
await expect(
createParty(service, scope, {
kind: "organization",
displayName: "Acme",
country: "LT",
linkSelf: true,
requestId: "r3",
}),
).rejects.toMatchObject({ problem: { status: 403 } })
const system: ProjectScope = { ...scope, actor: { userId: "system:sweep", system: true } }
await expect(
createParty(service, system, {
kind: "person",
displayName: "Ada",
country: "LT",
linkSelf: true,
requestId: "r4",
}),
).rejects.toMatchObject({ problem: { status: 403 } })
expect(asked).toHaveLength(1)
})
it("add a contact point from today, each purpose once", async () => {
const { service, asked } = recording()
const now = new Date("2026-11-02T23:30:00.000Z")
await addContact(
service,
scope,
{
partyId: "x",
kind: "phone",
value: " +370 600 00000 ",
purposes: ["billing", "billing"],
isPrimary: false,
requestId: "r5",
},
now,
)
expect(asked).toEqual([
[
"addContact",
{
partyId: "x",
kind: "phone",
value: "+370 600 00000",
purposes: ["billing"],
isPrimary: false,
effectiveFrom: "2026-11-02",
key: requestKey(scope, "r5"),
},
],
])
expect(dayOf(now)).toBe("2026-11-02")
})
it("hand a read handler a view without a single write", () => {
expect(Object.keys(crmReadsOf(recording().service)).sort()).toEqual([
"bindingId",
"getParty",
"listContacts",
"listParties",
])
})
})