@arcforgelabs/flow 2026.11.5 → 2026.11.6

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.
Files changed (36) hide show
  1. package/npm-shrinkwrap.json +2 -2
  2. package/package.json +1 -1
  3. package/server/api/invoicing.js +5 -2
  4. package/server/core/automations.js +4 -1
  5. package/server/core/invoice-rules.js +64 -15
  6. package/server/core/invoicing-rules.js +50 -6
  7. package/server/core/job-board.js +3 -2
  8. package/server/core/ledger.js +10 -2
  9. package/server/core/pipeline.js +5 -2
  10. package/server/core/price-list.js +41 -12
  11. package/server/core/sending.js +3 -2
  12. package/server/db/migrations/0029_job_remake.sql +1 -0
  13. package/server/db/migrations/meta/0029_snapshot.json +5069 -0
  14. package/server/db/migrations/meta/_journal.json +7 -0
  15. package/server/db/schema.js +3 -0
  16. package/server/integrations/port.js +1 -0
  17. package/server/integrations/seazona/feed.js +1 -0
  18. package/server/integrations/seazona/history/index.js +5 -2
  19. package/server/integrations/seazona/lanes.js +3 -0
  20. package/server/integrations/seazona/ledger.js +1 -0
  21. package/server/integrations/seazona/map.js +2 -0
  22. package/server/integrations/seazona/migrations/0008_order_remake.sql +1 -0
  23. package/server/integrations/seazona/migrations/meta/0008_snapshot.json +1526 -0
  24. package/server/integrations/seazona/migrations/meta/_journal.json +7 -0
  25. package/server/integrations/seazona/schema.js +2 -0
  26. package/server/integrations/seazona/store.js +14 -0
  27. package/server/integrations/seazona/sweep.js +3 -0
  28. package/server/integrations/sentry/index.js +9 -4
  29. package/server/integrations/xero/catalogue.js +40 -0
  30. package/server/integrations/xero/drafts.js +16 -5
  31. package/server/integrations/xero/index.js +10 -0
  32. package/server/integrations/xero/lookup.js +11 -4
  33. package/server/integrations/xero/settings.js +5 -0
  34. package/server/ui/monitoring.js +63 -3
  35. package/server/ui/price-list.js +5 -2
  36. package/skills/flow/SKILL.md +31 -1
@@ -204,6 +204,13 @@
204
204
  "when": 1794456064675,
205
205
  "tag": "0028_sharp_power_pack",
206
206
  "breakpoints": true
207
+ },
208
+ {
209
+ "idx": 29,
210
+ "version": "7",
211
+ "when": 1794463202822,
212
+ "tag": "0029_job_remake",
213
+ "breakpoints": true
207
214
  }
208
215
  ]
209
216
  }
