@arcforgelabs/flow 2026.10.30 → 2026.10.32
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 +2 -0
- package/npm-shrinkwrap.json +188 -2
- package/package.json +2 -1
- package/server/api/alerts.js +49 -0
- package/server/api/invoicing.js +125 -7
- package/server/app.js +29 -2
- package/server/core/alerts.js +157 -0
- package/server/core/automations.js +17 -3
- package/server/core/history.js +65 -0
- package/server/core/invoice-pdf.js +266 -0
- package/server/core/invoices.js +2 -2
- package/server/core/invoicing-rules.js +26 -1
- package/server/core/job-board.js +25 -3
- package/server/core/ledger.js +3 -2
- package/server/core/line-mapping.js +36 -10
- package/server/core/pipeline.js +76 -4
- package/server/core/price-list.js +348 -0
- package/server/core/references.js +99 -0
- package/server/core/sending.js +11 -5
- package/server/db/commands.js +42 -0
- package/server/db/doctor.js +168 -0
- package/server/db/migrations/0014_price_list.sql +34 -0
- package/server/db/migrations/0015_alert_dismissals.sql +17 -0
- package/server/db/migrations/0016_invoice_reference.sql +1 -0
- package/server/db/migrations/meta/0014_snapshot.json +3773 -0
- package/server/db/migrations/meta/0015_snapshot.json +3854 -0
- package/server/db/migrations/meta/0016_snapshot.json +3861 -0
- package/server/db/migrations/meta/_journal.json +21 -0
- package/server/db/schema.js +46 -0
- package/server/flow-server.js +20 -1
- package/server/index.js +2 -1
- package/server/integrations/call-guard.js +362 -0
- package/server/integrations/port.js +21 -1
- package/server/integrations/seazona/feed.js +31 -2
- package/server/integrations/seazona/governor.js +50 -122
- package/server/integrations/seazona/history/backfill.js +208 -0
- package/server/integrations/seazona/history/csv.js +65 -0
- package/server/integrations/seazona/history/index.js +584 -0
- package/server/integrations/seazona/history/read.js +196 -0
- package/server/integrations/seazona/index.js +46 -1
- package/server/integrations/seazona/lanes.js +187 -51
- package/server/integrations/seazona/migrations/0003_call_guard.sql +2 -0
- package/server/integrations/seazona/migrations/0004_history_import.sql +68 -0
- package/server/integrations/seazona/migrations/meta/0003_snapshot.json +895 -0
- package/server/integrations/seazona/migrations/meta/0004_snapshot.json +1323 -0
- package/server/integrations/seazona/migrations/meta/_journal.json +14 -0
- package/server/integrations/seazona/patients.js +30 -0
- package/server/integrations/seazona/routes.js +145 -0
- package/server/integrations/seazona/schema.js +94 -0
- package/server/integrations/seazona/settings.js +14 -3
- package/server/integrations/seazona/sync-once.js +1 -1
- package/server/integrations/seazona/tables.js +4 -0
- package/server/integrations/xero/client.js +39 -7
- package/server/integrations/xero/drafts.js +46 -8
- package/server/integrations/xero/import.js +131 -17
- package/server/integrations/xero/index.js +38 -7
- package/server/integrations/xero/lookup.js +26 -0
- package/server/integrations/xero/migrations/0006_xero_call_guard.sql +26 -0
- package/server/integrations/xero/migrations/0007_xero_webhooks.sql +30 -0
- package/server/integrations/xero/migrations/meta/_journal.json +14 -0
- package/server/integrations/xero/mirror.js +27 -10
- package/server/integrations/xero/oauth.js +4 -1
- package/server/integrations/xero/reconcile.js +173 -0
- package/server/integrations/xero/routes.js +55 -0
- package/server/integrations/xero/runtime.js +45 -4
- package/server/integrations/xero/settings.js +3 -0
- package/server/integrations/xero/webhooks.js +311 -0
- package/server/release-channel.js +3 -3
- package/server/ui/alerts.js +77 -0
- package/server/ui/app-page.js +1 -1
- package/server/ui/app.js +63 -21
- package/server/ui/clerk-appearance.js +42 -9
- package/server/ui/flow.css +78 -9
- package/server/ui/invoice-notes.js +26 -0
- package/server/ui/invoicing-settings.js +4 -2
- package/server/ui/invoicing.js +241 -49
- package/server/ui/line-mappings.js +3 -2
- package/server/ui/price-list.js +247 -0
- package/server/ui/routes.js +1 -1
package/README.md
CHANGED
|
@@ -37,8 +37,10 @@ run a lab.
|
|
|
37
37
|
| `docs/INVOICING_RULES.md` | The invoicing rules that draft a job's invoice, and the two ways an invoice goes to Xero |
|
|
38
38
|
| `docs/INVOICE_RULES.md` | Invoice rules a lab builds (When, If, Then) that make a ready job wait or never draft |
|
|
39
39
|
| `docs/INVOICE_SENDING.md` | Drafts, Problem and Ready invoices, Mark ready, and Send all ready to the accounting system |
|
|
40
|
+
| `docs/ALERTS.md` | Quiet alerts: the slim line on a page when something there needs a person, and how a person dismisses it |
|
|
40
41
|
| `docs/AUTOMATIONS.md` | Automations: Flow routines that mark jobs ready to invoice and draft them, each on or off |
|
|
41
42
|
| `docs/LINE_MAPPING.md` | Line mapping: how each work system line finds its price item, and Price codes in Settings |
|
|
43
|
+
| `docs/PRICE_LIST.md` | The price list: Flow's own codes and prices, their links to Xero items and Seazona products, and the price check |
|
|
42
44
|
| `docs/RELEASING.md` | Versions and channels, the release workflows, how to cut and roll back a release, and the one-time setup |
|
|
43
45
|
| `docs/SELF_HOSTING.md` | Running a Flow release with Docker, Node or Replit, the update notice, and Clerk sign-in |
|
|
44
46
|
| `docs/REPLIT.md` | A Replit workspace of this repository that follows a release channel (`FLOW_DEPLOY_REF`) |
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcforgelabs/flow",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.32",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@arcforgelabs/flow",
|
|
9
|
-
"version": "2026.10.
|
|
9
|
+
"version": "2026.10.32",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@clerk/backend": "3.23.2",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"drizzle-orm": "0.45.3",
|
|
26
26
|
"express": "5.3.0",
|
|
27
27
|
"express-rate-limit": "8.7.0",
|
|
28
|
+
"pdfkit": "0.20.2",
|
|
28
29
|
"pg": "8.23.1"
|
|
29
30
|
},
|
|
30
31
|
"bin": {
|
|
@@ -656,6 +657,30 @@
|
|
|
656
657
|
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
|
|
657
658
|
"license": "MIT"
|
|
658
659
|
},
|
|
660
|
+
"node_modules/@noble/ciphers": {
|
|
661
|
+
"version": "1.3.0",
|
|
662
|
+
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
|
|
663
|
+
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
|
|
664
|
+
"license": "MIT",
|
|
665
|
+
"engines": {
|
|
666
|
+
"node": "^14.21.3 || >=16"
|
|
667
|
+
},
|
|
668
|
+
"funding": {
|
|
669
|
+
"url": "https://paulmillr.com/funding/"
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
"node_modules/@noble/hashes": {
|
|
673
|
+
"version": "1.8.0",
|
|
674
|
+
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
|
675
|
+
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
|
676
|
+
"license": "MIT",
|
|
677
|
+
"engines": {
|
|
678
|
+
"node": "^14.21.3 || >=16"
|
|
679
|
+
},
|
|
680
|
+
"funding": {
|
|
681
|
+
"url": "https://paulmillr.com/funding/"
|
|
682
|
+
}
|
|
683
|
+
},
|
|
659
684
|
"node_modules/@opentelemetry/api": {
|
|
660
685
|
"version": "1.9.1",
|
|
661
686
|
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
|
|
@@ -1100,6 +1125,15 @@
|
|
|
1100
1125
|
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
|
1101
1126
|
"license": "MIT"
|
|
1102
1127
|
},
|
|
1128
|
+
"node_modules/@swc/helpers": {
|
|
1129
|
+
"version": "0.5.23",
|
|
1130
|
+
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
|
|
1131
|
+
"integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
|
|
1132
|
+
"license": "Apache-2.0",
|
|
1133
|
+
"dependencies": {
|
|
1134
|
+
"tslib": "^2.8.0"
|
|
1135
|
+
}
|
|
1136
|
+
},
|
|
1103
1137
|
"node_modules/@tanstack/query-core": {
|
|
1104
1138
|
"version": "5.104.4",
|
|
1105
1139
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.104.4.tgz",
|
|
@@ -1132,6 +1166,26 @@
|
|
|
1132
1166
|
"node": "18 || 20 || >=22"
|
|
1133
1167
|
}
|
|
1134
1168
|
},
|
|
1169
|
+
"node_modules/base64-js": {
|
|
1170
|
+
"version": "1.5.1",
|
|
1171
|
+
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
|
1172
|
+
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
|
1173
|
+
"funding": [
|
|
1174
|
+
{
|
|
1175
|
+
"type": "github",
|
|
1176
|
+
"url": "https://github.com/sponsors/feross"
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
"type": "patreon",
|
|
1180
|
+
"url": "https://www.patreon.com/feross"
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
"type": "consulting",
|
|
1184
|
+
"url": "https://feross.org/support"
|
|
1185
|
+
}
|
|
1186
|
+
],
|
|
1187
|
+
"license": "MIT"
|
|
1188
|
+
},
|
|
1135
1189
|
"node_modules/body-parser": {
|
|
1136
1190
|
"version": "2.3.0",
|
|
1137
1191
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
|
|
@@ -1168,6 +1222,15 @@
|
|
|
1168
1222
|
"node": "20 || >=22"
|
|
1169
1223
|
}
|
|
1170
1224
|
},
|
|
1225
|
+
"node_modules/brotli": {
|
|
1226
|
+
"version": "1.3.3",
|
|
1227
|
+
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
|
|
1228
|
+
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
|
|
1229
|
+
"license": "MIT",
|
|
1230
|
+
"dependencies": {
|
|
1231
|
+
"base64-js": "^1.1.2"
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1171
1234
|
"node_modules/buffer-from": {
|
|
1172
1235
|
"version": "1.1.2",
|
|
1173
1236
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
|
@@ -1213,6 +1276,15 @@
|
|
|
1213
1276
|
"url": "https://github.com/sponsors/ljharb"
|
|
1214
1277
|
}
|
|
1215
1278
|
},
|
|
1279
|
+
"node_modules/clone": {
|
|
1280
|
+
"version": "2.1.2",
|
|
1281
|
+
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
|
1282
|
+
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
|
1283
|
+
"license": "MIT",
|
|
1284
|
+
"engines": {
|
|
1285
|
+
"node": ">=0.8"
|
|
1286
|
+
}
|
|
1287
|
+
},
|
|
1216
1288
|
"node_modules/content-disposition": {
|
|
1217
1289
|
"version": "2.0.1",
|
|
1218
1290
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz",
|
|
@@ -1292,6 +1364,12 @@
|
|
|
1292
1364
|
"node": ">=6"
|
|
1293
1365
|
}
|
|
1294
1366
|
},
|
|
1367
|
+
"node_modules/dfa": {
|
|
1368
|
+
"version": "1.2.0",
|
|
1369
|
+
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
|
|
1370
|
+
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
|
|
1371
|
+
"license": "MIT"
|
|
1372
|
+
},
|
|
1295
1373
|
"node_modules/dotenv": {
|
|
1296
1374
|
"version": "17.4.2",
|
|
1297
1375
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
|
@@ -1627,12 +1705,24 @@
|
|
|
1627
1705
|
"express": ">= 4.11"
|
|
1628
1706
|
}
|
|
1629
1707
|
},
|
|
1708
|
+
"node_modules/fast-deep-equal": {
|
|
1709
|
+
"version": "3.1.3",
|
|
1710
|
+
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
|
1711
|
+
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
|
1712
|
+
"license": "MIT"
|
|
1713
|
+
},
|
|
1630
1714
|
"node_modules/fast-sha256": {
|
|
1631
1715
|
"version": "1.3.0",
|
|
1632
1716
|
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
|
1633
1717
|
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
|
1634
1718
|
"license": "Unlicense"
|
|
1635
1719
|
},
|
|
1720
|
+
"node_modules/fflate": {
|
|
1721
|
+
"version": "0.8.3",
|
|
1722
|
+
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
|
|
1723
|
+
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
|
1724
|
+
"license": "MIT"
|
|
1725
|
+
},
|
|
1636
1726
|
"node_modules/finalhandler": {
|
|
1637
1727
|
"version": "2.1.1",
|
|
1638
1728
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
|
@@ -1654,6 +1744,23 @@
|
|
|
1654
1744
|
"url": "https://opencollective.com/express"
|
|
1655
1745
|
}
|
|
1656
1746
|
},
|
|
1747
|
+
"node_modules/fontkit": {
|
|
1748
|
+
"version": "2.0.4",
|
|
1749
|
+
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
|
|
1750
|
+
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
|
|
1751
|
+
"license": "MIT",
|
|
1752
|
+
"dependencies": {
|
|
1753
|
+
"@swc/helpers": "^0.5.12",
|
|
1754
|
+
"brotli": "^1.3.2",
|
|
1755
|
+
"clone": "^2.1.2",
|
|
1756
|
+
"dfa": "^1.2.0",
|
|
1757
|
+
"fast-deep-equal": "^3.1.3",
|
|
1758
|
+
"restructure": "^3.0.0",
|
|
1759
|
+
"tiny-inflate": "^1.0.3",
|
|
1760
|
+
"unicode-properties": "^1.4.0",
|
|
1761
|
+
"unicode-trie": "^2.0.0"
|
|
1762
|
+
}
|
|
1763
|
+
},
|
|
1657
1764
|
"node_modules/forwarded": {
|
|
1658
1765
|
"version": "0.2.0",
|
|
1659
1766
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
|
@@ -1886,6 +1993,25 @@
|
|
|
1886
1993
|
"integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==",
|
|
1887
1994
|
"license": "MIT"
|
|
1888
1995
|
},
|
|
1996
|
+
"node_modules/linebreak": {
|
|
1997
|
+
"version": "1.1.0",
|
|
1998
|
+
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
|
|
1999
|
+
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
|
|
2000
|
+
"license": "MIT",
|
|
2001
|
+
"dependencies": {
|
|
2002
|
+
"base64-js": "0.0.8",
|
|
2003
|
+
"unicode-trie": "^2.0.0"
|
|
2004
|
+
}
|
|
2005
|
+
},
|
|
2006
|
+
"node_modules/linebreak/node_modules/base64-js": {
|
|
2007
|
+
"version": "0.0.8",
|
|
2008
|
+
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
|
|
2009
|
+
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
|
|
2010
|
+
"license": "MIT",
|
|
2011
|
+
"engines": {
|
|
2012
|
+
"node": ">= 0.4"
|
|
2013
|
+
}
|
|
2014
|
+
},
|
|
1889
2015
|
"node_modules/lru-cache": {
|
|
1890
2016
|
"version": "11.5.3",
|
|
1891
2017
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
|
|
@@ -2078,6 +2204,12 @@
|
|
|
2078
2204
|
"@oxc-parser/binding-win32-x64-msvc": "0.152.0"
|
|
2079
2205
|
}
|
|
2080
2206
|
},
|
|
2207
|
+
"node_modules/pako": {
|
|
2208
|
+
"version": "0.2.9",
|
|
2209
|
+
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
|
2210
|
+
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
|
|
2211
|
+
"license": "MIT"
|
|
2212
|
+
},
|
|
2081
2213
|
"node_modules/parseurl": {
|
|
2082
2214
|
"version": "1.3.3",
|
|
2083
2215
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
|
@@ -2113,6 +2245,20 @@
|
|
|
2113
2245
|
"url": "https://opencollective.com/express"
|
|
2114
2246
|
}
|
|
2115
2247
|
},
|
|
2248
|
+
"node_modules/pdfkit": {
|
|
2249
|
+
"version": "0.20.2",
|
|
2250
|
+
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.20.2.tgz",
|
|
2251
|
+
"integrity": "sha512-Q/w03ICAQyXfHNfTsg1udp0ADerdBN0s7a6XSPL7J7Ro6ABnafoBOCDBstIO9U02mvzHEV8HrvFIw5iV5ejIRA==",
|
|
2252
|
+
"license": "MIT",
|
|
2253
|
+
"dependencies": {
|
|
2254
|
+
"@noble/ciphers": "^1.3.0",
|
|
2255
|
+
"@noble/hashes": "^1.8.0",
|
|
2256
|
+
"fflate": "^0.8.3",
|
|
2257
|
+
"fontkit": "^2.0.4",
|
|
2258
|
+
"linebreak": "^1.1.0",
|
|
2259
|
+
"png-js": "^2.0.0"
|
|
2260
|
+
}
|
|
2261
|
+
},
|
|
2116
2262
|
"node_modules/pg": {
|
|
2117
2263
|
"version": "8.23.1",
|
|
2118
2264
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz",
|
|
@@ -2202,6 +2348,14 @@
|
|
|
2202
2348
|
"split2": "^4.1.0"
|
|
2203
2349
|
}
|
|
2204
2350
|
},
|
|
2351
|
+
"node_modules/png-js": {
|
|
2352
|
+
"version": "2.0.0",
|
|
2353
|
+
"resolved": "https://registry.npmjs.org/png-js/-/png-js-2.0.0.tgz",
|
|
2354
|
+
"integrity": "sha512-GdzJuUMc6ZSpxFJWVxtOH1bzYHym+TOnveqUjb+VJIbZWbZzyiRGFiKhbiielfpYbgMlhHVhsJ0FTazfuRFkMA==",
|
|
2355
|
+
"dependencies": {
|
|
2356
|
+
"fflate": "^0.8.2"
|
|
2357
|
+
}
|
|
2358
|
+
},
|
|
2205
2359
|
"node_modules/postgres-array": {
|
|
2206
2360
|
"version": "2.0.0",
|
|
2207
2361
|
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
|
@@ -2312,6 +2466,12 @@
|
|
|
2312
2466
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
|
2313
2467
|
}
|
|
2314
2468
|
},
|
|
2469
|
+
"node_modules/restructure": {
|
|
2470
|
+
"version": "3.0.2",
|
|
2471
|
+
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
|
|
2472
|
+
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
|
|
2473
|
+
"license": "MIT"
|
|
2474
|
+
},
|
|
2315
2475
|
"node_modules/router": {
|
|
2316
2476
|
"version": "2.2.0",
|
|
2317
2477
|
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
|
@@ -2533,6 +2693,12 @@
|
|
|
2533
2693
|
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
|
2534
2694
|
}
|
|
2535
2695
|
},
|
|
2696
|
+
"node_modules/tiny-inflate": {
|
|
2697
|
+
"version": "1.0.3",
|
|
2698
|
+
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
|
2699
|
+
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
|
|
2700
|
+
"license": "MIT"
|
|
2701
|
+
},
|
|
2536
2702
|
"node_modules/toidentifier": {
|
|
2537
2703
|
"version": "1.0.1",
|
|
2538
2704
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
|
@@ -3069,6 +3235,26 @@
|
|
|
3069
3235
|
"url": "https://opencollective.com/express"
|
|
3070
3236
|
}
|
|
3071
3237
|
},
|
|
3238
|
+
"node_modules/unicode-properties": {
|
|
3239
|
+
"version": "1.4.1",
|
|
3240
|
+
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
|
|
3241
|
+
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
|
|
3242
|
+
"license": "MIT",
|
|
3243
|
+
"dependencies": {
|
|
3244
|
+
"base64-js": "^1.3.0",
|
|
3245
|
+
"unicode-trie": "^2.0.0"
|
|
3246
|
+
}
|
|
3247
|
+
},
|
|
3248
|
+
"node_modules/unicode-trie": {
|
|
3249
|
+
"version": "2.0.0",
|
|
3250
|
+
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
|
|
3251
|
+
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
|
|
3252
|
+
"license": "MIT",
|
|
3253
|
+
"dependencies": {
|
|
3254
|
+
"pako": "^0.2.5",
|
|
3255
|
+
"tiny-inflate": "^1.0.0"
|
|
3256
|
+
}
|
|
3257
|
+
},
|
|
3072
3258
|
"node_modules/unpipe": {
|
|
3073
3259
|
"version": "1.0.0",
|
|
3074
3260
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcforgelabs/flow",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.32",
|
|
4
4
|
"description": "Invoicing, bill entry and a financial dashboard for labs, with Seazona and Xero integrations.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"drizzle-orm": "0.45.3",
|
|
52
52
|
"express": "5.3.0",
|
|
53
53
|
"express-rate-limit": "8.7.0",
|
|
54
|
+
"pdfkit": "0.20.2",
|
|
54
55
|
"pg": "8.23.1"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { ALERT_PAGES, alertForBrowser, alertsFor, dismissAlert } from '../core/alerts.js';
|
|
3
|
+
import { FlowError } from '../core/errors.js';
|
|
4
|
+
import { resolveOrganisation } from './invoicing.js';
|
|
5
|
+
|
|
6
|
+
// Quiet alerts for signed-in people (server/core/alerts.js, docs/ALERTS.md).
|
|
7
|
+
//
|
|
8
|
+
// GET /api/alerts?page=invoices[&scope=][&organisationId=] what needs this person on that page;
|
|
9
|
+
// `scope` names one part of it (an integration id on page integrations)
|
|
10
|
+
// POST /api/alerts/dismiss { key[, organisationId] } hide one alert for this person
|
|
11
|
+
//
|
|
12
|
+
// `sources` maps each page to the functions that find its alerts.
|
|
13
|
+
export function createAlertsRouter({ db, personAuth, sources, log = console }) {
|
|
14
|
+
const router = express.Router();
|
|
15
|
+
router.use(express.json({ limit: '8kb' }));
|
|
16
|
+
|
|
17
|
+
const handle = (fn) => async (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
await fn(req, res);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (!(error instanceof FlowError)) {
|
|
22
|
+
log.error?.(`[alerts] ${error.message}`);
|
|
23
|
+
return res.status(500).json({ ok: false, code: 'internal', error: 'Something went wrong.' });
|
|
24
|
+
}
|
|
25
|
+
const status = { not_found: 404, organisation_required: 409 }[error.code] ?? 400;
|
|
26
|
+
return res.status(status).json({ ok: false, code: error.code, error: error.message });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
router.get('/', personAuth.requireSession, handle(async (req, res) => {
|
|
31
|
+
const page = typeof req.query.page === 'string' ? req.query.page : '';
|
|
32
|
+
if (!ALERT_PAGES.includes(page)) throw new FlowError('invalid', `page must be one of ${ALERT_PAGES.join(', ')}`);
|
|
33
|
+
const organisationId = await resolveOrganisation(db, req.query.organisationId);
|
|
34
|
+
const scope = typeof req.query.scope === 'string' && req.query.scope ? req.query.scope : null;
|
|
35
|
+
if (scope && !/^[a-z][a-z0-9-]{1,31}$/.test(scope)) throw new FlowError('invalid', 'scope must be an integration id');
|
|
36
|
+
const alerts = await alertsFor(db, organisationId, req.person, { page, scope, sources, withHidden: true });
|
|
37
|
+
res.set('Cache-Control', 'no-store');
|
|
38
|
+
// `danger`: an alert of that tone is open, even one this person hid.
|
|
39
|
+
res.json({ ok: true, alerts: alerts.filter((a) => !a.hidden).map(alertForBrowser), danger: alerts.some((a) => a.tone === 'danger') });
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
router.post('/dismiss', personAuth.requireSession, handle(async (req, res) => {
|
|
43
|
+
const organisationId = await resolveOrganisation(db, req.body?.organisationId ?? req.query.organisationId);
|
|
44
|
+
const result = await dismissAlert(db, organisationId, req.person, { key: req.body?.key, sources });
|
|
45
|
+
res.json({ ok: true, ...result });
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
return router;
|
|
49
|
+
}
|
package/server/api/invoicing.js
CHANGED
|
@@ -13,10 +13,16 @@ import {
|
|
|
13
13
|
} from '../core/line-mapping.js';
|
|
14
14
|
import {
|
|
15
15
|
effectiveSettings, INVOICING_DEFAULTS, readCustomerBilling, readInvoicingSettings,
|
|
16
|
-
saveCustomerBilling, saveInvoicingSettings,
|
|
16
|
+
saveCustomerBilling, saveInvoicingSettings, sendModeFor, sendModeSummary,
|
|
17
17
|
} from '../core/invoicing-rules.js';
|
|
18
18
|
import { boardJobs, describeObligations } from '../core/job-board.js';
|
|
19
|
+
import { jobReference } from '../core/references.js';
|
|
20
|
+
import {
|
|
21
|
+
acceptFindings, archivePriceItem, checkPrices, createPriceItem, linkPriceItem, readCatalogues, readPriceList, unlinkPriceItem, updatePriceItem,
|
|
22
|
+
} from '../core/price-list.js';
|
|
19
23
|
import { draftInvoiceForObligation, explainObligation, listObligations, setReady } from '../core/ledger.js';
|
|
24
|
+
import { isHistoryHeld } from '../core/history.js';
|
|
25
|
+
import { invoicePdfDocument, pdfFields, pdfFilename, renderInvoicePdf } from '../core/invoice-pdf.js';
|
|
20
26
|
import { invoiceView, invoiceViews, markNotReady, markReady, sendAgain, sendAllReady } from '../core/sending.js';
|
|
21
27
|
import {
|
|
22
28
|
billingObligations, contacts, invoices, INVOICE_STATUSES, organisations, priceItems, TAX_CODES,
|
|
@@ -31,9 +37,12 @@ import {
|
|
|
31
37
|
// POST /invoices/mark-ready { invoiceIds }: a person checked these drafts; one with a problem stays
|
|
32
38
|
// POST /invoices/mark-not-ready { invoiceIds }: back to To check
|
|
33
39
|
// POST /invoices/send-ready owner or admin: issue every ready draft and queue it for the
|
|
34
|
-
// accounting system { issueDate }
|
|
40
|
+
// accounting system { issueDate, invoiceIds? (only these, each ready) }
|
|
35
41
|
// POST /invoices/send-again owner or admin: queue failed sends again { invoiceIds? }
|
|
36
|
-
// GET /invoices/:id one invoice: lines, contact, job, issued versions
|
|
42
|
+
// GET /invoices/:id one invoice: lines, contact, job, issued versions, and where it sends
|
|
43
|
+
// GET /invoices/:id/pdf?version= the invoice as a PDF, drawn by Flow (server/core/invoice-pdf.js),
|
|
44
|
+
// with the accounting system's number and dates once it holds it;
|
|
45
|
+
// no accounting system calls
|
|
37
46
|
// POST /invoices a new draft
|
|
38
47
|
// PATCH /invoices/:id change a draft
|
|
39
48
|
// POST /invoices/:id/issue owner or admin: number it and keep the document as sent
|
|
@@ -58,6 +67,15 @@ import {
|
|
|
58
67
|
// POST /line-mappings owner or admin: a new mapping
|
|
59
68
|
// PUT /line-mappings/:id owner or admin: change some of a mapping
|
|
60
69
|
// DELETE /line-mappings/:id owner or admin
|
|
70
|
+
// GET /price-list the price list with its links, the connected systems' records with
|
|
71
|
+
// no link, and what differs (server/core/price-list.js)
|
|
72
|
+
// POST /price-list/check owner or admin: run the price check now (no calls to other systems)
|
|
73
|
+
// POST /price-items owner or admin: a new price item { code, name, unitAmountCents, taxCode, ... }
|
|
74
|
+
// PUT /price-items/:id owner or admin: change some of a price item
|
|
75
|
+
// POST /price-items/:id/archive owner or admin: { archived } archive it, or bring it back
|
|
76
|
+
// POST /price-items/:id/links owner or admin: { system, externalId } link a record of a system's list
|
|
77
|
+
// POST /price-items/:id/resolve owner or admin: Mark resolved: check again, accept what still differs
|
|
78
|
+
// DELETE /price-links/:id owner or admin: remove a link
|
|
61
79
|
// GET /automations?place= Flow's routines, each on or off (server/core/automations.js)
|
|
62
80
|
// PUT /automations/:key owner or admin: { enabled, config }
|
|
63
81
|
// GET /work-statuses the work system's job statuses, with job counts
|
|
@@ -233,6 +251,11 @@ async function invoiceDetail(db, organisationId, invoiceId, accounting = null) {
|
|
|
233
251
|
return {
|
|
234
252
|
...invoice,
|
|
235
253
|
...(await invoiceView(db, organisationId, invoice, { accounting })),
|
|
254
|
+
// Where a draft goes when it is sent, and how: this customer's send mode.
|
|
255
|
+
sendsTo: accounting?.name ?? null,
|
|
256
|
+
sendMode: accounting && invoice.status === 'draft'
|
|
257
|
+
? { mode: await sendModeFor(db, organisationId, invoice.contactId), otherCustomers: 0, ...(await accounting.capabilities.sendStatus({ organisationId })) }
|
|
258
|
+
: null,
|
|
236
259
|
contact: contact ?? null,
|
|
237
260
|
job: jobs.get(invoiceId) ?? null,
|
|
238
261
|
versions: versions.map(({ version, issuedAt, issuedBy }) => ({ version, issuedAt, issuedBy })),
|
|
@@ -240,7 +263,7 @@ async function invoiceDetail(db, organisationId, invoiceId, accounting = null) {
|
|
|
240
263
|
}
|
|
241
264
|
|
|
242
265
|
export function createInvoicingRouter({
|
|
243
|
-
db, personAuth, log = console, liveCheckFor = () => null, accounting = () => null, work = () => [],
|
|
266
|
+
db, personAuth, log = console, liveCheckFor = () => null, accounting = () => null, work = () => [], priceSystems = () => [],
|
|
244
267
|
}) {
|
|
245
268
|
const router = express.Router();
|
|
246
269
|
router.use(express.json({ limit: '64kb' }));
|
|
@@ -302,6 +325,7 @@ export function createInvoicingRouter({
|
|
|
302
325
|
})),
|
|
303
326
|
counts,
|
|
304
327
|
sendsTo: accounting()?.name ?? null,
|
|
328
|
+
sendMode: accounting() ? { ...(await sendModeSummary(db, organisationId)), ...(await accounting().capabilities.sendStatus({ organisationId })) } : null,
|
|
305
329
|
});
|
|
306
330
|
}));
|
|
307
331
|
|
|
@@ -330,7 +354,8 @@ export function createInvoicingRouter({
|
|
|
330
354
|
router.post('/invoices/send-ready', ...manager, handle(log, async (req, res) => {
|
|
331
355
|
const organisationId = await organisationOf(req);
|
|
332
356
|
const issueDate = optionalDate(req.body?.issueDate, 'issueDate') ?? today();
|
|
333
|
-
const
|
|
357
|
+
const ids = req.body?.invoiceIds === undefined ? null : invoiceIdsOf(req.body);
|
|
358
|
+
const result = await sendAllReady(db, organisationId, { accounting: accounting(), personId: req.person.id, issueDate, invoiceIds: ids });
|
|
334
359
|
log.info?.(`[invoicing] organisation ${organisationId}: person ${req.person.id} sent all ready: ${JSON.stringify(result)}`);
|
|
335
360
|
res.json({ ok: true, ...result });
|
|
336
361
|
}));
|
|
@@ -349,6 +374,22 @@ export function createInvoicingRouter({
|
|
|
349
374
|
res.json({ ok: true, invoice: await invoiceDetail(db, organisationId, invoiceId, accounting()) });
|
|
350
375
|
}));
|
|
351
376
|
|
|
377
|
+
router.get('/invoices/:id/pdf', ...signedIn, handle(log, async (req, res) => {
|
|
378
|
+
const invoiceId = pathId(req.params.id, 'invoice');
|
|
379
|
+
const organisationId = await organisationOf(req);
|
|
380
|
+
const version = req.query.version === undefined ? null : Number(req.query.version);
|
|
381
|
+
if (version !== null && !(Number.isSafeInteger(version) && version > 0)) throw invalid('version must be a whole number above 0');
|
|
382
|
+
const source = await invoicePdfDocument(db, organisationId, invoiceId, { version, accounting: accounting() });
|
|
383
|
+
const bytes = await renderInvoicePdf(source);
|
|
384
|
+
res.set({
|
|
385
|
+
'Content-Type': 'application/pdf',
|
|
386
|
+
'Content-Disposition': `inline; filename="${pdfFilename(pdfFields(source))}"`,
|
|
387
|
+
'Cache-Control': 'no-store',
|
|
388
|
+
'X-Content-Type-Options': 'nosniff',
|
|
389
|
+
});
|
|
390
|
+
res.end(bytes);
|
|
391
|
+
}));
|
|
392
|
+
|
|
352
393
|
router.post('/invoices', ...signedIn, handle(log, async (req, res) => {
|
|
353
394
|
const organisationId = await organisationOf(req);
|
|
354
395
|
const body = req.body ?? {};
|
|
@@ -427,8 +468,13 @@ export function createInvoicingRouter({
|
|
|
427
468
|
const view = text(req.query.view);
|
|
428
469
|
if (view && !VIEWS.includes(view)) throw invalid(`view must be one of ${VIEWS.join(', ')}`);
|
|
429
470
|
const limit = limitOf(req.query.limit, 200, 500);
|
|
430
|
-
const
|
|
471
|
+
const listedAll = await listObligations(db, organisationId, { accounting: Boolean(liveCheckFor(organisationId)) });
|
|
472
|
+
// Held history jobs (core/history.js) are left out unless asked for: they
|
|
473
|
+
// are past work, listed in the history import's own reconciliation.
|
|
474
|
+
const withHistory = req.query.history === 'include';
|
|
475
|
+
const all = withHistory ? listedAll : listedAll.filter((entry) => !isHistoryHeld(entry.obligation));
|
|
431
476
|
const counts = Object.fromEntries(VIEWS.map((v) => [v, 0]));
|
|
477
|
+
counts.history = listedAll.length - listedAll.filter((entry) => !isHistoryHeld(entry.obligation)).length;
|
|
432
478
|
for (const entry of all) counts[entry.view] += 1;
|
|
433
479
|
const listed = (view ? all.filter((entry) => entry.view === view) : all).slice(0, limit);
|
|
434
480
|
res.json({ ok: true, jobs: await describeObligations(db, organisationId, listed, { accounting: accounting() }), counts });
|
|
@@ -449,7 +495,11 @@ export function createInvoicingRouter({
|
|
|
449
495
|
const organisationId = await organisationOf(req);
|
|
450
496
|
// With no accounting system connected, Flow's own records are the check.
|
|
451
497
|
const liveCheck = liveCheckFor(organisationId) ?? null;
|
|
452
|
-
const
|
|
498
|
+
const [obligation] = await db.select().from(billingObligations)
|
|
499
|
+
.where(and(eq(billingObligations.organisationId, organisationId), eq(billingObligations.id, obligationId)));
|
|
500
|
+
const { referenceTemplate: template } = await readInvoicingSettings(db, organisationId);
|
|
501
|
+
const reference = obligation ? await jobReference(db, organisationId, obligation, { template, work: work().find((a) => a.id === obligation.sourceSystem) ?? null }) : null;
|
|
502
|
+
const { invoice } = await draftInvoiceForObligation(db, organisationId, obligationId, { liveCheck, actor: `person:${req.person.id}`, reference });
|
|
453
503
|
log.info?.(`[invoicing] organisation ${organisationId}: person ${req.person.id} drafted invoice ${invoice.id} from job ${obligationId}`);
|
|
454
504
|
res.status(201).json({ ok: true, invoice: await invoiceDetail(db, organisationId, invoice.id) });
|
|
455
505
|
}));
|
|
@@ -602,6 +652,74 @@ export function createInvoicingRouter({
|
|
|
602
652
|
res.json({ ok: true });
|
|
603
653
|
}));
|
|
604
654
|
|
|
655
|
+
// The price list (server/core/price-list.js). Every read is from Flow's own
|
|
656
|
+
// copies of the connected systems' lists.
|
|
657
|
+
const catalogues = (organisationId) => readCatalogues(organisationId, priceSystems(), { log });
|
|
658
|
+
|
|
659
|
+
router.get('/price-list', ...signedIn, handle(log, async (req, res) => {
|
|
660
|
+
const organisationId = await organisationOf(req);
|
|
661
|
+
res.json({ ok: true, ...(await readPriceList(db, organisationId, { catalogues: await catalogues(organisationId) })) });
|
|
662
|
+
}));
|
|
663
|
+
|
|
664
|
+
router.post('/price-list/check', ...manager, handle(log, async (req, res) => {
|
|
665
|
+
const organisationId = await organisationOf(req);
|
|
666
|
+
const summary = await checkPrices(db, organisationId, { catalogues: await catalogues(organisationId), actor: `person:${req.person.id}` });
|
|
667
|
+
res.json({ ok: true, summary });
|
|
668
|
+
}));
|
|
669
|
+
|
|
670
|
+
router.post('/price-items', ...manager, handle(log, async (req, res) => {
|
|
671
|
+
const organisationId = await organisationOf(req);
|
|
672
|
+
const { organisationId: _named, ...raw } = req.body ?? {};
|
|
673
|
+
const item = await createPriceItem(db, organisationId, raw, { personId: req.person.id });
|
|
674
|
+
log.info?.(`[invoicing] organisation ${organisationId}: person ${req.person.id} added price item ${item.id}`);
|
|
675
|
+
res.status(201).json({ ok: true, item });
|
|
676
|
+
}));
|
|
677
|
+
|
|
678
|
+
router.put('/price-items/:id', ...manager, handle(log, async (req, res) => {
|
|
679
|
+
const itemId = pathId(req.params.id, 'price item');
|
|
680
|
+
const organisationId = await organisationOf(req);
|
|
681
|
+
const { organisationId: _named, ...changes } = req.body ?? {};
|
|
682
|
+
const item = await updatePriceItem(db, organisationId, itemId, changes, { personId: req.person.id });
|
|
683
|
+
log.info?.(`[invoicing] organisation ${organisationId}: person ${req.person.id} changed price item ${item.id}`);
|
|
684
|
+
res.json({ ok: true, item });
|
|
685
|
+
}));
|
|
686
|
+
|
|
687
|
+
router.post('/price-items/:id/archive', ...manager, handle(log, async (req, res) => {
|
|
688
|
+
const itemId = pathId(req.params.id, 'price item');
|
|
689
|
+
const organisationId = await organisationOf(req);
|
|
690
|
+
const archived = req.body?.archived ?? true;
|
|
691
|
+
if (typeof archived !== 'boolean') throw invalid('archived must be true or false');
|
|
692
|
+
const item = await archivePriceItem(db, organisationId, itemId, { archived, personId: req.person.id });
|
|
693
|
+
log.info?.(`[invoicing] organisation ${organisationId}: person ${req.person.id} ${archived ? 'archived' : 'restored'} price item ${item.id}`);
|
|
694
|
+
res.json({ ok: true, item });
|
|
695
|
+
}));
|
|
696
|
+
|
|
697
|
+
router.post('/price-items/:id/links', ...manager, handle(log, async (req, res) => {
|
|
698
|
+
const itemId = pathId(req.params.id, 'price item');
|
|
699
|
+
const organisationId = await organisationOf(req);
|
|
700
|
+
const system = text(req.body?.system);
|
|
701
|
+
const externalId = text(req.body?.externalId);
|
|
702
|
+
if (!system || !externalId) throw invalid('system and externalId are required');
|
|
703
|
+
const link = await linkPriceItem(db, organisationId, itemId, { system, externalId }, { catalogues: await catalogues(organisationId), personId: req.person.id });
|
|
704
|
+
log.info?.(`[invoicing] organisation ${organisationId}: person ${req.person.id} linked price item ${itemId} to ${system}`);
|
|
705
|
+
res.status(201).json({ ok: true, link: { id: link.id, system: link.system, externalId: link.externalId, externalCode: link.externalCode, matchedBy: link.matchedBy } });
|
|
706
|
+
}));
|
|
707
|
+
|
|
708
|
+
router.post('/price-items/:id/resolve', ...manager, handle(log, async (req, res) => {
|
|
709
|
+
const itemId = pathId(req.params.id, 'price item');
|
|
710
|
+
const organisationId = await organisationOf(req);
|
|
711
|
+
const result = await acceptFindings(db, organisationId, itemId, { catalogues: await catalogues(organisationId), actor: `person:${req.person.id}` });
|
|
712
|
+
res.json({ ok: true, ...result });
|
|
713
|
+
}));
|
|
714
|
+
|
|
715
|
+
router.delete('/price-links/:id', ...manager, handle(log, async (req, res) => {
|
|
716
|
+
const linkId = pathId(req.params.id, 'link');
|
|
717
|
+
const organisationId = await organisationOf(req);
|
|
718
|
+
await unlinkPriceItem(db, organisationId, linkId);
|
|
719
|
+
log.info?.(`[invoicing] organisation ${organisationId}: person ${req.person.id} removed price link ${linkId}`);
|
|
720
|
+
res.json({ ok: true });
|
|
721
|
+
}));
|
|
722
|
+
|
|
605
723
|
router.get('/settings', ...signedIn, handle(log, async (req, res) => {
|
|
606
724
|
const organisationId = await organisationOf(req);
|
|
607
725
|
res.json({ ok: true, settings: await readInvoicingSettings(db, organisationId), defaults: INVOICING_DEFAULTS });
|