@arcforgelabs/flow 2026.11.8 → 2026.11.9

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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@arcforgelabs/flow",
3
- "version": "2026.11.8",
3
+ "version": "2026.11.9",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@arcforgelabs/flow",
9
- "version": "2026.11.8",
9
+ "version": "2026.11.9",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@clerk/backend": "3.25.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcforgelabs/flow",
3
- "version": "2026.11.8",
3
+ "version": "2026.11.9",
4
4
  "description": "Invoicing, bill entry and a financial dashboard for labs, with Seazona and Xero integrations.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,4 +1,4 @@
1
- import { and, asc, eq, gt, gte, inArray, isNull, like, lt, or, sql } from 'drizzle-orm';
1
+ import { and, asc, desc, eq, gt, gte, inArray, isNotNull, isNull, like, lt, or, sql } from 'drizzle-orm';
2
2
  import {
3
3
  clientListSignature,
4
4
  invoiceListSignature,
@@ -492,6 +492,61 @@ async function readHighWater(db, laneStates, sync, nowMs) {
492
492
 
493
493
  // The sweep lane's run. `helpers` come from lanes.js: readOrder (issue #12
494
494
  // for each order read), isLaneLevel, describeReadFailure, emptyResponse and
495
+ // Orders whose invoice Flow's copy does not hold. The invoice lane reads
496
+ // invoices changed since its first run, so an invoice last changed before
497
+ // that was never read, and its job took the order's products alone: no
498
+ // Shipping/Handling or Rush, which are service lines on the invoice. Seazona
499
+ // gives an order's invoice the order's own id. The sweep reads it and keeps it
500
+ // when its number is the order's; the feed then gives the job the invoice's
501
+ // lines and totals. An order Seazona answers 404 for, or with another number,
502
+ // is noted (`no_invoice`) and not asked again.
503
+ export const ORDER_INVOICE_KIND = 'no_invoice';
504
+ async function readOrderInvoices(ctx, helpers, found) {
505
+ const { db, client, sync, at } = ctx;
506
+ const rows = await db.select({ id: seazonaOrders.id, invoiceNumber: seazonaOrders.invoiceNumber })
507
+ .from(seazonaOrders)
508
+ .leftJoin(seazonaInvoices, eq(seazonaInvoices.invoiceNumber, seazonaOrders.invoiceNumber))
509
+ .leftJoin(seazonaSweepFindings, and(
510
+ eq(seazonaSweepFindings.resource, 'orders'),
511
+ eq(seazonaSweepFindings.recordKey, seazonaOrders.id),
512
+ eq(seazonaSweepFindings.kind, ORDER_INVOICE_KIND),
513
+ ))
514
+ .where(and(
515
+ isNull(seazonaOrders.deletedInSourceAt),
516
+ isNotNull(seazonaOrders.invoiceNumber),
517
+ isNull(seazonaInvoices.invoiceNumber),
518
+ isNull(seazonaSweepFindings.recordKey),
519
+ ))
520
+ .orderBy(desc(seazonaOrders.lastChangedAt), seazonaOrders.id)
521
+ .limit(CONFIRM_BATCH);
522
+ for (const row of rows) {
523
+ if (!client.hasCall()) throw new SweepBudgetSpent();
524
+ let detail;
525
+ try {
526
+ detail = await client.get(`invoices/${encodeURIComponent(row.id)}`);
527
+ } catch (error) {
528
+ if (error instanceof SweepBudgetSpent || helpers.isLaneLevel(error)) throw error;
529
+ if (error?.statusCode === 404) {
530
+ await recordFinding(db, 'orders', row.id, ORDER_INVOICE_KIND, { at, detail: { status: 404 } });
531
+ continue;
532
+ }
533
+ // Any other failure: try again on a later run.
534
+ return;
535
+ }
536
+ const invoice = detail && typeof detail === 'object' && !Array.isArray(detail)
537
+ ? mapInvoice({}, detail, { keepPatientReference: sync.keepPatientReference }) : null;
538
+ if (!invoice || invoice.invoiceNumber !== row.invoiceNumber) {
539
+ await recordFinding(db, 'orders', row.id, ORDER_INVOICE_KIND, { at, detail: { otherNumber: Boolean(invoice?.invoiceNumber) } });
540
+ continue;
541
+ }
542
+ const summary = await upsertRecords(db, 'invoices', [invoice], { at });
543
+ if (summary.created) {
544
+ found.created += 1;
545
+ await recordFinding(db, 'invoices', invoice.invoiceNumber, 'created', { at, detail: { lastModified: invoice.lastModifiedText } });
546
+ }
547
+ }
548
+ }
549
+
495
550
  // LaneError.
496
551
  export async function runSweep(ctx, helpers) {
497
552
  const { db, sync, at, state } = ctx;
@@ -506,6 +561,7 @@ export async function runSweep(ctx, helpers) {
506
561
  const save = () => ({ ...cursor, budget: { day: today, calls: spentToday + budget.spent } });
507
562
 
508
563
  try {
564
+ await readOrderInvoices(sweepCtx, helpers, found);
509
565
  for (const kind of SWEEP_KINDS) {
510
566
  if (budget.left <= 0) break;
511
567
  if (!isDue(cursor[kind], at, sync, kind)) continue;
@@ -557,7 +613,7 @@ export async function readSweepHealth(db, settings, { now = Date.now } = {}) {
557
613
  .groupBy(seazonaSweepFindings.resource, seazonaSweepFindings.kind);
558
614
  const foundLastWeek = {};
559
615
  for (const row of counts) {
560
- if (row.kind === 'missing') continue;
616
+ if (row.kind === 'missing' || row.kind === ORDER_INVOICE_KIND) continue;
561
617
  foundLastWeek[row.resource] = { ...foundLastWeek[row.resource], [row.kind]: row.records };
562
618
  }
563
619
  const missingRows = await db.select({ resource: seazonaSweepFindings.resource, key: seazonaSweepFindings.recordKey })