@arcforgelabs/flow 2026.10.53-1 → 2026.10.53-2
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/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server/core/sending.js +13 -1
- package/server/db/schema.js +2 -1
- package/server/ui/invoicing.js +16 -0
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcforgelabs/flow",
|
|
3
|
-
"version": "2026.10.53-
|
|
3
|
+
"version": "2026.10.53-2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@arcforgelabs/flow",
|
|
9
|
-
"version": "2026.10.53-
|
|
9
|
+
"version": "2026.10.53-2",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@clerk/backend": "3.25.0",
|
package/package.json
CHANGED
package/server/core/sending.js
CHANGED
|
@@ -439,7 +439,8 @@ export async function sendQueued(db, organisationId, { accounting, limit = WORKE
|
|
|
439
439
|
summary.waiting += 1;
|
|
440
440
|
} else {
|
|
441
441
|
const error = result.error ?? { code: `send_${result.outcome}`, message: `Sending ended as ${result.outcome}.` };
|
|
442
|
-
|
|
442
|
+
const xero = xeroMatch(error);
|
|
443
|
+
await settle(db, organisationId, result.invoiceId, { sendState: 'failed', sendError: { code: error.code, message: error.message, ...(error.short ? { short: error.short } : {}), ...(xero ? { xero } : {}) } });
|
|
443
444
|
summary.failed += 1;
|
|
444
445
|
summary.refused = [...new Set([...(summary.refused ?? []), error.code])];
|
|
445
446
|
const fact = refusalFact(known.get(result.invoiceId), error);
|
|
@@ -450,6 +451,17 @@ export async function sendQueued(db, organisationId, { accounting, limit = WORKE
|
|
|
450
451
|
return summary;
|
|
451
452
|
}
|
|
452
453
|
|
|
454
|
+
// The Xero invoice a refusal matched, kept so the page can link to it.
|
|
455
|
+
// Only an id, a number and a status. Anything else in the error stays out.
|
|
456
|
+
function xeroMatch(error) {
|
|
457
|
+
const xero = error?.details?.xero;
|
|
458
|
+
if (!xero || typeof xero !== 'object') return null;
|
|
459
|
+
if (!INVOICE_ID.test(xero.invoiceId ?? '')) return null;
|
|
460
|
+
const number = INVOICE_NUMBER.test(xero.number ?? '') ? xero.number : null;
|
|
461
|
+
const status = /^[A-Z0-9_]{1,32}$/.test(xero.status ?? '') ? xero.status : null;
|
|
462
|
+
return { invoiceId: xero.invoiceId, ...(number ? { number } : {}), ...(status ? { status } : {}) };
|
|
463
|
+
}
|
|
464
|
+
|
|
453
465
|
function settle(db, organisationId, invoiceId, fields) {
|
|
454
466
|
return db.update(invoices).set({ ...fields, sendUpdatedAt: sql`now()` })
|
|
455
467
|
.where(and(eq(invoices.organisationId, organisationId), eq(invoices.id, invoiceId), eq(invoices.sendState, 'queued')));
|
package/server/db/schema.js
CHANGED
|
@@ -112,7 +112,8 @@ export const invoices = pgTable('invoices', {
|
|
|
112
112
|
readyAt: timestamp('ready_at', { withTimezone: true }),
|
|
113
113
|
readyBy: uuid('ready_by').references(() => people.id, { onDelete: 'set null' }),
|
|
114
114
|
// Sending to the accounting system (server/core/sending.js): queued by a
|
|
115
|
-
// person, then sent or failed. `sendError` is { code, message
|
|
115
|
+
// person, then sent or failed. `sendError` is { code, message, short?, xero? }
|
|
116
|
+
// while failed. `xero` is the matched invoice { invoiceId, number, status }.
|
|
116
117
|
sendState: text('send_state'),
|
|
117
118
|
sendError: jsonb('send_error'),
|
|
118
119
|
sendRequestedBy: uuid('send_requested_by').references(() => people.id, { onDelete: 'set null' }),
|
package/server/ui/invoicing.js
CHANGED
|
@@ -1423,6 +1423,21 @@ function issuedView(invoice, ui, context, redraw) {
|
|
|
1423
1423
|
? h('button', { class: 'fx-btn', type: 'button', onclick: () => act('revise', `${invoice.number} is a draft again.`) }, 'Revise') : null;
|
|
1424
1424
|
const preview = previewButton(ui, invoice.id);
|
|
1425
1425
|
actionBar = h('div', { class: 'fx-ph__actions' }, [voidButton, reviseButton, preview, send].filter(Boolean));
|
|
1426
|
+
// The same problem notice as a draft. A matched Xero invoice gets the
|
|
1427
|
+
// same Open in Xero link as the Xero card (go.xero.com, reconcile.js).
|
|
1428
|
+
const found = invoice.problems ?? [];
|
|
1429
|
+
const shorts = found.length && found.every((p) => p.short) ? [...new Set(found.map((p) => p.short))].join(', ') : null;
|
|
1430
|
+
const problemNotice = found.length
|
|
1431
|
+
? ui.notice([...new Set(found.map((p) => brief(p.message)))].join(' '), 'danger', 'Problem.', { short: shorts })
|
|
1432
|
+
: null;
|
|
1433
|
+
const matched = found.find((p) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(p.xero?.invoiceId ?? ''));
|
|
1434
|
+
const openXero = matched ? h('a', {
|
|
1435
|
+
class: 'fx-btn fx-btn--sm fx-btn--ghost',
|
|
1436
|
+
href: `https://go.xero.com/AccountsReceivable/View.aspx?InvoiceID=${encodeURIComponent(matched.xero.invoiceId)}`,
|
|
1437
|
+
target: '_blank',
|
|
1438
|
+
rel: 'noopener',
|
|
1439
|
+
}, 'Open in Xero') : null;
|
|
1440
|
+
const problemRow = problemNotice ? (openXero ? h('div', { class: 'fx-offer' }, problemNotice, openXero) : problemNotice) : null;
|
|
1426
1441
|
const header = h('header', { class: 'fx-ph' },
|
|
1427
1442
|
h('div', { class: 'fx-ph__text' },
|
|
1428
1443
|
h('h1', { class: 'fx-title' }, invoice.number),
|
|
@@ -1449,6 +1464,7 @@ function issuedView(invoice, ui, context, redraw) {
|
|
|
1449
1464
|
chrome.record,
|
|
1450
1465
|
chrome.pager,
|
|
1451
1466
|
header,
|
|
1467
|
+
problemRow,
|
|
1452
1468
|
h('div', { class: 'fx-invoice__grid' },
|
|
1453
1469
|
h('div', { class: 'fx-invoice__main' },
|
|
1454
1470
|
h('section', { class: 'fx-card fx-doc' },
|