@arcforgelabs/flow 2026.10.29 → 2026.10.32-beta.1
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 +110 -3
- package/server/app.js +29 -2
- package/server/core/alerts.js +157 -0
- package/server/core/automations.js +32 -1
- package/server/core/history.js +65 -0
- package/server/core/invoice-pdf.js +266 -0
- package/server/core/invoicing-rules.js +12 -0
- package/server/core/job-board.js +25 -3
- package/server/core/line-mapping.js +36 -10
- package/server/core/pipeline.js +84 -6
- package/server/core/price-list.js +348 -0
- package/server/core/queue.js +22 -0
- package/server/core/sending.js +16 -4
- 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/meta/0014_snapshot.json +3773 -0
- package/server/db/migrations/meta/0015_snapshot.json +3854 -0
- package/server/db/migrations/meta/_journal.json +14 -0
- package/server/db/schema.js +44 -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 +23 -2
- package/server/integrations/seazona/client.js +6 -0
- package/server/integrations/seazona/feed.js +23 -1
- package/server/integrations/seazona/governor.js +74 -21
- 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 +51 -2
- package/server/integrations/seazona/lanes.js +251 -62
- package/server/integrations/seazona/migrations/0002_call_safety.sql +15 -0
- 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/0002_snapshot.json +882 -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 +21 -0
- package/server/integrations/seazona/routes.js +145 -0
- package/server/integrations/seazona/scheduler.js +37 -1
- package/server/integrations/seazona/schema.js +116 -0
- package/server/integrations/seazona/settings.js +24 -3
- package/server/integrations/seazona/store.js +74 -0
- package/server/integrations/seazona/sync-once.js +1 -1
- package/server/integrations/seazona/tables.js +5 -0
- package/server/integrations/xero/client.js +27 -7
- package/server/integrations/xero/drafts.js +46 -8
- package/server/integrations/xero/import.js +125 -16
- package/server/integrations/xero/index.js +36 -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/meta/_journal.json +7 -0
- package/server/integrations/xero/mirror.js +11 -2
- package/server/integrations/xero/oauth.js +4 -1
- package/server/integrations/xero/reconcile.js +173 -0
- package/server/integrations/xero/routes.js +35 -0
- package/server/integrations/xero/runtime.js +45 -4
- 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 +48 -14
- package/server/ui/clerk-appearance.js +42 -9
- package/server/ui/flow.css +31 -2
- package/server/ui/invoicing-settings.js +1 -1
- package/server/ui/invoicing.js +121 -8
- package/server/ui/line-mappings.js +3 -2
- package/server/ui/monitoring.js +14 -0
- 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-beta.1",
|
|
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-beta.1",
|
|
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-beta.1",
|
|
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,15 @@ import {
|
|
|
13
13
|
} from '../core/line-mapping.js';
|
|
14
14
|
import {
|
|
15
15
|
effectiveSettings, INVOICING_DEFAULTS, readCustomerBilling, readInvoicingSettings,
|
|
16
|
-
saveCustomerBilling, saveInvoicingSettings,
|
|
16
|
+
saveCustomerBilling, saveInvoicingSettings, sendModeSummary,
|
|
17
17
|
} from '../core/invoicing-rules.js';
|
|
18
18
|
import { boardJobs, describeObligations } from '../core/job-board.js';
|
|
19
|
+
import {
|
|
20
|
+
acceptFindings, archivePriceItem, checkPrices, createPriceItem, linkPriceItem, readCatalogues, readPriceList, unlinkPriceItem, updatePriceItem,
|
|
21
|
+
} from '../core/price-list.js';
|
|
19
22
|
import { draftInvoiceForObligation, explainObligation, listObligations, setReady } from '../core/ledger.js';
|
|
23
|
+
import { isHistoryHeld } from '../core/history.js';
|
|
24
|
+
import { invoicePdfDocument, pdfFields, pdfFilename, renderInvoicePdf } from '../core/invoice-pdf.js';
|
|
20
25
|
import { invoiceView, invoiceViews, markNotReady, markReady, sendAgain, sendAllReady } from '../core/sending.js';
|
|
21
26
|
import {
|
|
22
27
|
billingObligations, contacts, invoices, INVOICE_STATUSES, organisations, priceItems, TAX_CODES,
|
|
@@ -34,6 +39,9 @@ import {
|
|
|
34
39
|
// accounting system { issueDate }
|
|
35
40
|
// POST /invoices/send-again owner or admin: queue failed sends again { invoiceIds? }
|
|
36
41
|
// GET /invoices/:id one invoice: lines, contact, job, issued versions
|
|
42
|
+
// GET /invoices/:id/pdf?version= the invoice as a PDF, drawn by Flow (server/core/invoice-pdf.js),
|
|
43
|
+
// with the accounting system's number and dates once it holds it;
|
|
44
|
+
// no accounting system calls
|
|
37
45
|
// POST /invoices a new draft
|
|
38
46
|
// PATCH /invoices/:id change a draft
|
|
39
47
|
// POST /invoices/:id/issue owner or admin: number it and keep the document as sent
|
|
@@ -58,6 +66,15 @@ import {
|
|
|
58
66
|
// POST /line-mappings owner or admin: a new mapping
|
|
59
67
|
// PUT /line-mappings/:id owner or admin: change some of a mapping
|
|
60
68
|
// DELETE /line-mappings/:id owner or admin
|
|
69
|
+
// GET /price-list the price list with its links, the connected systems' records with
|
|
70
|
+
// no link, and what differs (server/core/price-list.js)
|
|
71
|
+
// POST /price-list/check owner or admin: run the price check now (no calls to other systems)
|
|
72
|
+
// POST /price-items owner or admin: a new price item { code, name, unitAmountCents, taxCode, ... }
|
|
73
|
+
// PUT /price-items/:id owner or admin: change some of a price item
|
|
74
|
+
// POST /price-items/:id/archive owner or admin: { archived } archive it, or bring it back
|
|
75
|
+
// POST /price-items/:id/links owner or admin: { system, externalId } link a record of a system's list
|
|
76
|
+
// POST /price-items/:id/resolve owner or admin: Mark resolved: check again, accept what still differs
|
|
77
|
+
// DELETE /price-links/:id owner or admin: remove a link
|
|
61
78
|
// GET /automations?place= Flow's routines, each on or off (server/core/automations.js)
|
|
62
79
|
// PUT /automations/:key owner or admin: { enabled, config }
|
|
63
80
|
// GET /work-statuses the work system's job statuses, with job counts
|
|
@@ -240,7 +257,7 @@ async function invoiceDetail(db, organisationId, invoiceId, accounting = null) {
|
|
|
240
257
|
}
|
|
241
258
|
|
|
242
259
|
export function createInvoicingRouter({
|
|
243
|
-
db, personAuth, log = console, liveCheckFor = () => null, accounting = () => null, work = () => [],
|
|
260
|
+
db, personAuth, log = console, liveCheckFor = () => null, accounting = () => null, work = () => [], priceSystems = () => [],
|
|
244
261
|
}) {
|
|
245
262
|
const router = express.Router();
|
|
246
263
|
router.use(express.json({ limit: '64kb' }));
|
|
@@ -302,6 +319,7 @@ export function createInvoicingRouter({
|
|
|
302
319
|
})),
|
|
303
320
|
counts,
|
|
304
321
|
sendsTo: accounting()?.name ?? null,
|
|
322
|
+
sendMode: accounting() ? { ...(await sendModeSummary(db, organisationId)), ...(await accounting().capabilities.sendStatus({ organisationId })) } : null,
|
|
305
323
|
});
|
|
306
324
|
}));
|
|
307
325
|
|
|
@@ -349,6 +367,22 @@ export function createInvoicingRouter({
|
|
|
349
367
|
res.json({ ok: true, invoice: await invoiceDetail(db, organisationId, invoiceId, accounting()) });
|
|
350
368
|
}));
|
|
351
369
|
|
|
370
|
+
router.get('/invoices/:id/pdf', ...signedIn, handle(log, async (req, res) => {
|
|
371
|
+
const invoiceId = pathId(req.params.id, 'invoice');
|
|
372
|
+
const organisationId = await organisationOf(req);
|
|
373
|
+
const version = req.query.version === undefined ? null : Number(req.query.version);
|
|
374
|
+
if (version !== null && !(Number.isSafeInteger(version) && version > 0)) throw invalid('version must be a whole number above 0');
|
|
375
|
+
const source = await invoicePdfDocument(db, organisationId, invoiceId, { version, accounting: accounting() });
|
|
376
|
+
const bytes = await renderInvoicePdf(source);
|
|
377
|
+
res.set({
|
|
378
|
+
'Content-Type': 'application/pdf',
|
|
379
|
+
'Content-Disposition': `inline; filename="${pdfFilename(pdfFields(source))}"`,
|
|
380
|
+
'Cache-Control': 'no-store',
|
|
381
|
+
'X-Content-Type-Options': 'nosniff',
|
|
382
|
+
});
|
|
383
|
+
res.end(bytes);
|
|
384
|
+
}));
|
|
385
|
+
|
|
352
386
|
router.post('/invoices', ...signedIn, handle(log, async (req, res) => {
|
|
353
387
|
const organisationId = await organisationOf(req);
|
|
354
388
|
const body = req.body ?? {};
|
|
@@ -427,8 +461,13 @@ export function createInvoicingRouter({
|
|
|
427
461
|
const view = text(req.query.view);
|
|
428
462
|
if (view && !VIEWS.includes(view)) throw invalid(`view must be one of ${VIEWS.join(', ')}`);
|
|
429
463
|
const limit = limitOf(req.query.limit, 200, 500);
|
|
430
|
-
const
|
|
464
|
+
const listedAll = await listObligations(db, organisationId, { accounting: Boolean(liveCheckFor(organisationId)) });
|
|
465
|
+
// Held history jobs (core/history.js) are left out unless asked for: they
|
|
466
|
+
// are past work, listed in the history import's own reconciliation.
|
|
467
|
+
const withHistory = req.query.history === 'include';
|
|
468
|
+
const all = withHistory ? listedAll : listedAll.filter((entry) => !isHistoryHeld(entry.obligation));
|
|
431
469
|
const counts = Object.fromEntries(VIEWS.map((v) => [v, 0]));
|
|
470
|
+
counts.history = listedAll.length - listedAll.filter((entry) => !isHistoryHeld(entry.obligation)).length;
|
|
432
471
|
for (const entry of all) counts[entry.view] += 1;
|
|
433
472
|
const listed = (view ? all.filter((entry) => entry.view === view) : all).slice(0, limit);
|
|
434
473
|
res.json({ ok: true, jobs: await describeObligations(db, organisationId, listed, { accounting: accounting() }), counts });
|
|
@@ -602,6 +641,74 @@ export function createInvoicingRouter({
|
|
|
602
641
|
res.json({ ok: true });
|
|
603
642
|
}));
|
|
604
643
|
|
|
644
|
+
// The price list (server/core/price-list.js). Every read is from Flow's own
|
|
645
|
+
// copies of the connected systems' lists.
|
|
646
|
+
const catalogues = (organisationId) => readCatalogues(organisationId, priceSystems(), { log });
|
|
647
|
+
|
|
648
|
+
router.get('/price-list', ...signedIn, handle(log, async (req, res) => {
|
|
649
|
+
const organisationId = await organisationOf(req);
|
|
650
|
+
res.json({ ok: true, ...(await readPriceList(db, organisationId, { catalogues: await catalogues(organisationId) })) });
|
|
651
|
+
}));
|
|
652
|
+
|
|
653
|
+
router.post('/price-list/check', ...manager, handle(log, async (req, res) => {
|
|
654
|
+
const organisationId = await organisationOf(req);
|
|
655
|
+
const summary = await checkPrices(db, organisationId, { catalogues: await catalogues(organisationId), actor: `person:${req.person.id}` });
|
|
656
|
+
res.json({ ok: true, summary });
|
|
657
|
+
}));
|
|
658
|
+
|
|
659
|
+
router.post('/price-items', ...manager, handle(log, async (req, res) => {
|
|
660
|
+
const organisationId = await organisationOf(req);
|
|
661
|
+
const { organisationId: _named, ...raw } = req.body ?? {};
|
|
662
|
+
const item = await createPriceItem(db, organisationId, raw, { personId: req.person.id });
|
|
663
|
+
log.info?.(`[invoicing] organisation ${organisationId}: person ${req.person.id} added price item ${item.id}`);
|
|
664
|
+
res.status(201).json({ ok: true, item });
|
|
665
|
+
}));
|
|
666
|
+
|
|
667
|
+
router.put('/price-items/:id', ...manager, handle(log, async (req, res) => {
|
|
668
|
+
const itemId = pathId(req.params.id, 'price item');
|
|
669
|
+
const organisationId = await organisationOf(req);
|
|
670
|
+
const { organisationId: _named, ...changes } = req.body ?? {};
|
|
671
|
+
const item = await updatePriceItem(db, organisationId, itemId, changes, { personId: req.person.id });
|
|
672
|
+
log.info?.(`[invoicing] organisation ${organisationId}: person ${req.person.id} changed price item ${item.id}`);
|
|
673
|
+
res.json({ ok: true, item });
|
|
674
|
+
}));
|
|
675
|
+
|
|
676
|
+
router.post('/price-items/:id/archive', ...manager, handle(log, async (req, res) => {
|
|
677
|
+
const itemId = pathId(req.params.id, 'price item');
|
|
678
|
+
const organisationId = await organisationOf(req);
|
|
679
|
+
const archived = req.body?.archived ?? true;
|
|
680
|
+
if (typeof archived !== 'boolean') throw invalid('archived must be true or false');
|
|
681
|
+
const item = await archivePriceItem(db, organisationId, itemId, { archived, personId: req.person.id });
|
|
682
|
+
log.info?.(`[invoicing] organisation ${organisationId}: person ${req.person.id} ${archived ? 'archived' : 'restored'} price item ${item.id}`);
|
|
683
|
+
res.json({ ok: true, item });
|
|
684
|
+
}));
|
|
685
|
+
|
|
686
|
+
router.post('/price-items/:id/links', ...manager, handle(log, async (req, res) => {
|
|
687
|
+
const itemId = pathId(req.params.id, 'price item');
|
|
688
|
+
const organisationId = await organisationOf(req);
|
|
689
|
+
const system = text(req.body?.system);
|
|
690
|
+
const externalId = text(req.body?.externalId);
|
|
691
|
+
if (!system || !externalId) throw invalid('system and externalId are required');
|
|
692
|
+
const link = await linkPriceItem(db, organisationId, itemId, { system, externalId }, { catalogues: await catalogues(organisationId), personId: req.person.id });
|
|
693
|
+
log.info?.(`[invoicing] organisation ${organisationId}: person ${req.person.id} linked price item ${itemId} to ${system}`);
|
|
694
|
+
res.status(201).json({ ok: true, link: { id: link.id, system: link.system, externalId: link.externalId, externalCode: link.externalCode, matchedBy: link.matchedBy } });
|
|
695
|
+
}));
|
|
696
|
+
|
|
697
|
+
router.post('/price-items/:id/resolve', ...manager, handle(log, async (req, res) => {
|
|
698
|
+
const itemId = pathId(req.params.id, 'price item');
|
|
699
|
+
const organisationId = await organisationOf(req);
|
|
700
|
+
const result = await acceptFindings(db, organisationId, itemId, { catalogues: await catalogues(organisationId), actor: `person:${req.person.id}` });
|
|
701
|
+
res.json({ ok: true, ...result });
|
|
702
|
+
}));
|
|
703
|
+
|
|
704
|
+
router.delete('/price-links/:id', ...manager, handle(log, async (req, res) => {
|
|
705
|
+
const linkId = pathId(req.params.id, 'link');
|
|
706
|
+
const organisationId = await organisationOf(req);
|
|
707
|
+
await unlinkPriceItem(db, organisationId, linkId);
|
|
708
|
+
log.info?.(`[invoicing] organisation ${organisationId}: person ${req.person.id} removed price link ${linkId}`);
|
|
709
|
+
res.json({ ok: true });
|
|
710
|
+
}));
|
|
711
|
+
|
|
605
712
|
router.get('/settings', ...signedIn, handle(log, async (req, res) => {
|
|
606
713
|
const organisationId = await organisationOf(req);
|
|
607
714
|
res.json({ ok: true, settings: await readInvoicingSettings(db, organisationId), defaults: INVOICING_DEFAULTS });
|
package/server/app.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
+
import { createAlertsRouter } from './api/alerts.js';
|
|
2
3
|
import { createConnectorApi } from './api/connector.js';
|
|
3
4
|
import { createInvoicingRouter } from './api/invoicing.js';
|
|
4
5
|
import { createSignInRouter } from './auth/routes.js';
|
|
6
|
+
import { integrationAlertsSource, invoiceProblemsSource, priceDifferencesSource } from './core/alerts.js';
|
|
7
|
+
import { appliedMigrationCounts } from './db/doctor.js';
|
|
5
8
|
import { deployedBuildStatus } from './deployed-build.js';
|
|
6
9
|
import { integrationsHealth } from './integrations/health.js';
|
|
7
10
|
import { createSetupRouter } from './integrations/setup.js';
|
|
@@ -27,10 +30,14 @@ import { createUiAssetsRouter } from './ui/routes.js';
|
|
|
27
30
|
// each one (server/integrations/health.js). `defaultFamily` is the design
|
|
28
31
|
// system every page starts on (FLOW_THEME, server/theme/themes.js). `log`
|
|
29
32
|
// is the console, or the monitored log (server/core/monitoring.js) that also
|
|
30
|
-
// sends each error to the installation's error service.
|
|
33
|
+
// sends each error to the installation's error service. `schemaJournals` are
|
|
34
|
+
// the migration journals this build ships (server/db/doctor.js): GET /health
|
|
35
|
+
// reports how many of each the database has applied, so the workspace's
|
|
36
|
+
// doctor can tell what Replit's publish step will offer (docs/REPLIT.md).
|
|
31
37
|
export function createApp({
|
|
32
38
|
build = deployedBuildStatus(),
|
|
33
39
|
database = null,
|
|
40
|
+
schemaJournals = null,
|
|
34
41
|
auth = null,
|
|
35
42
|
updates = null,
|
|
36
43
|
integrations = null,
|
|
@@ -59,6 +66,11 @@ export function createApp({
|
|
|
59
66
|
// records). An integration problem does not fail the health check: Flow's
|
|
60
67
|
// core works with no integration connected (VISION.md).
|
|
61
68
|
const body = { ok, service: 'flow', database: databaseOk, build };
|
|
69
|
+
// Counts only: no table names and no data.
|
|
70
|
+
if (databaseOk && schemaJournals) {
|
|
71
|
+
body.schema = await appliedMigrationCounts(database.pool, schemaJournals)
|
|
72
|
+
.then((migrations) => ({ migrations }), () => null);
|
|
73
|
+
}
|
|
62
74
|
if (integrations) body.integrations = await integrationsHealth(integrations);
|
|
63
75
|
res.status(ok ? 200 : 503).json(body);
|
|
64
76
|
});
|
|
@@ -116,7 +128,22 @@ export function createApp({
|
|
|
116
128
|
const accounting = () => integrations?.with('pushInvoices')[0] ?? null;
|
|
117
129
|
// The Jobs board: every enabled integration that lists its jobs (Seazona).
|
|
118
130
|
const work = () => integrations?.with('listWork') ?? [];
|
|
119
|
-
|
|
131
|
+
// The price list: every enabled integration that keeps its own (Xero
|
|
132
|
+
// items, Seazona products).
|
|
133
|
+
const priceSystems = () => integrations?.with('listPriceItems') ?? [];
|
|
134
|
+
app.use('/api/invoicing', createInvoicingRouter({ db: database.db, personAuth: auth.personAuth, log, liveCheckFor, accounting, work, priceSystems }));
|
|
135
|
+
// Quiet alerts (server/core/alerts.js): what needs a person on each page.
|
|
136
|
+
// Integrations add their own through the `alerts` capability.
|
|
137
|
+
app.use('/api/alerts', createAlertsRouter({
|
|
138
|
+
db: database.db,
|
|
139
|
+
personAuth: auth.personAuth,
|
|
140
|
+
log,
|
|
141
|
+
sources: {
|
|
142
|
+
invoices: [invoiceProblemsSource(database.db, { accounting })],
|
|
143
|
+
'price-list': [priceDifferencesSource(database.db)],
|
|
144
|
+
integrations: [integrationAlertsSource(() => integrations?.with('alerts') ?? [])],
|
|
145
|
+
},
|
|
146
|
+
}));
|
|
120
147
|
}
|
|
121
148
|
// What a signed-in browser needs to report its errors and problems to the
|
|
122
149
|
// installation's own error service (docs/SENTRY.md): the first integration
|