@arcforgelabs/flow 2026.10.53-2 → 2026.10.53-3

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.10.53-2",
3
+ "version": "2026.10.53-3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@arcforgelabs/flow",
9
- "version": "2026.10.53-2",
9
+ "version": "2026.10.53-3",
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.10.53-2",
3
+ "version": "2026.10.53-3",
4
4
  "description": "Invoicing, bill entry and a financial dashboard for labs, with Seazona and Xero integrations.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -124,6 +124,20 @@ export async function reconcileDays(db, organisationId, { integrations, from, to
124
124
  and e.kind in ('observed', 'source_changed') and e.actor <> ${HISTORY_ACTOR})`,
125
125
  ));
126
126
 
127
+ // A billable feed cannot prove absence. Check the remaining ledger refs
128
+ // against the source copy without billing filters. Older adapters retain
129
+ // the feed comparison until they declare workJobPresence.
130
+ const present = new Set(work.keys());
131
+ for (const adapter of workAdapters) {
132
+ if (!adapter.capabilities.workJobPresence) continue;
133
+ const jobRefs = [...new Set(obligations
134
+ .filter((o) => o.sourceSystem === adapter.id && !present.has(`${adapter.id}:${o.sourceJobRef}`))
135
+ .map((o) => o.sourceJobRef))];
136
+ for (let i = 0; i < jobRefs.length; i += FIND_BATCH) {
137
+ const { presentRefs } = await adapter.capabilities.workJobPresence({ organisationId, jobRefs: jobRefs.slice(i, i + FIND_BATCH) });
138
+ for (const ref of presentRefs) present.add(`${adapter.id}:${ref}`);
139
+ }
140
+ }
127
141
  const reported = new Set(workAdapters.map((a) => a.id));
128
142
  const inLedger = new Set();
129
143
  for (const o of obligations) {
@@ -139,7 +153,7 @@ export async function reconcileDays(db, organisationId, { integrations, from, to
139
153
  if (reported.has(o.sourceSystem)) {
140
154
  const key = `${o.sourceSystem}:${o.sourceJobRef}`;
141
155
  inLedger.add(key);
142
- if (!work.has(key)) entry.differences.missingInWork.push({ id: o.id, cents: o.totalCents });
156
+ if (!present.has(key)) entry.differences.missingInWork.push({ id: o.id, cents: o.totalCents });
143
157
  }
144
158
  }
145
159
  for (const [key, job] of work) {
@@ -24,6 +24,7 @@ export const CAPABILITIES = Object.freeze([
24
24
  'pullPayments', // payments since a cursor, matched to invoices and bills
25
25
  'checkInvoice', // does the accounting system hold an invoice for a billable job? (the ledger's liveCheck)
26
26
  'billableJobs', // billable jobs by day from the work system's copy in Flow, for the daily reconcile; never a live call
27
+ 'workJobPresence', // { organisationId, jobRefs } -> { presentRefs }: requested jobs still present in the work copy, without status, date or line filters; deleted jobs are absent; never a live call
27
28
  'jobReferenceFields', // { patient }: the fields a job gives the invoice Reference (server/core/references.js)
28
29
  'jobLink', // { url, exact } or null: the link to one job in the work system's own web app, for "Open in" on a job's problem; never a live call
29
30
  'listWork', // the work system's jobs from its copy in Flow, open and lately finished, for the Jobs board; a job may carry `pendingMove`
@@ -4,7 +4,7 @@ import { createSeazonaClient } from './client.js';
4
4
  import { listClients, listProducts, listWork, readBillableWork } from './feed.js';
5
5
  import { historyJobIssues, historySummary, readHistoryWork } from './history/index.js';
6
6
  import { readSeazonaSyncHealth, runSeazonaSync } from './lanes.js';
7
- import { readBillableJobs } from './reconcile.js';
7
+ import { readBillableJobs, readWorkJobPresence } from './reconcile.js';
8
8
  import { SEAZONA_CALL_GUARD } from './governor.js';
9
9
  import { createSeazonaRouter } from './routes.js';
10
10
  import {
@@ -200,6 +200,10 @@ export function createSeazonaAdapter({ env = null, fetchImpl = null, now = Date.
200
200
  return readBillableJobs(current().db, { from, to });
201
201
  },
202
202
 
203
+ async workJobPresence({ jobRefs }) {
204
+ return readWorkJobPresence(current().db, { jobRefs });
205
+ },
206
+
203
207
  // Seazona clients by id and company name, for the identity map. Reads
204
208
  // Flow's copy only.
205
209
  async listCustomers() {
@@ -1,4 +1,16 @@
1
+ import { and, inArray, isNull } from 'drizzle-orm';
1
2
  import { readBillableWork } from './feed.js';
3
+ import { seazonaOrders } from './schema.js';
4
+
5
+ // Presence is not billing eligibility. Keep a job that returned to an open
6
+ // status, lost its lines, or changed its date separate from a missing job.
7
+ export async function readWorkJobPresence(db, { jobRefs }) {
8
+ if (!jobRefs.length) return { presentRefs: [] };
9
+ const rows = await db.select({ ref: seazonaOrders.id }).from(seazonaOrders).where(and(
10
+ inArray(seazonaOrders.id, jobRefs), isNull(seazonaOrders.deletedInSourceAt),
11
+ ));
12
+ return { presentRefs: rows.map((row) => row.ref) };
13
+ }
2
14
 
3
15
  // The billable jobs in Flow's copy of Seazona, by day, behind the port's
4
16
  // `billableJobs` (the daily reconcile, server/core/reconcile.js). It walks the