@arcforgelabs/flow 2026.10.18 → 2026.10.19
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 +3 -0
- package/SECURITY.md +4 -1
- package/npm-shrinkwrap.json +32 -3
- package/package.json +3 -2
- package/server/app.js +26 -3
- package/server/auth/routes.js +27 -12
- package/server/auth/session.js +6 -3
- package/server/auth/settings.js +26 -7
- package/server/auth/sign-in.html +17 -6
- package/server/core/invoices.js +6 -3
- package/server/core/ledger.js +420 -0
- package/server/db/migrations/0003_billing_ledger.sql +84 -0
- package/server/db/migrations/0004_ledger_events_append_only.sql +5 -0
- package/server/db/migrations/meta/0003_snapshot.json +1327 -0
- package/server/db/migrations/meta/0004_snapshot.json +1327 -0
- package/server/db/migrations/meta/_journal.json +14 -0
- package/server/db/schema.js +137 -0
- package/server/import/earlier-flow.js +230 -0
- package/server/import-earlier-flow.js +66 -0
- package/server/index.js +16 -4
- package/server/integrations/adapters.js +6 -0
- package/server/integrations/health.js +21 -0
- package/server/integrations/index.js +26 -0
- package/server/integrations/port.js +154 -6
- package/server/integrations/remove-tables.js +31 -0
- package/server/integrations/schema.js +62 -0
- package/server/integrations/seazona/client.js +196 -0
- package/server/integrations/seazona/drizzle.config.js +9 -0
- package/server/integrations/seazona/feed.js +104 -0
- package/server/integrations/seazona/governor.js +174 -0
- package/server/integrations/seazona/index.js +84 -0
- package/server/integrations/seazona/lanes.js +431 -0
- package/server/integrations/seazona/ledger.js +66 -0
- package/server/integrations/seazona/map.js +229 -0
- package/server/integrations/seazona/migrations/0000_api_governor.sql +20 -0
- package/server/integrations/seazona/migrations/0001_mirror.sql +106 -0
- package/server/integrations/seazona/migrations/meta/0000_snapshot.json +149 -0
- package/server/integrations/seazona/migrations/meta/0001_snapshot.json +793 -0
- package/server/integrations/seazona/migrations/meta/_journal.json +20 -0
- package/server/integrations/seazona/replay.js +79 -0
- package/server/integrations/seazona/scheduler.js +78 -0
- package/server/integrations/seazona/schema.js +157 -0
- package/server/integrations/seazona/settings.js +161 -0
- package/server/integrations/seazona/store.js +219 -0
- package/server/integrations/seazona/sync-once.js +44 -0
- package/server/integrations/seazona/tables.js +17 -0
- package/server/integrations/xero/catalogue.js +184 -0
- package/server/integrations/xero/client.js +253 -0
- package/server/integrations/xero/connections.js +104 -0
- package/server/integrations/xero/drafts.js +525 -0
- package/server/integrations/xero/fixtures/replay.json +45 -0
- package/server/integrations/xero/governor.js +275 -0
- package/server/integrations/xero/index.js +56 -0
- package/server/integrations/xero/ledger.js +269 -0
- package/server/integrations/xero/migrations/0000_xero_connections.sql +17 -0
- package/server/integrations/xero/migrations/0001_xero_catalogue.sql +49 -0
- package/server/integrations/xero/migrations/0002_xero_invoice_pushes.sql +32 -0
- package/server/integrations/xero/migrations/0003_xero_payment_cursors.sql +7 -0
- package/server/integrations/xero/migrations/meta/_journal.json +34 -0
- package/server/integrations/xero/oauth.js +143 -0
- package/server/integrations/xero/replay.js +83 -0
- package/server/integrations/xero/routes.js +234 -0
- package/server/integrations/xero/runtime.js +135 -0
- package/server/integrations/xero/settings.js +170 -0
- package/server/rate-limit.js +13 -19
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Seazona work, shaped for the billing obligation ledger (server/core/ledger.js,
|
|
2
|
+
// issue #3). The adapter does not import the ledger: the core reads the
|
|
3
|
+
// billing feed and passes these shapes to it.
|
|
4
|
+
//
|
|
5
|
+
// item.ledgerJob -> observeJob(db, organisationId, { ...item.ledgerJob, contactId })
|
|
6
|
+
// withdrawn.ledgerHold -> setHold(db, organisationId, <obligation id>, { ...ledgerHold, actor })
|
|
7
|
+
//
|
|
8
|
+
// One Seazona order is one billable job. The contact is Flow's, so the core
|
|
9
|
+
// resolves it (from item.client) and adds contactId; the ledger keeps a job
|
|
10
|
+
// without one pending.
|
|
11
|
+
|
|
12
|
+
export const SEAZONA_SOURCE_SYSTEM = 'seazona';
|
|
13
|
+
|
|
14
|
+
function ledgerLine(line) {
|
|
15
|
+
return {
|
|
16
|
+
// The ledger maps a line to a price item by its exact code only.
|
|
17
|
+
itemCode: line.code ?? null,
|
|
18
|
+
description: line.description,
|
|
19
|
+
quantity: 1,
|
|
20
|
+
// Unknown amount or tax: the price item with the same code fills them.
|
|
21
|
+
unitAmountCents: line.amountCents ?? undefined,
|
|
22
|
+
taxCode: typeof line.taxable === 'boolean' ? (line.taxable ? 'GST' : 'GST_FREE') : undefined,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The lines that bill the job. When the job's Seazona invoice carries this
|
|
27
|
+
// order and no other, its lines are what the lab billed, discounts included.
|
|
28
|
+
// When it carries several orders, its lines cannot be split between them, so
|
|
29
|
+
// the order's own lines are used and nothing is billed twice.
|
|
30
|
+
export function ledgerLinesFor(item) {
|
|
31
|
+
const invoiceLines = item.invoice?.lines ?? [];
|
|
32
|
+
const useInvoice = item.invoice && item.invoiceOrderCount === 1 && invoiceLines.length > 0;
|
|
33
|
+
return { lines: (useInvoice ? invoiceLines : item.lines ?? []).map(ledgerLine), linesFrom: useInvoice ? 'invoice' : 'order' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// observeJob's input for one billable feed item, without contactId.
|
|
37
|
+
// `observedAt` is the last successful read from Seazona, so the ledger's
|
|
38
|
+
// out-of-date check measures how old Seazona's report really is.
|
|
39
|
+
export function toLedgerJob(item) {
|
|
40
|
+
return {
|
|
41
|
+
sourceSystem: SEAZONA_SOURCE_SYSTEM,
|
|
42
|
+
sourceJobRef: item.sourceId,
|
|
43
|
+
sourceInvoiceRef: item.invoiceNumber ?? null,
|
|
44
|
+
// The Seazona order id is the case number. Never patient data.
|
|
45
|
+
caseRef: item.sourceId,
|
|
46
|
+
practitioner: item.doctorName ?? null,
|
|
47
|
+
// The Seazona API gives a shipped status but no shipped date.
|
|
48
|
+
shippedOn: null,
|
|
49
|
+
lines: ledgerLinesFor(item).lines,
|
|
50
|
+
observedAt: item.syncedAt,
|
|
51
|
+
actor: SEAZONA_SOURCE_SYSTEM,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// An order deleted in Seazona sends its obligation, if there is one, to
|
|
56
|
+
// review: a person decides what happens to any invoice already made. An order
|
|
57
|
+
// that only stopped being billable needs nothing from the ledger.
|
|
58
|
+
export function toLedgerHold(entry) {
|
|
59
|
+
if (entry.reason !== 'deleted_in_seazona') return null;
|
|
60
|
+
return {
|
|
61
|
+
sourceSystem: SEAZONA_SOURCE_SYSTEM,
|
|
62
|
+
sourceJobRef: entry.sourceId,
|
|
63
|
+
holdState: 'review',
|
|
64
|
+
reason: 'the order was deleted in Seazona',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
// Seazona API records, reduced to what billing needs. Each mapper takes the
|
|
4
|
+
// list row and the detail read and returns the mirror row. Notes, files,
|
|
5
|
+
// settings and other clinical detail are dropped. The patient field is kept
|
|
6
|
+
// only as a reference, and only when the installation asks for it
|
|
7
|
+
// (SEAZONA_KEEP_PATIENT_REFERENCE; VISION.md, "Why does Flow keep as little
|
|
8
|
+
// patient data as it can?").
|
|
9
|
+
|
|
10
|
+
export const DELETED_STATUS = 'Deleted in Seazona';
|
|
11
|
+
|
|
12
|
+
// Statuses after which Seazona does not change an order again. "Complete" is
|
|
13
|
+
// not one of them: a lab often moves a complete order to Shipped days or
|
|
14
|
+
// months later, and the list API has no lastModified cursor for orders, so a
|
|
15
|
+
// complete order stays in the active-order rotation until it ships.
|
|
16
|
+
const FINAL_STATUS = /(shipped|delivered|cancelled|canceled|void|closed|archived|deleted)/i;
|
|
17
|
+
|
|
18
|
+
export function text(value) {
|
|
19
|
+
if (value === null || value === undefined) return '';
|
|
20
|
+
return String(value).trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function textOrNull(value) {
|
|
24
|
+
return text(value) || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function number(value) {
|
|
28
|
+
if (value === null || value === undefined || value === '') return null;
|
|
29
|
+
const parsed = Number(value);
|
|
30
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Seazona reports dollars as decimals; Flow keeps integer cents.
|
|
34
|
+
export function toCents(value) {
|
|
35
|
+
const amount = number(value);
|
|
36
|
+
return amount === null ? null : Math.round(amount * 100);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function stableSerialize(value) {
|
|
40
|
+
if (value === null || value === undefined) return 'null';
|
|
41
|
+
if (Array.isArray(value)) return `[${value.map(stableSerialize).join(',')}]`;
|
|
42
|
+
if (typeof value === 'object') {
|
|
43
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableSerialize(value[key])}`).join(',')}}`;
|
|
44
|
+
}
|
|
45
|
+
return JSON.stringify(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function hashOf(value) {
|
|
49
|
+
return createHash('sha256').update(stableSerialize(value)).digest('hex');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isFinalStatus(status) {
|
|
53
|
+
return FINAL_STATUS.test(text(status));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isCompleteStatus(status) {
|
|
57
|
+
return /\bcomplete(?:d)?\b/i.test(text(status));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isShippedStatus(status) {
|
|
61
|
+
return /\b(shipped|delivered)\b/i.test(text(status));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// What a person sees for an order: its Seazona status, or "Deleted in
|
|
65
|
+
// Seazona" once Seazona answered 404 for it.
|
|
66
|
+
export function orderStatusLabel(row) {
|
|
67
|
+
return row?.deletedInSourceAt ? DELETED_STATUS : text(row?.status);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Seazona dates come as ISO text or as day/month/year (Australian order),
|
|
71
|
+
// with an optional time. Returns the calendar date (yyyy-mm-dd) or null.
|
|
72
|
+
export function parseSeazonaDate(value) {
|
|
73
|
+
const raw = text(value);
|
|
74
|
+
if (!raw) return null;
|
|
75
|
+
const dmy = raw.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})\b/);
|
|
76
|
+
if (dmy) {
|
|
77
|
+
const day = Number(dmy[1]);
|
|
78
|
+
const month = Number(dmy[2]);
|
|
79
|
+
const year = dmy[3].length === 2 ? 2000 + Number(dmy[3]) : Number(dmy[3]);
|
|
80
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
81
|
+
if (date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) return null;
|
|
82
|
+
return date.toISOString().slice(0, 10);
|
|
83
|
+
}
|
|
84
|
+
const iso = raw.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
85
|
+
return iso ? `${iso[1]}-${iso[2]}-${iso[3]}` : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function mapLine(item = {}, { discount = false } = {}) {
|
|
89
|
+
const amountCents = toCents(item.price ?? item.amount);
|
|
90
|
+
const line = {
|
|
91
|
+
itemId: textOrNull(item.id),
|
|
92
|
+
kind: discount ? 'discount' : (text(item.type) || 'product'),
|
|
93
|
+
description: text(item.name || item.description),
|
|
94
|
+
code: textOrNull(item.code),
|
|
95
|
+
amountCents: discount && amountCents !== null ? -Math.abs(amountCents) : amountCents,
|
|
96
|
+
};
|
|
97
|
+
if (typeof item.taxable === 'boolean') line.taxable = item.taxable;
|
|
98
|
+
if (item.tooth !== undefined && item.tooth !== null && text(item.tooth)) line.tooth = text(item.tooth);
|
|
99
|
+
return line;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const sumCents = (lines) => lines.reduce((sum, line) => sum + (line.amountCents ?? 0), 0);
|
|
103
|
+
|
|
104
|
+
// The list fields that tell an order changed. Equal signatures mean the
|
|
105
|
+
// detail read can be skipped. The patient field is left out, so not even a
|
|
106
|
+
// hash of it is stored.
|
|
107
|
+
export function orderListSignature(item = {}) {
|
|
108
|
+
return hashOf({
|
|
109
|
+
id: text(item.id),
|
|
110
|
+
invoiceNumber: text(item.invoiceNumber),
|
|
111
|
+
company: text(item.company),
|
|
112
|
+
doctor: text(item.clientName),
|
|
113
|
+
status: text(item.status),
|
|
114
|
+
department: text(item.department),
|
|
115
|
+
ordered: text(item.ordered),
|
|
116
|
+
due: text(item.due),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function mapOrder(listItem = {}, detail = null, { keepPatientReference = false } = {}) {
|
|
121
|
+
const merged = { ...listItem, ...(detail || {}) };
|
|
122
|
+
const lines = Array.isArray(merged.products)
|
|
123
|
+
? merged.products.map((item) => mapLine(item)).filter((line) => line.description)
|
|
124
|
+
: [];
|
|
125
|
+
return {
|
|
126
|
+
id: text(merged.id),
|
|
127
|
+
invoiceNumber: textOrNull(merged.invoiceNumber),
|
|
128
|
+
clientId: textOrNull(merged.clientId),
|
|
129
|
+
company: textOrNull(merged.company),
|
|
130
|
+
doctorName: textOrNull(merged.clientName),
|
|
131
|
+
status: textOrNull(merged.status),
|
|
132
|
+
orderedText: textOrNull(merged.ordered),
|
|
133
|
+
dueText: textOrNull(merged.due),
|
|
134
|
+
lastModifiedText: textOrNull(merged.lastModified),
|
|
135
|
+
department: textOrNull(merged.department),
|
|
136
|
+
lines,
|
|
137
|
+
subtotalCents: lines.length ? sumCents(lines) : null,
|
|
138
|
+
patientReference: keepPatientReference ? textOrNull(merged.patient) : null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function invoiceListSignature(item = {}) {
|
|
143
|
+
return hashOf({
|
|
144
|
+
id: text(item.id),
|
|
145
|
+
invoiceNumber: text(item.invoiceNumber),
|
|
146
|
+
clientId: text(item.clientId),
|
|
147
|
+
status: text(item.status),
|
|
148
|
+
due: text(item.due),
|
|
149
|
+
lastModified: text(item.lastModified),
|
|
150
|
+
subtotal: number(item.subtotal ?? item.sales),
|
|
151
|
+
tax: number(item.tax),
|
|
152
|
+
total: number(item.total),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function mapInvoice(listItem = {}, detail = null, { keepPatientReference = false } = {}) {
|
|
157
|
+
const merged = { ...listItem, ...(detail || {}) };
|
|
158
|
+
const products = Array.isArray(merged.productServiceItems) ? merged.productServiceItems.map((item) => mapLine(item)) : [];
|
|
159
|
+
const discounts = Array.isArray(merged.discountItems) ? merged.discountItems.map((item) => mapLine(item, { discount: true })) : [];
|
|
160
|
+
const lines = [...products, ...discounts].filter((line) => line.description);
|
|
161
|
+
return {
|
|
162
|
+
invoiceNumber: text(merged.invoiceNumber),
|
|
163
|
+
seazonaId: textOrNull(merged.id),
|
|
164
|
+
clientId: textOrNull(merged.clientId),
|
|
165
|
+
accountNumber: textOrNull(merged.accountNumber),
|
|
166
|
+
company: textOrNull(merged.company),
|
|
167
|
+
doctorName: textOrNull(merged.fullName),
|
|
168
|
+
status: textOrNull(merged.status),
|
|
169
|
+
invoiceDateText: textOrNull(merged.invoiceDate || merged.created),
|
|
170
|
+
dueText: textOrNull(merged.due),
|
|
171
|
+
lastModifiedText: textOrNull(merged.lastModified),
|
|
172
|
+
lines,
|
|
173
|
+
subtotalCents: toCents(merged.subtotal ?? merged.sales),
|
|
174
|
+
taxCents: toCents(merged.tax),
|
|
175
|
+
totalCents: toCents(merged.total),
|
|
176
|
+
discountCents: toCents(merged.discounts),
|
|
177
|
+
patientReference: keepPatientReference ? textOrNull(merged.patient) : null,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function clientListSignature(item = {}) {
|
|
182
|
+
return hashOf({
|
|
183
|
+
id: text(item.id),
|
|
184
|
+
accountNumber: text(item.accountNumber),
|
|
185
|
+
company: text(item.company),
|
|
186
|
+
fullName: text(item.fullName),
|
|
187
|
+
email: text(item.email || item.emailAddresses),
|
|
188
|
+
phone: text(item.phone1 || item.phone),
|
|
189
|
+
lastModified: text(item.lastModified),
|
|
190
|
+
discontinued: Boolean(item.discontinued),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function mapClient(listItem = {}, detail = null) {
|
|
195
|
+
const merged = { ...listItem, ...(detail || {}) };
|
|
196
|
+
const address = {};
|
|
197
|
+
for (const field of ['address1', 'address2', 'city', 'region', 'postalCode', 'countryCode']) {
|
|
198
|
+
if (text(merged[field])) address[field] = text(merged[field]);
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
id: text(merged.id),
|
|
202
|
+
accountNumber: textOrNull(merged.accountNumber),
|
|
203
|
+
company: textOrNull(merged.company),
|
|
204
|
+
fullName: textOrNull(merged.fullName),
|
|
205
|
+
email: textOrNull(merged.email || merged.emailAddresses),
|
|
206
|
+
phone: textOrNull(merged.phone1 || merged.phone),
|
|
207
|
+
address: Object.keys(address).length ? address : null,
|
|
208
|
+
taxRate: number(merged.taxRate) === null ? null : String(number(merged.taxRate)),
|
|
209
|
+
active: merged.discontinued === undefined ? null : !merged.discontinued,
|
|
210
|
+
// Who at the practice the work is for: an id and a name, nothing more.
|
|
211
|
+
doctors: Array.isArray(merged.doctors)
|
|
212
|
+
? merged.doctors.map((doctor) => ({ id: text(doctor?.id), fullName: text(doctor?.fullName) })).filter((doctor) => doctor.id)
|
|
213
|
+
: [],
|
|
214
|
+
lastModifiedText: textOrNull(merged.lastModified),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function mapReference(resource, item = {}) {
|
|
219
|
+
const name = text(item.name || item.fullName || [item.firstName, item.lastName].map(text).filter(Boolean).join(' '));
|
|
220
|
+
return {
|
|
221
|
+
resource,
|
|
222
|
+
id: text(item.id),
|
|
223
|
+
name: name || null,
|
|
224
|
+
code: resource === 'products' ? textOrNull(item.code) : null,
|
|
225
|
+
kind: resource === 'products' ? textOrNull(item.type) : null,
|
|
226
|
+
priceCents: resource === 'products' ? toCents(item.price ?? item.amount) : null,
|
|
227
|
+
taxable: resource === 'products' && typeof item.taxable === 'boolean' ? item.taxable : null,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
CREATE TABLE "seazona_api_governor" (
|
|
2
|
+
"client_key" text PRIMARY KEY NOT NULL,
|
|
3
|
+
"next_eligible_at" timestamp with time zone DEFAULT 'epoch' NOT NULL,
|
|
4
|
+
"cooldown_until" timestamp with time zone DEFAULT 'epoch' NOT NULL,
|
|
5
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
6
|
+
);
|
|
7
|
+
--> statement-breakpoint
|
|
8
|
+
CREATE TABLE "seazona_api_requests" (
|
|
9
|
+
"id" bigserial PRIMARY KEY NOT NULL,
|
|
10
|
+
"client_key" text NOT NULL,
|
|
11
|
+
"workflow" text NOT NULL,
|
|
12
|
+
"admitted_at" timestamp with time zone NOT NULL,
|
|
13
|
+
"request_path" text,
|
|
14
|
+
"status_code" integer,
|
|
15
|
+
"failure_kind" text,
|
|
16
|
+
"duration_ms" integer,
|
|
17
|
+
"is_retry" boolean DEFAULT false NOT NULL
|
|
18
|
+
);
|
|
19
|
+
--> statement-breakpoint
|
|
20
|
+
CREATE INDEX "seazona_api_requests_window" ON "seazona_api_requests" USING btree ("client_key","admitted_at");
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
CREATE TABLE "seazona_clients" (
|
|
2
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
3
|
+
"account_number" text,
|
|
4
|
+
"company" text,
|
|
5
|
+
"full_name" text,
|
|
6
|
+
"email" text,
|
|
7
|
+
"phone" text,
|
|
8
|
+
"address" jsonb,
|
|
9
|
+
"tax_rate" text,
|
|
10
|
+
"active" boolean,
|
|
11
|
+
"doctors" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
12
|
+
"last_modified_text" text,
|
|
13
|
+
"list_signature" text,
|
|
14
|
+
"record_hash" text NOT NULL,
|
|
15
|
+
"first_seen_at" timestamp with time zone NOT NULL,
|
|
16
|
+
"last_synced_at" timestamp with time zone NOT NULL,
|
|
17
|
+
"last_changed_at" timestamp with time zone NOT NULL
|
|
18
|
+
);
|
|
19
|
+
--> statement-breakpoint
|
|
20
|
+
CREATE TABLE "seazona_invoices" (
|
|
21
|
+
"invoice_number" text PRIMARY KEY NOT NULL,
|
|
22
|
+
"seazona_id" text,
|
|
23
|
+
"client_id" text,
|
|
24
|
+
"account_number" text,
|
|
25
|
+
"company" text,
|
|
26
|
+
"doctor_name" text,
|
|
27
|
+
"status" text,
|
|
28
|
+
"invoice_date_text" text,
|
|
29
|
+
"due_text" text,
|
|
30
|
+
"last_modified_text" text,
|
|
31
|
+
"lines" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
32
|
+
"subtotal_cents" bigint,
|
|
33
|
+
"tax_cents" bigint,
|
|
34
|
+
"total_cents" bigint,
|
|
35
|
+
"discount_cents" bigint,
|
|
36
|
+
"patient_reference" text,
|
|
37
|
+
"list_signature" text,
|
|
38
|
+
"record_hash" text NOT NULL,
|
|
39
|
+
"first_seen_at" timestamp with time zone NOT NULL,
|
|
40
|
+
"last_synced_at" timestamp with time zone NOT NULL,
|
|
41
|
+
"last_changed_at" timestamp with time zone NOT NULL
|
|
42
|
+
);
|
|
43
|
+
--> statement-breakpoint
|
|
44
|
+
CREATE TABLE "seazona_lane_state" (
|
|
45
|
+
"lane" text PRIMARY KEY NOT NULL,
|
|
46
|
+
"status" text DEFAULT 'idle' NOT NULL,
|
|
47
|
+
"cursor_time" timestamp with time zone,
|
|
48
|
+
"cursor" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
49
|
+
"last_success_at" timestamp with time zone,
|
|
50
|
+
"last_attempt_at" timestamp with time zone,
|
|
51
|
+
"next_run_at" timestamp with time zone,
|
|
52
|
+
"failure_count" integer DEFAULT 0 NOT NULL,
|
|
53
|
+
"last_error" text,
|
|
54
|
+
"failure_details" jsonb,
|
|
55
|
+
"held_reason" text,
|
|
56
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
57
|
+
);
|
|
58
|
+
--> statement-breakpoint
|
|
59
|
+
CREATE TABLE "seazona_order_read_failures" (
|
|
60
|
+
"order_id" text PRIMARY KEY NOT NULL,
|
|
61
|
+
"failure_count" integer NOT NULL,
|
|
62
|
+
"last_attempt_at" timestamp with time zone NOT NULL,
|
|
63
|
+
"last_http_status" integer,
|
|
64
|
+
"last_failure_kind" text,
|
|
65
|
+
"last_error" text,
|
|
66
|
+
"quarantined_until" timestamp with time zone
|
|
67
|
+
);
|
|
68
|
+
--> statement-breakpoint
|
|
69
|
+
CREATE TABLE "seazona_orders" (
|
|
70
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
71
|
+
"invoice_number" text,
|
|
72
|
+
"client_id" text,
|
|
73
|
+
"company" text,
|
|
74
|
+
"doctor_name" text,
|
|
75
|
+
"status" text,
|
|
76
|
+
"ordered_text" text,
|
|
77
|
+
"due_text" text,
|
|
78
|
+
"last_modified_text" text,
|
|
79
|
+
"department" text,
|
|
80
|
+
"lines" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
81
|
+
"subtotal_cents" bigint,
|
|
82
|
+
"patient_reference" text,
|
|
83
|
+
"deleted_in_source_at" timestamp with time zone,
|
|
84
|
+
"list_signature" text,
|
|
85
|
+
"record_hash" text NOT NULL,
|
|
86
|
+
"first_seen_at" timestamp with time zone NOT NULL,
|
|
87
|
+
"last_synced_at" timestamp with time zone NOT NULL,
|
|
88
|
+
"last_changed_at" timestamp with time zone NOT NULL
|
|
89
|
+
);
|
|
90
|
+
--> statement-breakpoint
|
|
91
|
+
CREATE TABLE "seazona_reference" (
|
|
92
|
+
"resource" text NOT NULL,
|
|
93
|
+
"id" text NOT NULL,
|
|
94
|
+
"name" text,
|
|
95
|
+
"code" text,
|
|
96
|
+
"kind" text,
|
|
97
|
+
"price_cents" bigint,
|
|
98
|
+
"taxable" boolean,
|
|
99
|
+
"record_hash" text NOT NULL,
|
|
100
|
+
"last_synced_at" timestamp with time zone NOT NULL,
|
|
101
|
+
CONSTRAINT "seazona_reference_resource_id_pk" PRIMARY KEY("resource","id")
|
|
102
|
+
);
|
|
103
|
+
--> statement-breakpoint
|
|
104
|
+
CREATE INDEX "seazona_invoices_changed" ON "seazona_invoices" USING btree ("last_changed_at");--> statement-breakpoint
|
|
105
|
+
CREATE INDEX "seazona_orders_invoice_number" ON "seazona_orders" USING btree ("invoice_number");--> statement-breakpoint
|
|
106
|
+
CREATE INDEX "seazona_orders_changed" ON "seazona_orders" USING btree ("last_changed_at");
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "3bb4b809-5730-4ae9-8127-2766cac649c6",
|
|
3
|
+
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
4
|
+
"version": "7",
|
|
5
|
+
"dialect": "postgresql",
|
|
6
|
+
"tables": {
|
|
7
|
+
"public.seazona_api_governor": {
|
|
8
|
+
"name": "seazona_api_governor",
|
|
9
|
+
"schema": "",
|
|
10
|
+
"columns": {
|
|
11
|
+
"client_key": {
|
|
12
|
+
"name": "client_key",
|
|
13
|
+
"type": "text",
|
|
14
|
+
"primaryKey": true,
|
|
15
|
+
"notNull": true
|
|
16
|
+
},
|
|
17
|
+
"next_eligible_at": {
|
|
18
|
+
"name": "next_eligible_at",
|
|
19
|
+
"type": "timestamp with time zone",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true,
|
|
22
|
+
"default": "'epoch'"
|
|
23
|
+
},
|
|
24
|
+
"cooldown_until": {
|
|
25
|
+
"name": "cooldown_until",
|
|
26
|
+
"type": "timestamp with time zone",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": true,
|
|
29
|
+
"default": "'epoch'"
|
|
30
|
+
},
|
|
31
|
+
"updated_at": {
|
|
32
|
+
"name": "updated_at",
|
|
33
|
+
"type": "timestamp with time zone",
|
|
34
|
+
"primaryKey": false,
|
|
35
|
+
"notNull": true,
|
|
36
|
+
"default": "now()"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"indexes": {},
|
|
40
|
+
"foreignKeys": {},
|
|
41
|
+
"compositePrimaryKeys": {},
|
|
42
|
+
"uniqueConstraints": {},
|
|
43
|
+
"policies": {},
|
|
44
|
+
"checkConstraints": {},
|
|
45
|
+
"isRLSEnabled": false
|
|
46
|
+
},
|
|
47
|
+
"public.seazona_api_requests": {
|
|
48
|
+
"name": "seazona_api_requests",
|
|
49
|
+
"schema": "",
|
|
50
|
+
"columns": {
|
|
51
|
+
"id": {
|
|
52
|
+
"name": "id",
|
|
53
|
+
"type": "bigserial",
|
|
54
|
+
"primaryKey": true,
|
|
55
|
+
"notNull": true
|
|
56
|
+
},
|
|
57
|
+
"client_key": {
|
|
58
|
+
"name": "client_key",
|
|
59
|
+
"type": "text",
|
|
60
|
+
"primaryKey": false,
|
|
61
|
+
"notNull": true
|
|
62
|
+
},
|
|
63
|
+
"workflow": {
|
|
64
|
+
"name": "workflow",
|
|
65
|
+
"type": "text",
|
|
66
|
+
"primaryKey": false,
|
|
67
|
+
"notNull": true
|
|
68
|
+
},
|
|
69
|
+
"admitted_at": {
|
|
70
|
+
"name": "admitted_at",
|
|
71
|
+
"type": "timestamp with time zone",
|
|
72
|
+
"primaryKey": false,
|
|
73
|
+
"notNull": true
|
|
74
|
+
},
|
|
75
|
+
"request_path": {
|
|
76
|
+
"name": "request_path",
|
|
77
|
+
"type": "text",
|
|
78
|
+
"primaryKey": false,
|
|
79
|
+
"notNull": false
|
|
80
|
+
},
|
|
81
|
+
"status_code": {
|
|
82
|
+
"name": "status_code",
|
|
83
|
+
"type": "integer",
|
|
84
|
+
"primaryKey": false,
|
|
85
|
+
"notNull": false
|
|
86
|
+
},
|
|
87
|
+
"failure_kind": {
|
|
88
|
+
"name": "failure_kind",
|
|
89
|
+
"type": "text",
|
|
90
|
+
"primaryKey": false,
|
|
91
|
+
"notNull": false
|
|
92
|
+
},
|
|
93
|
+
"duration_ms": {
|
|
94
|
+
"name": "duration_ms",
|
|
95
|
+
"type": "integer",
|
|
96
|
+
"primaryKey": false,
|
|
97
|
+
"notNull": false
|
|
98
|
+
},
|
|
99
|
+
"is_retry": {
|
|
100
|
+
"name": "is_retry",
|
|
101
|
+
"type": "boolean",
|
|
102
|
+
"primaryKey": false,
|
|
103
|
+
"notNull": true,
|
|
104
|
+
"default": false
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"indexes": {
|
|
108
|
+
"seazona_api_requests_window": {
|
|
109
|
+
"name": "seazona_api_requests_window",
|
|
110
|
+
"columns": [
|
|
111
|
+
{
|
|
112
|
+
"expression": "client_key",
|
|
113
|
+
"isExpression": false,
|
|
114
|
+
"asc": true,
|
|
115
|
+
"nulls": "last"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"expression": "admitted_at",
|
|
119
|
+
"isExpression": false,
|
|
120
|
+
"asc": true,
|
|
121
|
+
"nulls": "last"
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
"isUnique": false,
|
|
125
|
+
"concurrently": false,
|
|
126
|
+
"method": "btree",
|
|
127
|
+
"with": {}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
"foreignKeys": {},
|
|
131
|
+
"compositePrimaryKeys": {},
|
|
132
|
+
"uniqueConstraints": {},
|
|
133
|
+
"policies": {},
|
|
134
|
+
"checkConstraints": {},
|
|
135
|
+
"isRLSEnabled": false
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"enums": {},
|
|
139
|
+
"schemas": {},
|
|
140
|
+
"sequences": {},
|
|
141
|
+
"roles": {},
|
|
142
|
+
"policies": {},
|
|
143
|
+
"views": {},
|
|
144
|
+
"_meta": {
|
|
145
|
+
"columns": {},
|
|
146
|
+
"schemas": {},
|
|
147
|
+
"tables": {}
|
|
148
|
+
}
|
|
149
|
+
}
|