fix: make CRM detail and stale counts reliable
This commit is contained in:
@@ -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>> = {
|
||||
|
||||
Reference in New Issue
Block a user