import assert from "node:assert/strict"; import test from "node:test"; import { DEFAULT_CONFIG } from "../src/config.js"; import { CrmService } from "../src/crm-service.js"; import type { NocoBaseClient } from "../src/nocobase-client.js"; test("renders compact customer rows, labels, relations, timezone, and pagination", async () => { const requests: Array> = []; const fake = { listRecords: async (resource: string, options: Record) => { requests.push({ resource, ...options }); return { data: [ { id: 42, f_q2q60q7bqbg: "Acme", kh_progress: "kh_p_inprogress", f_225h6n6v654: "Referral", kehu_followup_person: [{ id: 7, nickname: "Alice" }], updatedAt: "2026-07-08T09:00:00Z", }, ], meta: { count: 1 }, }; }, } as unknown as NocoBaseClient; const config = structuredClone(DEFAULT_CONFIG); config.customer.resource = "crm_customers"; config.customer.fields = [ "id", "f_q2q60q7bqbg", "kh_progress", "f_225h6n6v654", "kehu_followup_person", "updatedAt", ]; config.customer.statusField = "kh_progress"; config.customer.sourceField = "f_225h6n6v654"; config.customer.ownerField = "kehu_followup_person"; const result = await new CrmService(config, fake).listCustomers({ limit: 20 }); assert.deepEqual(result.results, [ { id: 42, name: "Acme", status: "进行中", source: "Referral", owners: [{ id: 7, name: "Alice" }], updatedAt: "2026-07-08T17:00:00+08:00", }, ]); assert.equal(result.total, 1); assert.equal(result.timezone, "Asia/Shanghai"); assert.equal(requests[0]?.resource, "crm_customers"); }); test("implements exact arbitrary offsets across two NocoBase pages", async () => { const pages: number[] = []; const fake = { listRecords: async (_resource: string, options: Record) => { const page = options.page as number; pages.push(page); const start = (page - 1) * 20; return { data: Array.from({ length: 20 }, (_, index) => ({ id: start + index, name: `Customer ${start + index}`, })), meta: { count: 100 }, }; }, } as unknown as NocoBaseClient; const result = await new CrmService(structuredClone(DEFAULT_CONFIG), fake).listCustomers({ limit: 20, offset: 25, }); assert.deepEqual(pages, [2, 3]); assert.deepEqual( (result.results as Array>).map((row) => row.id), Array.from({ length: 20 }, (_, i) => 25 + i), ); assert.equal(result.next_offset, 45); }); test("resolves owner candidates and returns ambiguity as structured data", async () => { const fake = { listRecords: async (resource: string) => { if (resource === "users") { return { data: [ { id: 1, username: "alex", nickname: "Alex A" }, { id: 2, username: "alex", nickname: "Alex B" }, ], }; } return { data: [], meta: { count: 0 } }; }, } as unknown as NocoBaseClient; const service = new CrmService(structuredClone(DEFAULT_CONFIG), fake); const resolved = await service.resolveOwner("alex"); assert.equal(resolved.count, 2); const listed = await service.listCustomers({ owner: "alex" }); assert.equal((listed.error as Record).code, "owner_ambiguous"); }); test("health never returns the Authorize Key", async () => { const fake = { health: async () => ({ ok: true, endpoint: "https://nb.example", status: "authenticated" }), } as unknown as NocoBaseClient; const result = await new CrmService(structuredClone(DEFAULT_CONFIG), fake).crmHealth(); assert.equal(result.ok, true); assert.doesNotMatch(JSON.stringify(result), /token|secret|authorizeKey/i); }); test("all CRM service surfaces execute against the direct HTTP adapter contract", async () => { const config = structuredClone(DEFAULT_CONFIG); const rows: Record> = { [config.customer.resource]: { id: 1, name: "Acme", owner: [{ id: 10, nickname: "Alice" }], updatedAt: "2026-01-01T00:00:00Z", }, [config.contact.resource]: { id: 2, name: "Bob", customer: [{ id: 1, name: "Acme" }] }, [config.followup.resource]: { id: 3, followedAt: "2026-01-02T00:00:00Z", customer: [{ id: 1, name: "Acme" }], owner: [{ id: 10, nickname: "Alice" }], content: "Followed up", }, [config.user.resource]: { id: 10, username: "alice", nickname: "Alice", [config.team.userStatusField]: config.team.formalUserStatusValue, }, [config.team.resource]: { id: 20, [config.team.nameField]: "Alpha", [config.team.managerField]: [{ id: 10 }], [config.team.memberField]: [{ id: 10 }], }, [config.project.resource]: { id: 30, [config.project.nameField]: "Project", [config.project.progressField]: config.project.progressValues.已报价, [config.project.customerRelationField]: [{ id: 1, name: "Acme" }], }, [config.project.updateResource]: { id: 31, [config.project.updateDateField]: "2026-01-03T00:00:00Z", [config.project.updateContentField]: "Updated", createdBy: [{ id: 10, nickname: "Alice" }], }, }; const fake = { health: async () => ({ ok: true, endpoint: "https://nb.example", status: "authenticated" }), listCollectionFields: async () => ({ data: [] }), getRecord: async (resource: string) => ({ data: rows[resource] }), listRecords: async (resource: string) => ({ data: rows[resource] ? [rows[resource]] : [], meta: { count: rows[resource] ? 1 : 0 }, }), queryRecords: async (_resource: string, options: Record) => { const dimensions = options.dimensions as Array> | undefined; const alias = dimensions?.[0]?.alias; return { data: [{ ...(typeof alias === "string" ? { [alias]: "value" } : {}), count: 1 }] }; }, } as unknown as NocoBaseClient; const service = new CrmService(config, fake); const results = await Promise.all([ service.crmHealth(), service.crmSchema(true), service.crmConfigTemplate(), service.listCustomers(), service.getCustomer(1), service.listContacts(), service.listFollowups(), service.listProjects(), service.getProject(30), service.resolveOwner("alice"), service.listTeams({ includeMembers: true }), service.resolveTeam("Alpha"), service.getTeam(20), service.crmOverview(), service.crmFilterOptions("alice"), service.teamCrmOverview("Alpha"), service.getProjectTimeline(30), service.listStaleCustomers({ asOf: "2026-08-15", inactiveDays: 30 }), ]); assert.equal(results.length, 18); assert.ok(results.every((result) => !("error" in result))); });