fix: make CRM detail and stale counts reliable

This commit is contained in:
nac
2026-08-20 06:24:04 +00:00
parent 7cc521938f
commit 6b5b3a413a
2 changed files with 245 additions and 36 deletions
+148
View File
@@ -143,6 +143,154 @@ test("health never returns the Authorize Key", async () => {
assert.doesNotMatch(JSON.stringify(result), /token|secret|authorizeKey/i);
});
test("getProject returns contacts only at the top level when requested", async () => {
const config = structuredClone(DEFAULT_CONFIG);
const requests: Array<Record<string, unknown>> = [];
const fake = {
getRecord: async (_resource: string, _projectId: unknown, options: Record<string, unknown>) => {
requests.push(options);
return {
data: {
[config.project.idField]: 30,
[config.project.nameField]: "Project",
[config.project.contactField]: [{ [config.contact.idField]: 2, name: "Contact" }],
},
};
},
} as unknown as NocoBaseClient;
const service = new CrmService(config, fake);
const withoutContacts = await service.getProject(30, {
includeContacts: false,
includeFollowups: false,
includeUpdates: false,
});
assert.equal("contacts" in withoutContacts, false);
assert.equal("contacts" in (withoutContacts.project as Record<string, unknown>), false);
assert.equal(
((requests[0]?.fields ?? []) as string[]).some(
(field) => field === config.project.contactField || field.startsWith(`${config.project.contactField}.`),
),
false,
);
const withContacts = await service.getProject(30, {
includeContacts: true,
includeFollowups: false,
includeUpdates: false,
});
assert.deepEqual(withContacts.contacts, [{ id: 2, name: "Contact" }]);
assert.equal("contacts" in (withContacts.project as Record<string, unknown>), false);
assert.equal(
((requests[1]?.fields ?? []) as string[]).some((field) =>
field.startsWith(`${config.project.contactField}.`),
),
true,
);
});
test("listStaleCustomers uses paginated max aggregation and returns an exact total", async () => {
const config = structuredClone(DEFAULT_CONFIG);
const customers = Array.from({ length: 101 }, (_, index) => ({
[config.customer.idField]: index + 1,
name: `Customer ${index + 1}`,
}));
const latest = customers.map((customer) => ({
customer_id: customer[config.customer.idField],
last_followup_at: "2026-01-01T00:00:00Z",
}));
const queryRequests: Array<Record<string, unknown>> = [];
const fake = {
listRecords: async (resource: string, options: Record<string, unknown>) => {
assert.equal(resource, config.customer.resource);
const pageSize = options.pageSize as number;
const start = ((options.page as number) - 1) * pageSize;
return { data: customers.slice(start, start + pageSize), meta: { count: customers.length } };
},
queryRecords: async (resource: string, options: Record<string, unknown>) => {
assert.equal(resource, config.followup.resource);
queryRequests.push(options);
const offset = options.offset as number;
const limit = options.limit as number;
return { data: latest.slice(offset, offset + limit) };
},
} as unknown as NocoBaseClient;
const result = await new CrmService(config, fake).listStaleCustomers({
asOf: "2026-08-20",
inactiveDays: 30,
limit: 20,
});
assert.equal(result.total, 101);
assert.equal(result.total_is_exact, true);
assert.deepEqual(
queryRequests.map((request) => request.offset),
[0, 100],
);
assert.deepEqual(queryRequests[0]?.dimensions, [
{
field: [config.followup.customerField, config.customer.idField],
alias: "customer_id",
},
]);
assert.deepEqual(queryRequests[0]?.measures, [
{ field: [config.followup.dateField], aggregation: "max", alias: "last_followup_at" },
]);
assert.equal(queryRequests[0]?.timezone, config.timezone);
});
test("listStaleCustomers never presents a truncated scan count as an exact total", async () => {
const config = structuredClone(DEFAULT_CONFIG);
const fake = {
listRecords: async () => ({
data: [{ [config.customer.idField]: 1, name: "Customer" }],
meta: { count: 2 },
}),
queryRecords: async () => ({ data: [] }),
} as unknown as NocoBaseClient;
const result = await new CrmService(config, fake).listStaleCustomers({
asOf: "2026-08-20",
inactiveDays: 30,
});
assert.equal("total" in result, false);
assert.equal(result.total_is_exact, false);
assert.equal(result.scanned_stale_count, 1);
assert.equal(result.pagination_scope, "scanned_records_only");
assert.deepEqual(result.data_quality_warnings, ["customer scan was truncated"]);
});
test("listStaleCustomers marks an overflowing follow-up aggregation as inexact", async () => {
const config = structuredClone(DEFAULT_CONFIG);
const latest = Array.from({ length: 5_001 }, (_, index) => ({
customer_id: index + 1,
last_followup_at: "2026-01-01T00:00:00Z",
}));
const fake = {
listRecords: async () => ({
data: [{ [config.customer.idField]: 1, name: "Customer" }],
meta: { count: 1 },
}),
queryRecords: async (_resource: string, options: Record<string, unknown>) => {
const offset = options.offset as number;
const limit = options.limit as number;
return { data: latest.slice(offset, offset + limit) };
},
} as unknown as NocoBaseClient;
const result = await new CrmService(config, fake).listStaleCustomers({
asOf: "2026-08-20",
inactiveDays: 30,
});
assert.equal("total" in result, false);
assert.equal(result.total_is_exact, false);
assert.equal(result.scanned_stale_count, 1);
assert.deepEqual(result.data_quality_warnings, ["follow-up customer aggregation was truncated"]);
assert.deepEqual(result.truncation, {
followups: { minimum_total: 5_001, returned: 5_000, limit: 5_000, next_offset: 5_000 },
});
});
test("all CRM service surfaces execute against the direct HTTP adapter contract", async () => {
const config = structuredClone(DEFAULT_CONFIG);
const rows: Record<string, Record<string, unknown>> = {