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", ]) }) })