@@ -253,6 +253,9 @@ export const billingObligations = pgTable('billing_obligations', {
253
253
  // The work system's status for the job, as it reported it (for example
254
254
  // "Shipped"). The draft rules read it.
255
255
  sourceStatus: text('source_status'),
256
+ // The work system marks the job a remake (Seazona's remake flag): the
257
+ // invoice rule field `remake` reads it.
258
+ remake: boolean('remake').notNull().default(false),
256
259
  // Flow's own stage: the job is ready to invoice, since when and by whom (a
257
260
  // person, or an automation such as one that reads the work system status).
258
261
  readyAt: timestamp('ready_at', { withTimezone: true }),
@@ -37,6 +37,7 @@ export const CAPABILITIES = Object.freeze([
37
37
  'createCustomer', // { organisationId, contactId, actor }: create the system's customer from the Flow contact. Answers { outcome, contact }. Xero, when Create missing clients is on.
38
38
  'findInvoices', // sales invoices in the accounting system's copy, for the matcher; never a live call
39
39
  'readChanges', // { state: fresh | read | unavailable }: the accounting system's latest changes into its copy now, just before Flow drafts or sends (read before act)
40
+ 'itemMatches', // { organisationId, codes } -> the Flow price codes that send as an item the system sells (Xero: linked item, else same code), from its copy; null while not connected or its item list was never read; never a live call
40
41
  'listPriceItems', // the system's own price list from its copy in Flow, for the price list's links and check; never a live call
41
42
  'health', // connection state for Settings
42
43
  'alerts', // what only a person can clear (a refused sign-in, stopped calls), for the Integrations page; reads Flow's data only
@@ -77,6 +77,7 @@ function toFeedEntry({ order, invoice, client, invoiceOrderCount }) {
77
77
  lines: order.lines,
78
78
  subtotalCents: order.subtotalCents,
79
79
  patientReference: order.patientReference,
80
+ remake: order.remake === true,
80
81
  syncedAt: order.lastSyncedAt.toISOString(),
81
82
  invoice: invoice ? {
82
83
  invoiceNumber: invoice.invoiceNumber,
@@ -495,12 +495,13 @@ export async function readHistoryWork(db, { cursor = null, limit = 200 } = {}) {
495
495
  }
496
496
  const pageSize = Math.max(1, Math.min(1000, limit));
497
497
  const changedAt = sql`to_char(${seazonaHistoryJobs.lastChangedAt} at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`;
498
- const rows = await db.select({ job: seazonaHistoryJobs, changedAt })
498
+ const rows = await db.select({ job: seazonaHistoryJobs, changedAt, remake: seazonaOrders.remake })
499
499
  .from(seazonaHistoryJobs)
500
+ .leftJoin(seazonaOrders, eq(seazonaOrders.id, seazonaHistoryJobs.mirrorOrderId))
500
501
  .where(after ? sql`(${seazonaHistoryJobs.lastChangedAt}, ${seazonaHistoryJobs.jobKey}) > (${after.at}::timestamptz, ${after.key})` : undefined)
501
502
  .orderBy(seazonaHistoryJobs.lastChangedAt, seazonaHistoryJobs.jobKey)
502
503
  .limit(pageSize);
503
- const items = rows.map(({ job }) => {
504
+ const items = rows.map(({ job, remake }) => {
504
505
  const sourceJobRef = job.mirrorOrderId ?? job.caseRef ?? `history:${job.jobKey}`;
505
506
  return {
506
507
  source: 'seazona',
@@ -518,6 +519,8 @@ export async function readHistoryWork(db, { cursor = null, limit = 200 } = {}) {
518
519
  shippedOn: job.shippedDate,
519
520
  jobDate: job.jobDate ?? job.dueDate ?? job.shippedDate,
520
521
  sourceStatus: job.status,
522
+ // The order's remake flag, from Flow's copy of the order.
523
+ remake: remake === true,
521
524
  lines: (job.lines ?? []).map(ledgerLine),
522
525
  // The CSV reports a total, but no invoice tax allocation.
523
526
  sourceTotals: job.totalCents == null ? null : { totalCents: job.totalCents },
@@ -28,6 +28,7 @@ import {
28
28
  recordOrderFailure,
29
29
  recordRecordFailure,
30
30
  selectActiveOrders,
31
+ syncRemakeFlags,
31
32
  touchSynced,
32
33
  upsertRecords,
33
34
  upsertReference,
@@ -507,6 +508,8 @@ const laneRuns = {
507
508
  else failures.push(result.failure);
508
509
  }
509
510
  await touchSynced(db, 'orders', unchanged, { at });
511
+ // The remake flag is on the list row: kept with no detail read.
512
+ await syncRemakeFlags(db, new Map(unchanged.map((id) => [id, byId.get(id)?.isRemake === true])), { at });
510
513
  summary.unchanged += unchanged.length;
511
514
  const failureDetails = judgeReads('recent order reads', detailReads, failures);
512
515
  const through = lastDone ?? (Number.isFinite(doneThrough) ? doneThrough : null);
@@ -60,6 +60,7 @@ export function toLedgerJob(item) {
60
60
  jobDate: item.invoice?.invoiceDate ?? item.dueOn ?? item.orderedOn ?? null,
61
61
  // The draft rules bill only the statuses the lab chose (for example Shipped).
62
62
  sourceStatus: item.status ?? null,
63
+ remake: item.remake === true,
63
64
  lines,
64
65
  sourceTotals,
65
66
  observedAt: item.syncedAt,
@@ -136,6 +136,8 @@ export function mapOrder(listItem = {}, detail = null, { keepPatientReference =
136
136
  lines,
137
137
  subtotalCents: lines.length ? sumCents(lines) : null,
138
138
  patientReference: keepPatientReference ? textOrNull(merged.patient) : null,
139
+ // Seazona's own remake flag (the list and the order both carry it).
140
+ remake: merged.isRemake === true,
139
141
  };
140
142
  }
141
143
 
@@ -0,0 +1 @@
1
+ ALTER TABLE "seazona_orders" ADD COLUMN IF NOT EXISTS "remake" boolean DEFAULT false NOT NULL;