@opsrev/acculynx-cli 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -140,6 +140,34 @@ JOB_ID=$(acculynx jobs list --limit 1 | jq -r '.[0].id')
140
140
  acculynx jobs get "$JOB_ID" | jq .
141
141
  ```
142
142
 
143
+ ### Monitoring scans: payments with strict coverage
144
+
145
+ Scan every job in a set of milestones and fetch each one's received payments in
146
+ one process, with a coverage line a script can check:
147
+
148
+ ```bash
149
+ acculynx jobs scan --milestones Approved,Completed,Invoiced \
150
+ --enrich payments --strict --format jsonl --out payments.jsonl
151
+ ```
152
+
153
+ Each job line carries `payments`: the job's full received-payment objects
154
+ (`id`, `from`, `amount`, `paymentDate`, `checkNumber`, `notes`, …). The last
155
+ line is the coverage receipt:
156
+
157
+ ```json
158
+ {"type":"coverage","scanned":299,"serverCount":299,"complete":true,"pageError":null,"enrichErrors":0}
159
+ ```
160
+
161
+ - `--strict` (implied by `--enrich payments`) treats any pagination anomaly
162
+ (changing total, duplicate job IDs, overfull or malformed pages) as a partial
163
+ scan. Partial scans exit 3 and report `complete: false`. Never treat them as
164
+ a full inventory.
165
+ - A per-job payments failure doesn't abort the scan. It appears in that job's
166
+ `errors` (`{"jobId", "source": "payments", "message"}`) and counts toward
167
+ `enrichErrors`, so retry just those jobs with `acculynx jobs payments <id>`.
168
+ - Without `--strict` and without `payments`, `jsonl` output is unchanged: job
169
+ lines only.
170
+
143
171
  ## Development
144
172
 
145
173
  ```bash
@@ -179,6 +179,9 @@ async function paginate(client, path, params, limit = DEFAULT_LIMIT) {
179
179
  if (data.items.length < DEFAULT_PAGE_SIZE) {
180
180
  break;
181
181
  }
182
+ if (typeof data.count === "number" && pageStartIndex + DEFAULT_PAGE_SIZE >= data.count) {
183
+ break;
184
+ }
182
185
  pageStartIndex += DEFAULT_PAGE_SIZE;
183
186
  }
184
187
  return results;
package/dist/index.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  resolveTradeTypeIds,
41
41
  resolveWorkTypeId,
42
42
  usersList
43
- } from "./chunk-MQDU6DIH.js";
43
+ } from "./chunk-STEYTGZE.js";
44
44
 
45
45
  // src/index.ts
46
46
  import { createRequire } from "module";
@@ -139,7 +139,7 @@ async function scanJobs(client, filters, options = {}) {
139
139
  });
140
140
  return { jobs, scanned: fetched.length, serverCount, complete, ...pageError ? { pageError } : {} };
141
141
  }
142
- var ENRICHERS = ["financials", "reps", "dates", "messages"];
142
+ var ENRICHERS = ["financials", "reps", "dates", "messages", "payments"];
143
143
  var asRecord = (v) => v !== null && typeof v === "object" ? v : {};
144
144
  async function defaultLoadMessagesTool() {
145
145
  const { createRequire: createRequire2 } = await import("module");
@@ -201,6 +201,12 @@ async function enrichJobs(client, jobs, enrich, concurrency = 5, deps = {}) {
201
201
  by: String(msg.createdBy ?? ""),
202
202
  text: String(msg.message ?? "").replace(/\s+/g, " ").trim().slice(0, 200)
203
203
  }));
204
+ } else if (source === "payments") {
205
+ const received = asRecord(asRecord(await jobPayments(client, jobId)).receivedPayments);
206
+ if (!Array.isArray(received.payments) || received.payments.some((p) => p === null || typeof p !== "object")) {
207
+ throw new Error("invalid_payments");
208
+ }
209
+ entry.payments = received.payments;
204
210
  }
