biz-a-cli 2.3.80-15419 → 2.3.80
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/bin/app.js +20 -255
- package/bin/hub.js +0 -16
- package/bin/hubEvent.js +5 -98
- package/bin/tunnelTarget.js +0 -4
- package/engine/bpm/workflow.js +6 -7
- package/engine/cc/configValidate.js +0 -53
- package/engine/cc/guides/V2-SCREEN-AUTHORING.md +512 -531
- package/engine/cc/layer3Behavior.js +1 -2
- package/engine/cc/layer3Schema.js +1 -3
- package/engine/cc/livePipelines.js +3 -9
- package/engine/cc/publish.js +105 -86
- package/engine/domain/appConfig.js +18 -108
- package/engine/domain/applicationConfig.js +12 -87
- package/engine/domain/dialect.js +0 -138
- package/engine/domain/domainNavigation.js +50 -86
- package/engine/domain/entityConfigLoader.js +4 -16
- package/engine/domain/numericScale.js +0 -2
- package/engine/domain/system.js +0 -2
- package/engine/ext/publicTheme.js +1 -8
- package/engine/orm/apiRoute.js +61 -187
- package/engine/orm/entityConfig.js +2 -32
- package/engine/orm/finaAuth.js +0 -0
- package/engine/orm/finaEndpoint.js +0 -19
- package/engine/orm/imgQuery.js +0 -92
- package/engine/orm/listQuery.js +1 -6
- package/package.json +1 -1
- package/worker/cliScriptWorker.js +0 -5
- package/engine/attachments/store.js +0 -195
- package/engine/domain/domainRegistration.js +0 -65
- package/engine/fina/approval.js +0 -141
- package/engine/fina/approvalService.js +0 -59
- package/engine/fina/attachments.js +0 -165
- package/engine/fina/client.js +0 -84
- package/engine/fina/config.js +0 -26
- package/engine/fina/context.js +0 -58
- package/engine/fina/document.js +0 -67
- package/engine/fina/documentWrite.js +0 -173
- package/engine/fina/documents/index.js +0 -28
- package/engine/fina/documents/jv.js +0 -105
- package/engine/fina/documents/jvWrite.js +0 -260
- package/engine/fina/documents/si.js +0 -40
- package/engine/fina/errors.js +0 -17
- package/engine/fina/features.js +0 -86
- package/engine/fina/guard.js +0 -102
- package/engine/fina/identity.js +0 -87
- package/engine/fina/live.js +0 -19
- package/engine/fina/lock.js +0 -35
- package/engine/fina/master.js +0 -331
- package/engine/fina/masters/coa.js +0 -104
- package/engine/fina/masters/currency.js +0 -39
- package/engine/fina/masters/department.js +0 -32
- package/engine/fina/masters/index.js +0 -21
- package/engine/fina/masters/project.js +0 -53
- package/engine/fina/period.js +0 -56
- package/engine/fina/print.js +0 -136
- package/engine/fina/readPolicy.js +0 -67
- package/engine/fina/recurring.js +0 -200
- package/engine/fina/route.js +0 -61
- package/engine/fina/routes/attachment.js +0 -41
- package/engine/fina/routes/document.js +0 -66
- package/engine/fina/routes/master.js +0 -26
- package/engine/fina/routes/period.js +0 -24
- package/engine/fina/routes/print.js +0 -19
- package/engine/fina/routes/recurring.js +0 -27
- package/engine/fina/routes/session.js +0 -23
- package/engine/fina/routes/setup.js +0 -58
- package/engine/fina/routes/submission.js +0 -46
- package/engine/fina/routes/template.js +0 -31
- package/engine/fina/settings.js +0 -159
- package/engine/fina/sql.js +0 -34
- package/engine/fina/submission.js +0 -290
- package/engine/fina/submissionCompare.js +0 -171
- package/engine/fina/submissionRepo.js +0 -114
- package/engine/fina/template.js +0 -118
- package/engine/fina/userMap.js +0 -79
- package/engine/orm/companyLocale.js +0 -74
- package/engine/orm/userApiDataset.js +0 -153
- package/migrations/postgres/1790600000000__fina.sql +0 -33
- package/migrations/postgres/1790700000000__attachment.sql +0 -29
- package/migrations/postgres/1790800000000__fina_jv_submission.sql +0 -41
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 M5a — how a Journal Voucher is WRITTEN (contract §2.1, §2.1a; appendix F-03). Server-owned: what a browser sends
|
|
3
|
-
* is a request; the numbers, the order and the shape FINA receives are made here.
|
|
4
|
-
*
|
|
5
|
-
* - normalize: debit / credit → one signed GLAMOUNT (4 decimals, C19); PRIMEAMOUNT = base ÷ rate (D6, C16); blank
|
|
6
|
-
* lines dropped (V21); each problem named by the request's own line number.
|
|
7
|
-
* - validate: what needs FINA's data — the accounts (V25), the rate of the default currency (#7) or of a fixed rate
|
|
8
|
-
* (F-18), the subsidiary of an AR / AP line (V28–V30), departments and projects (V36, V38, D5), and the balance,
|
|
9
|
-
* in total (V20) and per department / project (F-05, D7).
|
|
10
|
-
* - payload: json2fina's data. ⚠️ ORDER MATTERS (spike 2026-10-10): a line is GLAccount, Rate, PrimeAmount, GLAmount —
|
|
11
|
-
* base last, or FINA recomputes it from prime; on edit EVERY line sends `seq` first (a new one max+1 …) — sent
|
|
12
|
-
* account-first, a new line overwrites a line on the same account; a FINA line no longer carried is deleted.
|
|
13
|
-
*/
|
|
14
|
-
import { createHash } from "node:crypto";
|
|
15
|
-
import { toDateOnly } from "../lock.js";
|
|
16
|
-
import { sqlString } from "../sql.js";
|
|
17
|
-
|
|
18
|
-
const round4 = (n) => Math.round(Number(n) * 10000) / 10000;
|
|
19
|
-
const TOLERANCE = 0.0005;
|
|
20
|
-
const MAX_AMOUNT = 1e14;
|
|
21
|
-
const TEXT_MAX = 80;
|
|
22
|
-
const AR = 8;
|
|
23
|
-
const AP = 13;
|
|
24
|
-
|
|
25
|
-
/* A number from the request: a number, or invariant text ("1500.5"). Empty → null; anything else → NaN. */
|
|
26
|
-
const num = (v) => {
|
|
27
|
-
if (v === null || v === undefined || v === "") return null;
|
|
28
|
-
if (typeof v === "number") return v;
|
|
29
|
-
const s = String(v).trim();
|
|
30
|
-
if (s === "") return null;
|
|
31
|
-
return /^[+-]?\d+(\.\d+)?$/.test(s) ? Number(s) : NaN;
|
|
32
|
-
};
|
|
33
|
-
const text = (v) => {
|
|
34
|
-
const s = v === null || v === undefined ? "" : String(v).trim();
|
|
35
|
-
return s === "" ? null : s;
|
|
36
|
-
};
|
|
37
|
-
const extOf = (row) => Object.fromEntries(Object.entries(row ?? {}).filter(([k]) => /^ext_(lookup|customfield)\d+$/.test(k)));
|
|
38
|
-
|
|
39
|
-
export const normalize = (masterIn = {}, linesIn = []) => {
|
|
40
|
-
const problems = [];
|
|
41
|
-
const transdate = toDateOnly(masterIn?.transdate);
|
|
42
|
-
if (!transdate) problems.push("The date is required.");
|
|
43
|
-
const transdescription = text(masterIn?.transdescription);
|
|
44
|
-
if (transdescription && transdescription.length > TEXT_MAX) problems.push(`The description is longer than ${TEXT_MAX} characters.`);
|
|
45
|
-
const templateid = Number.isInteger(Number(masterIn?.templateid)) && Number(masterIn?.templateid) > 0 ? Number(masterIn.templateid) : null;
|
|
46
|
-
|
|
47
|
-
const lines = [];
|
|
48
|
-
(Array.isArray(linesIn) ? linesIn : []).forEach((raw, i) => {
|
|
49
|
-
const n = i + 1;
|
|
50
|
-
const glaccount = text(raw?.glaccount);
|
|
51
|
-
const debit = num(raw?.debit);
|
|
52
|
-
const credit = num(raw?.credit);
|
|
53
|
-
if (!glaccount && !debit && !credit) return;
|
|
54
|
-
if (!glaccount) problems.push(`Line ${n}: the account is required.`);
|
|
55
|
-
if (Number.isNaN(debit) || Number.isNaN(credit)) {
|
|
56
|
-
problems.push(`Line ${n}: the amount is not a number.`);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
if (debit && credit) {
|
|
60
|
-
problems.push(`Line ${n}: has both a debit and a credit.`);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const base = round4((debit ?? 0) - (credit ?? 0));
|
|
64
|
-
if (Math.abs(base) >= MAX_AMOUNT) problems.push(`Line ${n}: the amount is too big.`);
|
|
65
|
-
else if (base === 0) problems.push(`Line ${n}: the amount is zero.`);
|
|
66
|
-
const rateIn = num(raw?.rate);
|
|
67
|
-
const rate = rateIn === null ? 1 : rateIn;
|
|
68
|
-
if (Number.isNaN(rate) || rate < 0) problems.push(`Line ${n}: the rate is not a number.`);
|
|
69
|
-
else if (rate === 0) problems.push(`Line ${n}: the rate cannot be zero.`);
|
|
70
|
-
const description = text(raw?.description);
|
|
71
|
-
if (description && description.length > TEXT_MAX) problems.push(`Line ${n}: the memo is longer than ${TEXT_MAX} characters.`);
|
|
72
|
-
const seq = Number.isInteger(Number(raw?.seq)) && raw?.seq !== null && raw?.seq !== "" && Number(raw.seq) >= 0 ? Number(raw.seq) : null;
|
|
73
|
-
lines.push({
|
|
74
|
-
n,
|
|
75
|
-
seq,
|
|
76
|
-
glaccount,
|
|
77
|
-
glamount: base,
|
|
78
|
-
rate: Number.isNaN(rate) ? 1 : rate,
|
|
79
|
-
primeamount: rate > 0 ? round4(base / rate) : base,
|
|
80
|
-
description,
|
|
81
|
-
personno: text(raw?.personno),
|
|
82
|
-
deptno: text(raw?.deptno),
|
|
83
|
-
projectno: text(raw?.projectno),
|
|
84
|
-
...extOf(raw),
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
if (lines.length === 0 && !problems.some((p) => p.startsWith("Line "))) problems.push("The detail has not been entered.");
|
|
88
|
-
return { master: { transdate, transdescription, templateid, ...extOf(masterIn) }, lines, problems };
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const inList = (values) => [...new Set(values)].map((v) => sqlString(v)).join(", ");
|
|
92
|
-
const yes = (v) => v === true || v === 1 || String(v ?? "").trim() === "1";
|
|
93
|
-
|
|
94
|
-
/* Everything that needs FINA's data, one query per kind; every problem in one list. */
|
|
95
|
-
export const validate = async ({ client, lines, settings, features }) => {
|
|
96
|
-
const problems = [];
|
|
97
|
-
const jv = settings?.jv ?? {};
|
|
98
|
-
const multiDept = !!features?.multiDept;
|
|
99
|
-
const multiProject = !!features?.multiProject;
|
|
100
|
-
|
|
101
|
-
const company = (await client.executeSql("SELECT DEFAULTCURRENCYID FROM COMPANY"))?.[0] ?? {};
|
|
102
|
-
const defaultCurrency = Number(company.defaultcurrencyid ?? 1);
|
|
103
|
-
|
|
104
|
-
const accounts = new Map();
|
|
105
|
-
const codes = lines.map((l) => l.glaccount).filter(Boolean);
|
|
106
|
-
if (codes.length > 0) {
|
|
107
|
-
const rows =
|
|
108
|
-
(await client.executeSql(
|
|
109
|
-
"SELECT A.GLACCOUNT, A.CURRENCYID, A.ACCOUNTTYPE, A.SUSPENDED, C.EXCHANGERATE, " +
|
|
110
|
-
"(SELECT COUNT(*) FROM GLACCNT S WHERE S.PARENTACCOUNT = A.GLACCOUNT) AS CHILDREN " +
|
|
111
|
-
`FROM GLACCNT A LEFT JOIN CURRENCY C ON C.CURRENCYID = A.CURRENCYID WHERE A.GLACCOUNT IN (${inList(codes)})`,
|
|
112
|
-
)) ?? [];
|
|
113
|
-
for (const r of rows) accounts.set(String(r.glaccount).trim(), r);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const persons = new Map();
|
|
117
|
-
const personCodes = lines.map((l) => l.personno).filter(Boolean);
|
|
118
|
-
if (personCodes.length > 0) {
|
|
119
|
-
const rows = (await client.executeSql(`SELECT PERSONNO, CURRENCYID, SUSPENDED FROM PERSONDATA WHERE PERSONNO IN (${inList(personCodes)})`)) ?? [];
|
|
120
|
-
for (const r of rows) persons.set(String(r.personno).trim(), r);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const depts = new Map();
|
|
124
|
-
const deptCodes = lines.map((l) => l.deptno).filter(Boolean);
|
|
125
|
-
if (deptCodes.length > 0) {
|
|
126
|
-
const rows =
|
|
127
|
-
(await client.executeSql(
|
|
128
|
-
"SELECT D.DEPTNO, D.SUSPENDED, (SELECT COUNT(*) FROM DEPARTMENT S WHERE S.SUBDEPTID = D.DEPTID AND S.DEPTID <> D.DEPTID) AS CHILDREN " +
|
|
129
|
-
`FROM DEPARTMENT D WHERE D.DEPTNO IN (${inList(deptCodes)})`,
|
|
130
|
-
)) ?? [];
|
|
131
|
-
for (const r of rows) depts.set(String(r.deptno).trim(), r);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const projects = new Map();
|
|
135
|
-
const projectCodes = lines.map((l) => l.projectno).filter(Boolean);
|
|
136
|
-
if (projectCodes.length > 0) {
|
|
137
|
-
const rows =
|
|
138
|
-
(await client.executeSql(
|
|
139
|
-
"SELECT P.PROJECTNO, P.SUSPENDED, P.FINISHED, (SELECT COUNT(*) FROM PROJECT S WHERE S.SUBPROJECTID = P.PROJECTID AND S.PROJECTID <> P.PROJECTID) AS CHILDREN " +
|
|
140
|
-
`FROM PROJECT P WHERE P.PROJECTNO IN (${inList(projectCodes)})`,
|
|
141
|
-
)) ?? [];
|
|
142
|
-
for (const r of rows) projects.set(String(r.projectno).trim(), r);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
for (const l of lines) {
|
|
146
|
-
const at = `Line ${l.n}`;
|
|
147
|
-
if (l.glaccount) {
|
|
148
|
-
const a = accounts.get(l.glaccount);
|
|
149
|
-
if (!a) problems.push(`${at}: account ${l.glaccount} does not exist in FINA.`);
|
|
150
|
-
else {
|
|
151
|
-
if (yes(a.suspended)) problems.push(`${at}: account ${l.glaccount} is suspended.`);
|
|
152
|
-
if (Number(a.children) > 0) problems.push(`${at}: account ${l.glaccount} is a parent account; use one of its sub-accounts.`);
|
|
153
|
-
const currency = a.currencyid === null || a.currencyid === undefined ? defaultCurrency : Number(a.currencyid);
|
|
154
|
-
if (currency === defaultCurrency && l.rate !== 1) problems.push(`${at}: the rate of an account in the default currency is 1.`);
|
|
155
|
-
else if (currency !== defaultCurrency && jv.rateEditable === false && l.rate !== Number(a.exchangerate)) {
|
|
156
|
-
problems.push(`${at}: the rate is fixed at the currency rate ${Number(a.exchangerate)}.`);
|
|
157
|
-
}
|
|
158
|
-
const type = Number(a.accounttype);
|
|
159
|
-
if ((type === AR || type === AP) && !l.personno) problems.push(`${at}: account ${l.glaccount} needs a subsidiary (customer or vendor).`);
|
|
160
|
-
if (l.personno) {
|
|
161
|
-
const p = persons.get(l.personno);
|
|
162
|
-
if (!p) problems.push(`${at}: subsidiary ${l.personno} does not exist in FINA.`);
|
|
163
|
-
else {
|
|
164
|
-
if (yes(p.suspended)) problems.push(`${at}: subsidiary ${l.personno} is suspended.`);
|
|
165
|
-
const personCurrency = p.currencyid === null || p.currencyid === undefined ? defaultCurrency : Number(p.currencyid);
|
|
166
|
-
if (personCurrency !== currency) problems.push(`${at}: the currency of subsidiary ${l.personno} does not match the account's.`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
if (l.deptno) {
|
|
172
|
-
const d = depts.get(l.deptno);
|
|
173
|
-
if (!d) problems.push(`${at}: department ${l.deptno} does not exist in FINA.`);
|
|
174
|
-
else if (yes(d.suspended)) problems.push(`${at}: department ${l.deptno} is suspended.`);
|
|
175
|
-
else if (Number(d.children) > 0) problems.push(`${at}: department ${l.deptno} is a parent; use one of its sub-departments.`);
|
|
176
|
-
} else if (multiDept && jv.deptMandatory) problems.push(`${at}: the department is required.`);
|
|
177
|
-
if (l.projectno) {
|
|
178
|
-
const p = projects.get(l.projectno);
|
|
179
|
-
if (!p) problems.push(`${at}: project ${l.projectno} does not exist in FINA.`);
|
|
180
|
-
else if (yes(p.suspended)) problems.push(`${at}: project ${l.projectno} is suspended.`);
|
|
181
|
-
else if (yes(p.finished)) problems.push(`${at}: project ${l.projectno} is finished.`);
|
|
182
|
-
else if (Number(p.children) > 0) problems.push(`${at}: project ${l.projectno} is a parent; use one of its sub-projects.`);
|
|
183
|
-
} else if (multiProject && jv.projectMandatory) problems.push(`${at}: the project is required.`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const out = (sum) => round4(sum);
|
|
187
|
-
const total = out(lines.reduce((s, l) => s + l.glamount, 0));
|
|
188
|
-
if (Math.abs(total) > TOLERANCE) problems.push(`Debits and credits are out of balance by ${Math.abs(total)}.`);
|
|
189
|
-
const byDimension = (key, label) => {
|
|
190
|
-
const sums = new Map();
|
|
191
|
-
for (const l of lines) if (l[key]) sums.set(l[key], (sums.get(l[key]) ?? 0) + l.glamount);
|
|
192
|
-
for (const [code, sum] of sums) if (Math.abs(out(sum)) > TOLERANCE) problems.push(`${label} ${code} is out of balance by ${Math.abs(out(sum))}.`);
|
|
193
|
-
};
|
|
194
|
-
if (multiDept) byDimension("deptno", "Department");
|
|
195
|
-
if (multiProject) byDimension("projectno", "Project");
|
|
196
|
-
return problems;
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const str = (v) => (v === null || v === undefined ? null : String(v));
|
|
200
|
-
const extKeyOf = (mapping) => `extended.${mapping.field}`;
|
|
201
|
-
|
|
202
|
-
const lineFields = (l, before, isEdit, mappings) => {
|
|
203
|
-
const out = { GLAccount: str(l.glaccount), Rate: str(l.rate), PrimeAmount: str(l.primeamount), GLAmount: str(l.glamount) };
|
|
204
|
-
/* An optional value: sent when set; on an edited line also when it was set and is now cleared (null). */
|
|
205
|
-
const optional = (name, value, was) => {
|
|
206
|
-
if (value !== null && value !== undefined && value !== "") out[name] = String(value);
|
|
207
|
-
else if (isEdit && was !== null && was !== undefined && was !== "") out[name] = null;
|
|
208
|
-
};
|
|
209
|
-
optional("Description", l.description, before?.description);
|
|
210
|
-
optional("PersonNo", l.personno, before?.personno);
|
|
211
|
-
optional("DeptNo", l.deptno, before?.deptno);
|
|
212
|
-
optional("ProjectNo", l.projectno, before?.projectno);
|
|
213
|
-
for (const m of mappings.filter((x) => x.part === "detail")) optional(extKeyOf(m), l[m.key], before?.[m.key]);
|
|
214
|
-
return out;
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
export const payload = ({ isNew, master, lines, current, mappings = [] }) => {
|
|
218
|
-
const m = { TransDate: master.transdate, TransDescription: master.transdescription ?? "" };
|
|
219
|
-
if (master.templateid) m.TemplateID = String(master.templateid);
|
|
220
|
-
for (const mapping of mappings.filter((x) => x.part === "master")) {
|
|
221
|
-
if (!(mapping.key in master)) continue;
|
|
222
|
-
const v = master[mapping.key];
|
|
223
|
-
if (v === null || v === undefined || v === "") {
|
|
224
|
-
if (!isNew) m[extKeyOf(mapping)] = null;
|
|
225
|
-
} else m[extKeyOf(mapping)] = String(v);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (isNew) {
|
|
229
|
-
m.detail = lines.map((l) => lineFields(l, null, false, mappings));
|
|
230
|
-
return { master: m };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const existing = new Map((current?.lines ?? []).map((l) => [Number(l.seq), l]));
|
|
234
|
-
let next = Math.max(-1, ...existing.keys()) + 1;
|
|
235
|
-
const carried = new Set();
|
|
236
|
-
const detail = lines.map((l) => {
|
|
237
|
-
if (l.seq !== null && existing.has(l.seq) && !carried.has(l.seq)) {
|
|
238
|
-
carried.add(l.seq);
|
|
239
|
-
return { seq: String(l.seq), ...lineFields(l, existing.get(l.seq), true, mappings) };
|
|
240
|
-
}
|
|
241
|
-
return { seq: String(next++), ...lineFields(l, null, false, mappings) };
|
|
242
|
-
});
|
|
243
|
-
for (const seq of existing.keys()) if (!carried.has(seq)) detail.push({ seq: String(seq), delete: "1" });
|
|
244
|
-
m.detail = detail;
|
|
245
|
-
return { master: m };
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
/* What a JV looked like when it was read: an edit sent against another version is refused (DOCUMENT_CHANGED). */
|
|
249
|
-
export const version = (document) => {
|
|
250
|
-
const m = document?.master ?? {};
|
|
251
|
-
const core = {
|
|
252
|
-
d: toDateOnly(m.transdate),
|
|
253
|
-
t: m.transdescription ?? null,
|
|
254
|
-
p: m.templateid ?? null,
|
|
255
|
-
l: (document?.lines ?? []).map((l) => [l.seq, l.glaccount, Number(l.glamount), Number(l.rate), Number(l.primeamount), l.description ?? null, l.deptno ?? null, l.projectno ?? null, l.personno ?? null]),
|
|
256
|
-
};
|
|
257
|
-
return createHash("sha1").update(JSON.stringify(core)).digest("hex").slice(0, 16);
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
export default { normalize, validate, payload, version };
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 M4 — the Sales Invoice: the proof of T-03 (a second form from its descriptor, with no change to the template
|
|
3
|
-
* engine). Read and printed only; Biz-A writes no invoice. Rights: FINA's SALES columns (C/E/R).
|
|
4
|
-
*
|
|
5
|
-
* `ARINVOICEID` drives its report (verified live on template 5, 2026-10-10).
|
|
6
|
-
*/
|
|
7
|
-
const number = (v) => (v === null || v === undefined || v === "" ? null : Number(v));
|
|
8
|
-
|
|
9
|
-
export default {
|
|
10
|
-
name: "si",
|
|
11
|
-
label: "Sales Invoice",
|
|
12
|
-
rights: "sales",
|
|
13
|
-
templateType: 0,
|
|
14
|
-
master: {
|
|
15
|
-
table: "ARINV",
|
|
16
|
-
key: "ARINVOICEID",
|
|
17
|
-
select:
|
|
18
|
-
"M.ARINVOICEID, M.INVOICENO, M.INVOICEDATE, P.PERSONNO AS CUSTOMERNO, P.NAME AS CUSTOMERNAME, M.FOB, M.SHIPVIA, M.SHIPDATE, " +
|
|
19
|
-
"M.PURCHASEORDERNO, M.DESCRIPTION, M.INVOICEAMOUNT, M.TEMPLATEID, M.EXTENDEDID",
|
|
20
|
-
from: "ARINV M LEFT JOIN PERSONDATA P ON P.ID = M.CUSTOMERID",
|
|
21
|
-
filter: null,
|
|
22
|
-
},
|
|
23
|
-
detail: {
|
|
24
|
-
select: "D.SEQ, D.ITEMNO, I.ITEMDESCRIPTION, D.QUANTITY, D.ITEMUNIT, D.UNITPRICE, D.ITEMDISCPC, D.TAXCODES, D.EXTENDEDID",
|
|
25
|
-
from: "ARINVDET D LEFT JOIN ITEM I ON I.ITEMNO = D.ITEMNO",
|
|
26
|
-
key: "D.ARINVOICEID",
|
|
27
|
-
order: "D.SEQ",
|
|
28
|
-
/* The line amount as the screen shows it; a discount FINA keeps as a formula (10+5) is left to FINA. */
|
|
29
|
-
line: (row) => {
|
|
30
|
-
const gross = (number(row.quantity) ?? 0) * (number(row.unitprice) ?? 0);
|
|
31
|
-
const text = String(row.itemdiscpc ?? "").trim();
|
|
32
|
-
const disc = /^\d+(\.\d+)?$/.test(text) ? Number(text) : 0;
|
|
33
|
-
return { ...row, amount: Math.round(gross * (1 - disc / 100) * 10000) / 10000 };
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
extended: { master: "ARINV", detail: "ARINVDET" },
|
|
37
|
-
print: {
|
|
38
|
-
dictionary: async ({ id }) => [{ param: "ARINVOICEID", value: String(id) }],
|
|
39
|
-
},
|
|
40
|
-
};
|
package/engine/fina/errors.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 — engine/fina's errors carry a CODE, which is what a route answers with (`{ok:false, code, error}`).
|
|
3
|
-
*
|
|
4
|
-
* - UNAUTHENTICATED / FORBIDDEN / NOT_CONFIGURED / FINA_CLOUD_NEEDS_POSTGRES: who may call.
|
|
5
|
-
* - INVALID: the payload, with every reason in the message.
|
|
6
|
-
* - FINA_UNREACHABLE / FINA_ERROR / FINA_REFUSED: the FINA API (down, broken, or said no — its own message).
|
|
7
|
-
* - FEATURE_IN_USE / FEATURES_CHANGED: the features console (C-12, a concurrent desktop write).
|
|
8
|
-
*/
|
|
9
|
-
export class FinaError extends Error {
|
|
10
|
-
constructor(code, message) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = "FinaError";
|
|
13
|
-
this.code = code;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const finaError = (code, message) => new FinaError(code, message);
|
package/engine/fina/features.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 C-04 / C-05 / C-12, D3 — the FINA features the console shows and sets.
|
|
3
|
-
*
|
|
4
|
-
* - Multi Department and Multi Project are characters 9 and 10 of COMPANY.RESERVED1, in Delphi's 1-based string
|
|
5
|
-
* index (BankConst.pas: idxMultiDept = 9, idxMultiProject = 10, RESERVED1LEN = 19; ReadWriteFeatures.pas). The
|
|
6
|
-
* console reads and writes the very column the desktop does, so both always agree (C-05).
|
|
7
|
-
* - Multi Currency has NO flag (D3): it is active when CURRENCY holds more than one currency, and the console only
|
|
8
|
-
* shows it.
|
|
9
|
-
*
|
|
10
|
-
* ⚠️ ONLY CHARACTERS 9 AND 10 ARE WRITTEN. The desktop's Write_Features rebuilds the whole string from every flag it
|
|
11
|
-
* knows; Biz-A knows two, so it keeps every other character exactly as stored (warehouse, serial numbers, rounding…).
|
|
12
|
-
*
|
|
13
|
-
* ⚠️ OPTIMISTIC: the UPDATE matches the value read, then the row is read again. If the desktop wrote in between, the
|
|
14
|
-
* answer is FEATURES_CHANGED ("reload"), never a silent overwrite of the desktop's change.
|
|
15
|
-
*
|
|
16
|
-
* ⚠️ C-12: switching a feature OFF is refused while GL history carries a department / project. Switching ON is
|
|
17
|
-
* always allowed. (The desktop itself has no such check; this is Biz-A's rule.)
|
|
18
|
-
*/
|
|
19
|
-
import { finaError } from "./errors.js";
|
|
20
|
-
|
|
21
|
-
const POSITION = { multiDept: 9, multiProject: 10 };
|
|
22
|
-
const DIGITS = /^[0-9]*$/;
|
|
23
|
-
|
|
24
|
-
const flagAt = (raw, position) => String(raw ?? "").charAt(position - 1) === "1";
|
|
25
|
-
|
|
26
|
-
export const readFlags = (raw) => ({ multiDept: flagAt(raw, POSITION.multiDept), multiProject: flagAt(raw, POSITION.multiProject) });
|
|
27
|
-
|
|
28
|
-
export const withFlags = (raw, { multiDept, multiProject }) => {
|
|
29
|
-
const chars = String(raw ?? "").padEnd(POSITION.multiProject, "0").split("");
|
|
30
|
-
chars[POSITION.multiDept - 1] = multiDept ? "1" : "0";
|
|
31
|
-
chars[POSITION.multiProject - 1] = multiProject ? "1" : "0";
|
|
32
|
-
return chars.join("");
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const IN_USE = {
|
|
36
|
-
multiDept: { column: "DEPTID", label: "Multi Department", what: "a department" },
|
|
37
|
-
multiProject: { column: "PROJECTID", label: "Multi Project", what: "a project" },
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export const createFeatures = ({ client }) => {
|
|
41
|
-
const readRaw = async () => {
|
|
42
|
-
const rows = await client.executeSql("SELECT RESERVED1 FROM COMPANY");
|
|
43
|
-
const value = rows?.[0]?.reserved1;
|
|
44
|
-
return value === undefined ? null : value;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const read = async () => {
|
|
48
|
-
const raw = await readRaw();
|
|
49
|
-
const count = Number((await client.executeSql("SELECT COUNT(*) AS N FROM CURRENCY"))?.[0]?.n ?? 0);
|
|
50
|
-
return { ...readFlags(raw), multiCurrency: { active: count > 1, count } };
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const save = async ({ multiDept, multiProject } = {}) => {
|
|
54
|
-
if (typeof multiDept !== "boolean" || typeof multiProject !== "boolean") {
|
|
55
|
-
throw finaError("INVALID", "multiDept and multiProject must be true or false.");
|
|
56
|
-
}
|
|
57
|
-
const raw = await readRaw();
|
|
58
|
-
if (raw !== null && !DIGITS.test(String(raw))) {
|
|
59
|
-
throw finaError("INVALID", "COMPANY.RESERVED1 holds something other than digits; it is left as it is. Check it from FINA desktop.");
|
|
60
|
-
}
|
|
61
|
-
const current = readFlags(raw);
|
|
62
|
-
const wanted = { multiDept, multiProject };
|
|
63
|
-
|
|
64
|
-
for (const key of ["multiDept", "multiProject"]) {
|
|
65
|
-
if (!current[key] || wanted[key]) continue;
|
|
66
|
-
const { column, label, what } = IN_USE[key];
|
|
67
|
-
const used = await client.executeSql(`SELECT FIRST 1 1 AS X FROM GLHIST WHERE ${column} IS NOT NULL`);
|
|
68
|
-
if (used.length > 0) {
|
|
69
|
-
throw finaError("FEATURE_IN_USE", `${label} cannot be switched off: the general ledger already has entries with ${what}.`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/* Nothing to change → nothing written. */
|
|
74
|
-
if (current.multiDept === multiDept && current.multiProject === multiProject) return read();
|
|
75
|
-
|
|
76
|
-
const next = withFlags(raw, wanted);
|
|
77
|
-
const old = raw === null ? "NULL" : `'${raw}'`;
|
|
78
|
-
await client.block(`EXECUTE BLOCK AS BEGIN UPDATE COMPANY SET RESERVED1 = '${next}' WHERE RESERVED1 IS NOT DISTINCT FROM ${old}; END`);
|
|
79
|
-
if ((await readRaw()) !== next) {
|
|
80
|
-
throw finaError("FEATURES_CHANGED", "The FINA features were changed meanwhile (from FINA desktop?). Reload and try again.");
|
|
81
|
-
}
|
|
82
|
-
return read();
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
return { read, save };
|
|
86
|
-
};
|
package/engine/fina/guard.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 D2 / S-03 — a browser may not write to the FINA Cloud database directly.
|
|
3
|
-
*
|
|
4
|
-
* Every FINA write from Biz-A goes through engine/fina: verified user → FINA user → right → Biz-A date lock → FINA
|
|
5
|
-
* API (which adds FINA's own period lock). The CLI also forwards a browser's raw DataSnap calls to FINA
|
|
6
|
-
* (bin/hubEvent.js forwardToApi, engine/orm/finaEndpoint.js); left open, one hand-made `json2fina` or `block` would
|
|
7
|
-
* skip all of it. So, for the FINA Cloud database only, this lets READS through and refuses everything else.
|
|
8
|
-
*
|
|
9
|
-
* - Reads: `list`, `lookup`, `lookuptotal`, and `orm` with method `get`. Login (`get_valid_user_id`) is a read of its
|
|
10
|
-
* own and always passes.
|
|
11
|
-
* - ⚠️⚠️ A request is FINA-bound when ANY database index it carries is FINA's — the lookup's, the object's or its own,
|
|
12
|
-
* `dbIndex` or the legacy `dbindex`. Routing takes them in that order (engine/orm/index.js resolveDbIndex, Delphi's
|
|
13
|
-
* order too), so a browser reading FINA puts the index INSIDE the object while the outer one names its tenant.
|
|
14
|
-
* Reading only the outer one let an `orm put` with the tenant outside and FINA inside through to FINA (found
|
|
15
|
-
* planning Doc 8 M3).
|
|
16
|
-
* - ⚠️ A read that names FINA's USERS table anywhere (a column, a join, a filter subquery) is refused: it holds every
|
|
17
|
-
* FINA user's password and rights (Agung 2026-10-09; the full read policy is Doc 8 M5).
|
|
18
|
-
* - Refused: `block`, `orm` put / delete / deletemd, `ExecuteSQL`, `genId`, `json2fina`, `template` — anything not
|
|
19
|
-
* on the read list, including endpoints this file has never heard of.
|
|
20
|
-
* - ⚠️ FAIL CLOSED: while FINA Cloud is on, a FINA-bound request whose database cannot be read from it is refused.
|
|
21
|
-
* An unreadable request is exactly what a bypass would look like; a legitimate one can be taught to the parsers.
|
|
22
|
-
* - Other databases are untouched, and with FINA Cloud off (no --finaCloudDbindex) this does nothing at all.
|
|
23
|
-
*
|
|
24
|
-
* The request is parsed with the ORM router's own parsers (engine/orm/apiRoute.js), so the guard and the router
|
|
25
|
-
* can never disagree about what a request is.
|
|
26
|
-
*/
|
|
27
|
-
import { parseAuthRequest, parseDataSnapRequest, parseExecuteSqlRequest, parseGenIdRequest } from "../orm/apiRoute.js";
|
|
28
|
-
|
|
29
|
-
export const FINA_WRITE_REFUSED = "FINA_WRITE_REFUSED";
|
|
30
|
-
export const FINA_READ_REFUSED = "FINA_READ_REFUSED";
|
|
31
|
-
|
|
32
|
-
const ENDPOINT = /\/fina\/rest\/\w+\/(?:%22|")(\w+)(?:%22|")/i;
|
|
33
|
-
const endpointOf = (path) => ENDPOINT.exec(String(path ?? ""))?.[1] ?? String(path ?? "");
|
|
34
|
-
|
|
35
|
-
const refuse = (what) => ({
|
|
36
|
-
status: 403,
|
|
37
|
-
body: {
|
|
38
|
-
code: FINA_WRITE_REFUSED,
|
|
39
|
-
error:
|
|
40
|
-
`FINA Cloud: ${what} is refused. Changes to the FINA database go through Biz-A's FINA engine, ` +
|
|
41
|
-
"which checks the user's FINA rights and the date lock; they cannot be sent to FINA directly.",
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const READ_ENDPOINTS = new Set(["list", "lookup", "lookuptotal"]);
|
|
46
|
-
const isRead = ({ endpoint, param }) => READ_ENDPOINTS.has(endpoint) || (endpoint === "orm" && String(param?.method ?? "").toLowerCase() === "get");
|
|
47
|
-
|
|
48
|
-
/* Every database index the request carries, wherever the router could take one from. */
|
|
49
|
-
const INDEX_KEYS = ["dbIndex", "dbindex"];
|
|
50
|
-
const indexesOf = (param) => {
|
|
51
|
-
const found = [];
|
|
52
|
-
for (const holder of [param, param?.object, param?.lookup]) {
|
|
53
|
-
if (!holder || typeof holder !== "object") continue;
|
|
54
|
-
for (const key of INDEX_KEYS) {
|
|
55
|
-
const value = Number(holder[key]);
|
|
56
|
-
if (Number.isInteger(value) && value > 0) found.push(value);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return found;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const USERS_TABLE = /\bUSERS\b/i;
|
|
63
|
-
const refuseUsersRead = () => ({
|
|
64
|
-
status: 403,
|
|
65
|
-
body: {
|
|
66
|
-
code: FINA_READ_REFUSED,
|
|
67
|
-
error: "FINA Cloud: reading FINA's USERS table from the browser is refused.",
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
/*
|
|
72
|
-
* Doc 8 M8 — is this a browser READ of the FINA Cloud database (`target`)? The read policy (readPolicy.js) asks HERE,
|
|
73
|
-
* with the guard's own parsing, so the two can never disagree about what a request is. A login is not one.
|
|
74
|
-
*/
|
|
75
|
-
export const finaBoundRead = (path, body, target) => {
|
|
76
|
-
if (parseAuthRequest(path, body)) return false;
|
|
77
|
-
const request = parseDataSnapRequest(path, body);
|
|
78
|
-
if (!request) return false;
|
|
79
|
-
return indexesOf(request.param).includes(Number(target)) && isRead(request);
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
export const refuseFinaCloudWrite = ({ path, body, finaCloudDbIndex }) => {
|
|
83
|
-
const target = Number(finaCloudDbIndex);
|
|
84
|
-
if (finaCloudDbIndex === null || finaCloudDbIndex === undefined || finaCloudDbIndex === "" || !Number.isInteger(target) || target <= 0) {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (parseAuthRequest(path, body)) return null;
|
|
89
|
-
|
|
90
|
-
const genId = parseGenIdRequest(path);
|
|
91
|
-
if (genId) return genId.dbIndex === null || genId.dbIndex === target ? refuse("genId") : null;
|
|
92
|
-
|
|
93
|
-
const sql = parseExecuteSqlRequest(path, body);
|
|
94
|
-
if (sql) return sql.dbIndex === null || sql.dbIndex === target ? refuse("ExecuteSQL") : null;
|
|
95
|
-
|
|
96
|
-
const request = parseDataSnapRequest(path, body);
|
|
97
|
-
const indexes = request ? indexesOf(request.param) : [];
|
|
98
|
-
if (!request || indexes.length === 0) return refuse(`${endpointOf(path)} (its database cannot be read from the request)`);
|
|
99
|
-
if (!indexes.includes(target)) return null;
|
|
100
|
-
if (!isRead(request)) return refuse(request.endpoint === "orm" ? `orm ${String(request.param?.method ?? "")}` : request.endpoint);
|
|
101
|
-
return USERS_TABLE.test(JSON.stringify(request.param ?? {})) ? refuseUsersRead() : null;
|
|
102
|
-
};
|
package/engine/fina/identity.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 S-01 / S-02, D4 — who the caller is in FINA, and what they may do there.
|
|
3
|
-
*
|
|
4
|
-
* The VERIFIED Biz-A user (context.js; never an id from a request) → FINA$USER_MAP → FINA's USERS row.
|
|
5
|
-
*
|
|
6
|
-
* Rights are FINA's own columns, one per module × {C create, E edit, R report}. There is no delete or view
|
|
7
|
-
* right in FINA; the desktop uses E for delete (D4): create = C, edit = E, delete = E, print = R, view = every
|
|
8
|
-
* mapped user. Currency has no rights column — the desktop's Kurs menu needs user level 0 or 1 (appendix E-09).
|
|
9
|
-
*
|
|
10
|
-
* ⚠️ An inactive FINA user (USERPASSWORD '-1' or USERLEVEL -1, as FINA marks one) counts as UNMAPPED, so a FINA
|
|
11
|
-
* user switched off on the desktop loses Biz-A access too. USERPASSWORD is filtered on, never selected.
|
|
12
|
-
*/
|
|
13
|
-
import { finaError } from "./errors.js";
|
|
14
|
-
|
|
15
|
-
/* Doc 8 M4: SALES, the Sales Invoice (read and printed for the T-03 proof). */
|
|
16
|
-
const MODULES = { jv: "JV", coa: "COA", dept: "DEPT", proj: "PROJ", sales: "SALES" };
|
|
17
|
-
const NONE = Object.freeze({ create: false, edit: false, delete: false, print: false });
|
|
18
|
-
|
|
19
|
-
const on = (v) => v === true || v === 1 || String(v ?? "").trim() === "1";
|
|
20
|
-
|
|
21
|
-
export const ACTIVE_FINA_USER = "USERPASSWORD <> '-1' AND USERLEVEL <> -1";
|
|
22
|
-
|
|
23
|
-
export const rightsFromUserRow = (row = {}) => {
|
|
24
|
-
const rights = {};
|
|
25
|
-
for (const [key, prefix] of Object.entries(MODULES)) {
|
|
26
|
-
const c = on(row[`${prefix.toLowerCase()}c`]);
|
|
27
|
-
const e = on(row[`${prefix.toLowerCase()}e`]);
|
|
28
|
-
const r = on(row[`${prefix.toLowerCase()}r`]);
|
|
29
|
-
rights[key] = { create: c, edit: e, delete: e, print: r };
|
|
30
|
-
}
|
|
31
|
-
const level = Number(row.userlevel);
|
|
32
|
-
rights.currency = level === 0 || level === 1 ? { create: true, edit: true, delete: true, print: true } : { ...NONE };
|
|
33
|
-
return rights;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const RIGHT_COLUMNS = Object.values(MODULES)
|
|
37
|
-
.flatMap((p) => [`${p}C`, `${p}E`, `${p}R`])
|
|
38
|
-
.join(", ");
|
|
39
|
-
|
|
40
|
-
/*
|
|
41
|
-
* Doc 8 M3 — the server's own check of a FINA right (S-02, S-03; FINA's master datasets check none, appendix E-09).
|
|
42
|
-
* `action` null = only a mapped FINA user (D4: view is every mapped user). Answers the identity it was given.
|
|
43
|
-
*/
|
|
44
|
-
export const requireRight = (identity, module, action) => {
|
|
45
|
-
if (!identity?.mapped) {
|
|
46
|
-
throw finaError(
|
|
47
|
-
"FORBIDDEN",
|
|
48
|
-
identity?.reason ?? "Your Biz-A user is not mapped to a FINA user. An administrator maps it in the FINA Cloud console.",
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
if (action && identity.rights?.[module]?.[action] !== true) {
|
|
52
|
-
throw finaError("FORBIDDEN", `Your FINA user ${identity.finaUser?.name ?? ""} may not ${action} ${module} records.`.replace(/\s+/g, " "));
|
|
53
|
-
}
|
|
54
|
-
return identity;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export const verifiedUserId = (principal) => {
|
|
58
|
-
const raw = principal?.userId;
|
|
59
|
-
const id = Number(raw);
|
|
60
|
-
if (raw === null || raw === undefined || typeof raw === "boolean" || !Number.isInteger(id) || !/^\d+$/.test(String(raw))) {
|
|
61
|
-
throw finaError("UNAUTHENTICATED", "No verified Biz-A user — sign in again.");
|
|
62
|
-
}
|
|
63
|
-
return id;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export const createIdentity = ({ exec, client }) => ({
|
|
67
|
-
resolve: async (principal) => {
|
|
68
|
-
const userId = verifiedUserId(principal);
|
|
69
|
-
const mapped = (await exec(`SELECT FINA_USER_ID FROM FINA$USER_MAP WHERE BIZA_USER_ID = ${userId}`)) ?? [];
|
|
70
|
-
if (mapped.length === 0) return { mapped: false };
|
|
71
|
-
|
|
72
|
-
const finaUserId = Number(mapped[0].fina_user_id);
|
|
73
|
-
if (!Number.isInteger(finaUserId)) return { mapped: false, reason: "The FINA user mapping is unreadable." };
|
|
74
|
-
const rows = await client.executeSql(
|
|
75
|
-
`SELECT USERID, USERNAME, USERLEVEL, ${RIGHT_COLUMNS} FROM USERS WHERE USERID = ${finaUserId} AND ${ACTIVE_FINA_USER}`,
|
|
76
|
-
);
|
|
77
|
-
if (!rows?.length) {
|
|
78
|
-
return { mapped: false, reason: `FINA user ${finaUserId} does not exist or is inactive in FINA.` };
|
|
79
|
-
}
|
|
80
|
-
const row = rows[0];
|
|
81
|
-
return {
|
|
82
|
-
mapped: true,
|
|
83
|
-
finaUser: { id: Number(row.userid), name: String(row.username ?? ""), userLevel: Number(row.userlevel) },
|
|
84
|
-
rights: rightsFromUserRow(row),
|
|
85
|
-
};
|
|
86
|
-
},
|
|
87
|
-
});
|
package/engine/fina/live.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 — engine/fina's live dependencies: this hub's PostgreSQL database and the FINA API.
|
|
3
|
-
*
|
|
4
|
-
* Kept apart from the routes so a test hands its own (`context.deps`) and never opens a pool or a socket.
|
|
5
|
-
*/
|
|
6
|
-
import { execPostgres, execPostgresStatements, withPostgresTransaction } from "../domain/dialect.js";
|
|
7
|
-
import { resolveIsAdministrator } from "../orm/userGroups.js";
|
|
8
|
-
import { createFinaClient } from "./client.js";
|
|
9
|
-
|
|
10
|
-
export const createLiveFinaDeps = (config) => {
|
|
11
|
-
const tenant = config.tenantDbIndex;
|
|
12
|
-
return {
|
|
13
|
-
exec: (sql) => execPostgres(sql, tenant),
|
|
14
|
-
execStatements: (statements) => execPostgresStatements(statements, tenant),
|
|
15
|
-
transaction: (fn) => withPostgresTransaction(tenant, fn),
|
|
16
|
-
client: createFinaClient({ url: config.url, dbIndex: config.dbIndex }),
|
|
17
|
-
isAdministrator: (userId) => resolveIsAdministrator({ exec: execPostgres, dbIndex: tenant, userId }),
|
|
18
|
-
};
|
|
19
|
-
};
|
package/engine/fina/lock.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 C-14 / D2a — the Biz-A date lock.
|
|
3
|
-
*
|
|
4
|
-
* From Biz-A, nothing dated ON OR BEFORE "locked up to" may be created, edited, deleted or mass-edited. It is
|
|
5
|
-
* Biz-A's only (FINA desktop is unchanged); FINA's own period lock (C-15) stays in the FINA API.
|
|
6
|
-
*
|
|
7
|
-
* ⚠️ CALENDAR DAYS, compared as `YYYY-MM-DD` strings. A Date is read in the server's local zone and a string by its
|
|
8
|
-
* first ten characters, so no conversion can move a document to the day before.
|
|
9
|
-
*/
|
|
10
|
-
const DAY = /^(\d{4})-(\d{2})-(\d{2})/;
|
|
11
|
-
|
|
12
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
13
|
-
|
|
14
|
-
export const toDateOnly = (value) => {
|
|
15
|
-
if (value instanceof Date) {
|
|
16
|
-
return Number.isNaN(value.getTime()) ? null : `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
|
|
17
|
-
}
|
|
18
|
-
const match = DAY.exec(String(value ?? "").trim());
|
|
19
|
-
if (!match) return null;
|
|
20
|
-
const [, y, m, d] = match.map(Number);
|
|
21
|
-
const check = new Date(y, m - 1, d);
|
|
22
|
-
/* 2026-02-30 rolls over to March: not a day. */
|
|
23
|
-
if (check.getFullYear() !== y || check.getMonth() !== m - 1 || check.getDate() !== d) return null;
|
|
24
|
-
return match[0];
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/* ⚠️ With a lock set, a date that cannot be read is LOCKED: refusing is recoverable, a write that slipped past the
|
|
28
|
-
lock is not. */
|
|
29
|
-
export const isDateLocked = (date, upTo) => {
|
|
30
|
-
const lock = toDateOnly(upTo);
|
|
31
|
-
if (!lock) return false;
|
|
32
|
-
const day = toDateOnly(date);
|
|
33
|
-
if (!day) return true;
|
|
34
|
-
return day <= lock;
|
|
35
|
-
};
|