205
211
  } catch (error) {
206
212
  entry.errors.push({ jobId, source, message: error instanceof Error ? error.message : String(error) });
@@ -288,7 +294,7 @@ function jobLine(entry, enrich) {
288
294
  const date = mmdd(job.milestoneDate);
289
295
  let line = `${id8} | ${name} \u2014 ${street}, ${city} | ${milestone} ${date}`;
290
296
  const erroredSources = new Set(entry.errors.map((e) => e.source));
291
- for (const source of ["reps", "financials", "dates"]) {
297
+ for (const source of ["reps", "financials", "dates", "payments"]) {
292
298
  if (!enrich.includes(source)) continue;
293
299
  if (erroredSources.has(source)) {
294
300
  line += ` | ERR:${source}`;
@@ -305,6 +311,10 @@ function jobLine(entry, enrich) {
305
311
  } else if (source === "dates") {
306
312
  const approved = entry.dates?.find((d) => d.name === "Approved");
307
313
  line += ` | appr\u2019d ${approved ? mmdd(approved.date) : "-"}`;
314
+ } else if (source === "payments") {
315
+ const payments = entry.payments ?? [];
316
+ const total = payments.reduce((sum, p) => sum + (typeof p.amount === "number" ? p.amount : 0), 0);
317
+ line += ` | paid ${money(total)} (${payments.length})`;
308
318
  }
309
319
  }
310
320
  return line;
@@ -331,8 +341,20 @@ function formatScanDigest(report) {
331
341
  }
332
342
  return lines.join("\n");
333
343
  }
334
- function formatScanJsonl(report) {
335
- return report.jobs.map((j) => JSON.stringify(j)).join("\n");
344
+ function coverageLine(report) {
345
+ return JSON.stringify({
346
+ type: "coverage",
347
+ scanned: report.scanned,
348
+ serverCount: report.serverCount ?? null,
349
+ complete: report.complete,
350
+ pageError: report.pageError ?? null,
351
+ enrichErrors: report.jobs.reduce((sum, j) => sum + j.errors.length, 0)
352
+ });
353
+ }
354
+ function formatScanJsonl(report, options = {}) {
355
+ const lines = report.jobs.map((j) => JSON.stringify(j));
356
+ if (options.receipt) lines.push(coverageLine(report));
357
+ return lines.join("\n");
336
358
  }
337
359
 
338
360
  // src/ops/scan-marketing.ts
@@ -415,7 +437,7 @@ function registerJobsCommands(parentCmd, getClient2) {
415
437
  });
416
438
  console.log(JSON.stringify(result));
417
439
  });
418
- jobs.command("scan").description("Fetch ALL matching jobs and print a digest with a coverage receipt").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--end-date <date>", "End date (YYYY-MM-DD)").option("--date-filter-type <type>", "Date field to filter on").option("--milestones <milestones>", "Filter by milestones (comma-separated)").option("--assignment <type>", "Filter by assignment: assigned, unassigned").option("--trade-type <name>", "Client-side trade-type filter (repeatable)", collect, []).option("--enrich <list>", "Comma-separated: financials,reps,dates,messages").option("--format <fmt>", "digest, jsonl, or marketing-evidence (private operator data)", "digest").option("--out <path>", "Also write full jsonl to this file").action(async (opts) => {
440
+ jobs.command("scan").description("Fetch ALL matching jobs and print a digest with a coverage receipt").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--end-date <date>", "End date (YYYY-MM-DD)").option("--date-filter-type <type>", "Date field to filter on").option("--milestones <milestones>", "Filter by milestones (comma-separated)").option("--assignment <type>", "Filter by assignment: assigned, unassigned").option("--trade-type <name>", "Client-side trade-type filter (repeatable)", collect, []).option("--enrich <list>", "Comma-separated: financials,reps,dates,messages,payments").option("--format <fmt>", "digest, jsonl, or marketing-evidence (private operator data)", "digest").option("--out <path>", "Also write full jsonl to this file").option("--strict", 'Strict coverage: any pagination anomaly makes the scan partial (exit 3), and jsonl output ends with a {"type":"coverage"} line. Implied by --enrich payments').action(async (opts) => {
419
441
  const enrich = String(opts.enrich ?? "").split(",").map((s) => s.trim()).filter(Boolean);
420
442
  const badEnricher = enrich.find((e) => !ENRICHERS.includes(e));
421
443
  if (badEnricher) {
@@ -441,15 +463,17 @@ function registerJobsCommands(parentCmd, getClient2) {
441
463
  assignment: opts.assignment,
442
464
  tradeType: opts.tradeType.length ? opts.tradeType : void 0
443
465
  };
444
- const result = await scanJobs(getClient2(), filters, { strict: opts.format === "marketing-evidence" });
466
+ const strict = Boolean(opts.strict) || opts.format === "marketing-evidence" || enrich.includes("payments");
467
+ const result = await scanJobs(getClient2(), filters, { strict });
445
468
  const enrichedJobs = await enrichJobs(getClient2(), result.jobs, enrich);
446
469
  const report = { filters, enrich, jobs: enrichedJobs, scanned: result.scanned, serverCount: result.serverCount, complete: result.complete, ...result.pageError ? { pageError: result.pageError } : {} };
447
470
  const evidence = opts.format === "marketing-evidence" ? marketingEvidence(report) : null;
448
- console.log(evidence ? JSON.stringify(evidence) : opts.format === "jsonl" ? formatScanJsonl(report) : formatScanDigest(report));
471
+ const jsonl = formatScanJsonl(report, { receipt: strict });
472
+ console.log(evidence ? JSON.stringify(evidence) : opts.format === "jsonl" ? jsonl : formatScanDigest(report));
449
473
  if (!result.complete || evidence && (!evidence.coverage.queryComplete || !evidence.coverage.enrichmentComplete)) process.exitCode = 3;
450
474
  if (opts.out) {
451
475
  try {
452
- writeFileSync(opts.out, formatScanJsonl(report) + "\n");
476
+ writeFileSync(opts.out, jsonl + "\n");
453
477
  } catch (error) {
454
478
  console.error(`out: FAILED - ${error instanceof Error ? error.message : String(error)}`);
455
479
  }
package/dist/mcp.js CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  resolveTradeTypeIds,
39
39
  resolveWorkTypeId,
40
40
  usersList
41
- } from "./chunk-MQDU6DIH.js";
41
+ } from "./chunk-STEYTGZE.js";
42
42
 
43
43
  // src/mcp.ts
44
44
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opsrev/acculynx-cli",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "AccuLynx MCP server & CLI — use the AccuLynx roofing CRM in Claude Desktop, Cursor, and any MCP client. Also a JSON CLI for scripts and AI agents.",
5
5
  "repository": {
6
6
  "type": "git",