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.
Files changed (80) hide show
  1. package/bin/app.js +20 -255
  2. package/bin/hub.js +0 -16
  3. package/bin/hubEvent.js +5 -98
  4. package/bin/tunnelTarget.js +0 -4
  5. package/engine/bpm/workflow.js +6 -7
  6. package/engine/cc/configValidate.js +0 -53
  7. package/engine/cc/guides/V2-SCREEN-AUTHORING.md +512 -531
  8. package/engine/cc/layer3Behavior.js +1 -2
  9. package/engine/cc/layer3Schema.js +1 -3
  10. package/engine/cc/livePipelines.js +3 -9
  11. package/engine/cc/publish.js +105 -86
  12. package/engine/domain/appConfig.js +18 -108
  13. package/engine/domain/applicationConfig.js +12 -87
  14. package/engine/domain/dialect.js +0 -138
  15. package/engine/domain/domainNavigation.js +50 -86
  16. package/engine/domain/entityConfigLoader.js +4 -16
  17. package/engine/domain/numericScale.js +0 -2
  18. package/engine/domain/system.js +0 -2
  19. package/engine/ext/publicTheme.js +1 -8
  20. package/engine/orm/apiRoute.js +61 -187
  21. package/engine/orm/entityConfig.js +2 -32
  22. package/engine/orm/finaAuth.js +0 -0
  23. package/engine/orm/finaEndpoint.js +0 -19
  24. package/engine/orm/imgQuery.js +0 -92
  25. package/engine/orm/listQuery.js +1 -6
  26. package/package.json +1 -1
  27. package/worker/cliScriptWorker.js +0 -5
  28. package/engine/attachments/store.js +0 -195
  29. package/engine/domain/domainRegistration.js +0 -65
  30. package/engine/fina/approval.js +0 -141
  31. package/engine/fina/approvalService.js +0 -59
  32. package/engine/fina/attachments.js +0 -165
  33. package/engine/fina/client.js +0 -84
  34. package/engine/fina/config.js +0 -26
  35. package/engine/fina/context.js +0 -58
  36. package/engine/fina/document.js +0 -67
  37. package/engine/fina/documentWrite.js +0 -173
  38. package/engine/fina/documents/index.js +0 -28
  39. package/engine/fina/documents/jv.js +0 -105
  40. package/engine/fina/documents/jvWrite.js +0 -260
  41. package/engine/fina/documents/si.js +0 -40
  42. package/engine/fina/errors.js +0 -17
  43. package/engine/fina/features.js +0 -86
  44. package/engine/fina/guard.js +0 -102
  45. package/engine/fina/identity.js +0 -87
  46. package/engine/fina/live.js +0 -19
  47. package/engine/fina/lock.js +0 -35
  48. package/engine/fina/master.js +0 -331
  49. package/engine/fina/masters/coa.js +0 -104
  50. package/engine/fina/masters/currency.js +0 -39
  51. package/engine/fina/masters/department.js +0 -32
  52. package/engine/fina/masters/index.js +0 -21
  53. package/engine/fina/masters/project.js +0 -53
  54. package/engine/fina/period.js +0 -56
  55. package/engine/fina/print.js +0 -136
  56. package/engine/fina/readPolicy.js +0 -67
  57. package/engine/fina/recurring.js +0 -200
  58. package/engine/fina/route.js +0 -61
  59. package/engine/fina/routes/attachment.js +0 -41
  60. package/engine/fina/routes/document.js +0 -66
  61. package/engine/fina/routes/master.js +0 -26
  62. package/engine/fina/routes/period.js +0 -24
  63. package/engine/fina/routes/print.js +0 -19
  64. package/engine/fina/routes/recurring.js +0 -27
  65. package/engine/fina/routes/session.js +0 -23
  66. package/engine/fina/routes/setup.js +0 -58
  67. package/engine/fina/routes/submission.js +0 -46
  68. package/engine/fina/routes/template.js +0 -31
  69. package/engine/fina/settings.js +0 -159
  70. package/engine/fina/sql.js +0 -34
  71. package/engine/fina/submission.js +0 -290
  72. package/engine/fina/submissionCompare.js +0 -171
  73. package/engine/fina/submissionRepo.js +0 -114
  74. package/engine/fina/template.js +0 -118
  75. package/engine/fina/userMap.js +0 -79
  76. package/engine/orm/companyLocale.js +0 -74
  77. package/engine/orm/userApiDataset.js +0 -153
  78. package/migrations/postgres/1790600000000__fina.sql +0 -33
  79. package/migrations/postgres/1790700000000__attachment.sql +0 -29
  80. package/migrations/postgres/1790800000000__fina_jv_submission.sql +0 -41
@@ -1,290 +0,0 @@
1
- /*
2
- * Doc 8 M7 — a JV submission (pengajuan): with Need Approval on, a NEW JV is not saved to FINA but submitted (A-05). It
3
- * is kept in Biz-A (FINA$JV_SUBMISSION, engine/fina's own; migrations/postgres/1790800000000__fina_jv_submission.sql)
4
- * with a temporary number (A-10, T-32), and goes through the approval workflow (approval.js).
5
- *
6
- * - Submitting checks what a JV save checks (the FINA right, the JV rules, the Biz-A lock, FINA's period), so what
7
- * waits is a JV FINA would take — at that moment.
8
- * - Only a waiting one (MENUNGGU) is edited (the edit right, anyone: A-17) or deleted (M-20). A decided one is read only
9
- * (A-14); resubmitting is Salin (A-19).
10
- * - Setujui posts it to FINA AS THE SUBMITTER'S FINA USER (Agung 2026-10-14), through the JV writer (documentWrite.js,
11
- * `approved`). Success → DISETUJUI with FINA's number (A-07); any refusal → GAGAL_POSTING with the reason, nothing in
12
- * FINA (A-12). The creator and the approver are told (A-13, A-20). Tolak → DITOLAK (A-08).
13
- *
14
- * ⚠️ ONE APPROVAL, ONE JV. Posting runs as the workflow's effect, AFTER the approval commits (BPM agent). It first
15
- * CLAIMS the submission (MENUNGGU → PROSES_POSTING): a second run of the effect finds it claimed and posts nothing. A
16
- * run that dies after FINA saved leaves it PROSES_POSTING, which is honest — nobody knows; Daftar JV does.
17
- * ⚠️ Posting never throws: the BPM agent retries a script that throws, and a retry cannot change a refusal.
18
- */
19
- import { finaError } from "./errors.js";
20
- import { requireRight } from "./identity.js";
21
- import { documentOf, documentId } from "./documents/index.js";
22
- import { listTemplates, assertTemplateOfType } from "./template.js";
23
- import { isDateLocked, toDateOnly } from "./lock.js";
24
- import { checkPeriods } from "./period.js";
25
- import { ownerOf, submissionOwnerOf } from "./attachments.js";
26
- import { APPROVAL } from "./approval.js";
27
-
28
- export const SUBMISSION = Object.freeze({ waiting: "MENUNGGU", posting: "PROSES_POSTING", approved: "DISETUJUI", rejected: "DITOLAK", failed: "GAGAL_POSTING" });
29
-
30
- /* A submission's attachments: the owner `fina.jvsub` (attachments.js serves them by the same rule). */
31
- export { submissionOwnerOf };
32
-
33
- /* FINA said no: the reason is the whole story. Anything else (no answer, a broken one) may have saved it after all. */
34
- const DEFINITE = new Set(["DOCUMENT_RULE", "DATE_LOCKED", "PERIOD_CLOSED", "FORBIDDEN", "UNAUTHENTICATED", "INVALID", "FINA_REFUSED", "DOCUMENT_CHANGED"]);
35
- const UNSURE_NOTE = "FINA may have saved it anyway: look for it in Daftar JV before submitting again (Salin).";
36
-
37
- const REQUEST_TTL_MS = 10 * 60 * 1000;
38
- const requests = new Map();
39
-
40
- /* Tests only: forget the request ids seen. */
41
- export const resetSubmissionRequests = () => requests.clear();
42
-
43
- const round4 = (n) => Math.round(Number(n) * 10000) / 10000;
44
-
45
- /* A kept line is as the form sends it: Debit / Credit, besides FINA's signed amount. */
46
- const keptLine = ({ n, seq, ...line }) => ({
47
- ...line,
48
- debit: line.glamount > 0 ? line.glamount : null,
49
- credit: line.glamount < 0 ? -line.glamount : null,
50
- });
51
-
52
- const messageOf = (error) => String(error?.message ?? error ?? "unknown error");
53
-
54
- export const createSubmissionService = ({ client, identity, settings, features, repo, bpm, attachments, writer, now = () => Date.now() }) => {
55
- const document = documentOf("jv");
56
- const label = (row) => row?.number ?? `JV submission ${row?.id}`;
57
-
58
- const found = async (id) => {
59
- const row = await repo.read(id);
60
- if (!row) throw finaError("DOCUMENT_NOT_FOUND", `JV submission ${id} was not found.`);
61
- return row;
62
- };
63
-
64
- /* Changed only while it waits — in the table AND in its workflow (a decision commits before its effect runs). */
65
- const stillWaiting = async (row) => {
66
- if (row.status !== SUBMISSION.waiting) {
67
- throw finaError("DOCUMENT_RULE", `${label(row)} is ${String(row.status).toLowerCase()}: it is read only. Salin makes a new submission.`);
68
- }
69
- if (row.instanceId === null || row.instanceId === undefined) return;
70
- const instance = await bpm.instance(row.instanceId);
71
- if (instance && instance.current_state_code !== APPROVAL.waiting) {
72
- throw finaError("DOCUMENT_RULE", `${label(row)} is already decided; its outcome follows in a moment.`);
73
- }
74
- };
75
-
76
- const saveRun = async (principal, request) => {
77
- const blank = request?.id === null || request?.id === undefined || request?.id === "";
78
- const id = blank ? null : documentId(request.id);
79
- const isNew = id === null;
80
- const userId = Number(principal?.userId);
81
- requireRight(await identity.resolve(principal), document.rights, isNew ? "create" : "edit");
82
-
83
- const config = await settings.read();
84
- if (!document.write.needsApproval(config)) {
85
- throw finaError("DOCUMENT_RULE", "Need Approval is off in the FINA Cloud console: a new Journal Voucher is saved to FINA directly.");
86
- }
87
- let row = null;
88
- if (isNew) {
89
- if (!(await bpm.published())) {
90
- throw finaError("DOCUMENT_RULE", "The JV approval workflow is not published yet: Publish the FINA Cloud console first.");
91
- }
92
- } else {
93
- row = await found(id);
94
- await stillWaiting(row);
95
- }
96
-
97
- const flags = await features.read();
98
- const normalized = document.write.normalize(request?.master, request?.lines);
99
- const problems = [...normalized.problems];
100
- if (problems.length === 0) problems.push(...(await document.write.validate({ client, lines: normalized.lines, settings: config, features: flags })));
101
- if (problems.length > 0) throw finaError("DOCUMENT_RULE", problems.join("\n"));
102
-
103
- const date = normalized.master.transdate;
104
- const upTo = config?.lock?.upTo ?? null;
105
- if (isDateLocked(date, upTo)) {
106
- throw finaError("DATE_LOCKED", `${date} is on or before the Biz-A lock date ${toDateOnly(upTo)}: it cannot be submitted from Biz-A.`);
107
- }
108
- await checkPeriods(client, [date]);
109
- if (normalized.master.templateid) assertTemplateOfType(await listTemplates(client, document.templateType), normalized.master.templateid);
110
-
111
- const values = {
112
- transdate: date,
113
- transdescription: normalized.master.transdescription,
114
- amount: round4(normalized.lines.reduce((s, l) => s + (l.glamount > 0 ? l.glamount : 0), 0)),
115
- templateid: normalized.master.templateid,
116
- document: { master: normalized.master, lines: normalized.lines.map(keptLine) },
117
- };
118
-
119
- if (!isNew) {
120
- if (!(await repo.update(id, { ...values, updatedBy: userId }))) {
121
- throw finaError("DOCUMENT_RULE", `${label(row)} was decided meanwhile: it is read only now.`);
122
- }
123
- return { id, number: row.number };
124
- }
125
-
126
- const kept = await repo.insert({ ...values, userId });
127
- try {
128
- const { instanceId } = await bpm.initiate({ workflowKey: APPROVAL.key, entityType: APPROVAL.entityType, entityId: kept.id, userId });
129
- await repo.setInstance(kept.id, instanceId);
130
- } catch (error) {
131
- await repo.remove(kept.id).catch(() => false);
132
- throw finaError("DOCUMENT_RULE", `The JV approval workflow did not start, so nothing was submitted: ${messageOf(error)}`);
133
- }
134
- return { id: kept.id, number: kept.number };
135
- };
136
-
137
- const removeRun = async (principal, request) => {
138
- const id = documentId(request?.id);
139
- requireRight(await identity.resolve(principal), document.rights, "delete");
140
- const row = await found(id);
141
- await stillWaiting(row);
142
- if (row.instanceId !== null && row.instanceId !== undefined) await bpm.terminate({ instanceId: row.instanceId, userId: Number(principal?.userId) });
143
- const warnings = [];
144
- try {
145
- await attachments.removeOwner({ owner: submissionOwnerOf(id) });
146
- } catch (error) {
147
- warnings.push(`Its attachments could not be removed: ${messageOf(error)}`);
148
- }
149
- if (!(await repo.remove(id))) throw finaError("DOCUMENT_RULE", `${label(row)} was decided meanwhile: it is read only now.`);
150
- return { id, number: row.number, ...(warnings.length > 0 && { warnings }) };
151
- };
152
-
153
- /* `requestId` (from the browser, one per action) makes a resend answer the first outcome; `dbIndex` scopes it. */
154
- const once = (action, fn) => (principal, request = {}, { dbIndex } = {}) => {
155
- const requestId = String(request?.requestId ?? "").trim();
156
- if (!requestId) return fn(principal, request);
157
- const at = now();
158
- for (const [key, entry] of requests) if (at - entry.at > REQUEST_TTL_MS) requests.delete(key);
159
- const key = `${action}:${dbIndex ?? ""}:${principal?.userId ?? ""}:${requestId}`;
160
- if (!requests.has(key)) requests.set(key, { at, promise: fn(principal, request) });
161
- return requests.get(key).promise;
162
- };
163
-
164
- /* The submission an effect is about: an instance of THIS workflow's entity, standing where the effect expects. */
165
- const decided = async (instanceId, state) => {
166
- const instance = await bpm.instance(instanceId);
167
- if (!instance || instance.entity_type !== APPROVAL.entityType) return { refused: `Instance ${instanceId} is not a JV submission's.` };
168
- if (instance.current_state_code !== state) return { refused: `Instance ${instanceId} stands at ${instance.current_state_code}, not ${state}.` };
169
- const row = await repo.read(instance.entity_id);
170
- if (!row) return { refused: `JV submission ${instance.entity_id} was not found.` };
171
- return { row };
172
- };
173
-
174
- /* What a run answers for a submission already past waiting (a retried effect, a second run). */
175
- const asBefore = (row) => ({
176
- ok: row.status === SUBMISSION.approved || row.status === SUBMISSION.rejected,
177
- status: row.status,
178
- id: row.id,
179
- number: row.number,
180
- jvNumber: row.finaJvNumber ?? null,
181
- ...(row.postError && { error: row.postError }),
182
- notify: null,
183
- });
184
-
185
- const told = (row, userIds, extra) => ({
186
- userIds: [...new Set(userIds.filter((u) => Number.isInteger(u)))],
187
- data: { id: row.id, number: row.number, createdBy: row.createdBy, ...extra },
188
- });
189
-
190
- const postRun = async ({ instanceId } = {}) => {
191
- const { row, refused } = await decided(instanceId, APPROVAL.approved);
192
- if (refused) return { ok: false, error: refused, notify: null };
193
- if (row.status !== SUBMISSION.waiting || !(await repo.claim(row.id))) return asBefore(row.status === SUBMISSION.waiting ? await found(row.id) : row);
194
-
195
- const decidedBy = await bpm.decider(instanceId).catch(() => null);
196
- const createdBy = Number(row.createdBy);
197
- const userIds = [createdBy, decidedBy];
198
- let saved;
199
- try {
200
- saved = await writer.save(
201
- { userId: createdBy },
202
- "jv",
203
- { id: null, master: row.document?.master, lines: row.document?.lines, requestId: `sub-${row.id}` },
204
- { approved: true },
205
- );
206
- } catch (error) {
207
- const postError = DEFINITE.has(error?.code) ? messageOf(error) : `${messageOf(error)} ${UNSURE_NOTE}`;
208
- await repo.decide(row.id, { status: SUBMISSION.failed, postError, decidedBy, finaJvId: null, finaJvNumber: null }, SUBMISSION.posting);
209
- return { ok: false, status: SUBMISSION.failed, id: row.id, number: row.number, error: postError, notify: told(row, userIds, { status: SUBMISSION.failed, error: postError }) };
210
- }
211
-
212
- await repo.decide(row.id, { status: SUBMISSION.approved, postError: null, decidedBy, finaJvId: saved.id, finaJvNumber: saved.number ?? null }, SUBMISSION.posting);
213
- const warnings = [...(saved.warnings ?? [])];
214
- try {
215
- const files = await attachments.list({ owner: submissionOwnerOf(row.id) });
216
- if (files.length > 0) {
217
- const limits = (await settings.read())?.[document.name]?.attachments ?? null;
218
- await attachments.copy({ from: submissionOwnerOf(row.id), to: ownerOf(document.name, saved.id), ids: files.map((f) => f.id), userId: decidedBy }, limits);
219
- }
220
- } catch (error) {
221
- warnings.push(`Its attachments could not be copied to JV ${saved.number ?? saved.id}: ${messageOf(error)}`);
222
- }
223
- const data = { status: SUBMISSION.approved, jvId: saved.id, jvNumber: saved.number ?? null };
224
- return { ok: true, status: SUBMISSION.approved, id: row.id, number: row.number, jvNumber: data.jvNumber, ...(warnings.length > 0 && { warnings }), notify: told(row, userIds, data) };
225
- };
226
-
227
- const rejectRun = async ({ instanceId } = {}) => {
228
- const { row, refused } = await decided(instanceId, APPROVAL.rejected);
229
- if (refused) return { ok: false, error: refused, notify: null };
230
- if (row.status !== SUBMISSION.waiting) return asBefore(row);
231
- const decidedBy = await bpm.decider(instanceId).catch(() => null);
232
- if (!(await repo.decide(row.id, { status: SUBMISSION.rejected, decidedBy }, SUBMISSION.waiting))) return asBefore(await found(row.id));
233
- return { ok: true, status: SUBMISSION.rejected, id: row.id, number: row.number, notify: told(row, [Number(row.createdBy)], { status: SUBMISSION.rejected }) };
234
- };
235
-
236
- const never = (fn) => async (payload) => {
237
- try {
238
- return await fn(payload);
239
- } catch (error) {
240
- return { ok: false, error: messageOf(error), notify: null };
241
- }
242
- };
243
-
244
- return {
245
- save: once("save", saveRun),
246
- remove: once("remove", removeRun),
247
-
248
- /* Any mapped FINA user (D4: view). A waiting one says what would stop its posting now (A-18) — without blocking. */
249
- get: async (principal, rawId) => {
250
- requireRight(await identity.resolve(principal), document.rights, null);
251
- const row = await found(documentId(rawId));
252
- const warnings = [];
253
- if (row.status === SUBMISSION.waiting) {
254
- const config = await settings.read();
255
- const date = toDateOnly(row.transdate ?? row.document?.master?.transdate);
256
- const upTo = config?.lock?.upTo ?? null;
257
- if (isDateLocked(date, upTo)) warnings.push(`${date} is on or before the Biz-A lock date ${toDateOnly(upTo)}: Setujui will fail unless the lock moves.`);
258
- else {
259
- try {
260
- await checkPeriods(client, [date]);
261
- } catch (error) {
262
- warnings.push(messageOf(error));
263
- }
264
- }
265
- }
266
- /* The JV form reads it as it reads a JV (fina/document/get): its template, no version (only a JV in FINA has
267
- one), and the JV number once FINA gave one. */
268
- const master = row.document?.master ?? {};
269
- return {
270
- id: row.id,
271
- number: row.number,
272
- status: row.status,
273
- templateId: master.templateid ?? row.templateid ?? null,
274
- version: null,
275
- master: { ...master, jvnumber: row.finaJvNumber ?? null },
276
- lines: row.document?.lines ?? [],
277
- instanceId: row.instanceId ?? null,
278
- createdBy: row.createdBy ?? null,
279
- decidedBy: row.decidedBy ?? null,
280
- finaJv: row.finaJvId ? { id: row.finaJvId, number: row.finaJvNumber ?? null } : null,
281
- postError: row.postError ?? null,
282
- warnings,
283
- };
284
- },
285
-
286
- /* The workflow's effects (lib.fina.approval), never a route. */
287
- post: never(postRun),
288
- reject: never(rejectRun),
289
- };
290
- };
@@ -1,171 +0,0 @@
1
- /*
2
- * Doc 8 M7b — a posted JV submission next to the JV it became in FINA (L-02, A-16; spec §10.1).
3
- *
4
- * The Biz-A copy is kept as submitted and never synced (A-14, A-15): FINA may have changed the JV since, from the
5
- * desktop or from Biz-A, or deleted it (L-09: "Tidak ditemukan"). The answer carries both sides the same way, with the
6
- * display values FINA has now (account "1101.001 - Cash IDR", department, project, subsidiary, template, extended
7
- * lookups), and the differences — structured, so the app words them.
8
- *
9
- * Any mapped FINA user may look (D4: view). Lines are compared by position.
10
- */
11
- import { finaError } from "./errors.js";
12
- import { requireRight } from "./identity.js";
13
- import { documentOf, documentId } from "./documents/index.js";
14
- import { readDocument } from "./document.js";
15
- import { listTemplates, readMappings } from "./template.js";
16
- import { toDateOnly } from "./lock.js";
17
- import { sqlString } from "./sql.js";
18
-
19
- const round4 = (n) => Math.round(Number(n) * 10000) / 10000;
20
- const amount = (v) => (v === null || v === undefined || v === "" || Number(v) === 0 ? null : round4(v));
21
- const text = (v) => {
22
- const s = v === null || v === undefined ? "" : String(v).trim();
23
- return s === "" ? null : s;
24
- };
25
- const inList = (values) => [...new Set(values)].map((v) => sqlString(v)).join(", ");
26
-
27
- /* "code - name" for each code FINA knows; one IN query per kind. */
28
- const namesOf = async (client, table, codeColumn, nameColumn, codes) => {
29
- const wanted = [...new Set(codes.filter(Boolean))];
30
- if (wanted.length === 0) return new Map();
31
- const rows = (await client.executeSql(`SELECT ${codeColumn}, ${nameColumn} FROM ${table} WHERE ${codeColumn} IN (${inList(wanted)})`)) ?? [];
32
- return new Map(rows.map((r) => [String(r[codeColumn.toLowerCase()]).trim(), text(r[nameColumn.toLowerCase()])]));
33
- };
34
- const named = (names, code) => {
35
- const c = text(code);
36
- if (!c) return null;
37
- const name = names.get(c);
38
- return name ? `${c} - ${name}` : c;
39
- };
40
-
41
- /* A side's master and lines as they compare: the kept submission (request form) or FINA's JV (document.js). */
42
- const plain = (doc) => ({
43
- master: { transdate: toDateOnly(doc.master?.transdate), transdescription: text(doc.master?.transdescription), templateid: doc.master?.templateid ?? null, ext: doc.master ?? {} },
44
- lines: (doc.lines ?? []).map((l) => {
45
- const base = l.glamount !== undefined && l.glamount !== null ? Number(l.glamount) : Number(l.debit ?? 0) - Number(l.credit ?? 0);
46
- return {
47
- glaccount: text(l.glaccount),
48
- debit: amount(base > 0 ? base : null),
49
- credit: amount(base < 0 ? -base : null),
50
- rate: l.rate === null || l.rate === undefined ? 1 : round4(l.rate),
51
- memo: text(l.description),
52
- deptno: text(l.deptno),
53
- projectno: text(l.projectno),
54
- personno: text(l.personno),
55
- ext: l,
56
- };
57
- }),
58
- });
59
-
60
- export const compareSubmission = async ({ client, identity, repo }, principal, rawId) => {
61
- requireRight(await identity.resolve(principal), "jv", null);
62
- const id = documentId(rawId);
63
- const row = await repo.read(id);
64
- if (!row) throw finaError("DOCUMENT_NOT_FOUND", `JV submission ${id} was not found.`);
65
- const jvDocument = documentOf("jv");
66
-
67
- let jvState = "NOT_POSTED";
68
- let jvDoc = null;
69
- if (row.finaJvId) {
70
- try {
71
- jvDoc = await readDocument(client, jvDocument, row.finaJvId);
72
- jvState = "POSTED";
73
- } catch (error) {
74
- if (error?.code !== "DOCUMENT_NOT_FOUND") throw error;
75
- jvState = "NOT_FOUND";
76
- }
77
- }
78
-
79
- const left = plain(row.document ?? {});
80
- const right = jvDoc ? plain(jvDoc) : null;
81
- const both = [left, ...(right ? [right] : [])];
82
- const all = (key) => both.flatMap((s) => s.lines.map((l) => l[key]));
83
-
84
- const [accounts, depts, projects, persons, templates, mappings] = await Promise.all([
85
- namesOf(client, "GLACCNT", "GLACCOUNT", "ACCOUNTNAME", all("glaccount")),
86
- namesOf(client, "DEPARTMENT", "DEPTNO", "DEPTNAME", all("deptno")),
87
- namesOf(client, "PROJECT", "PROJECTNO", "PROJECTNAME", all("projectno")),
88
- namesOf(client, "PERSONDATA", "PERSONNO", "NAME", all("personno")),
89
- listTemplates(client, jvDocument.templateType),
90
- readMappings(client, jvDocument.extended),
91
- ]);
92
- const templateName = (templateId) => templates.find((t) => t.id === Number(templateId))?.name ?? (templateId ? String(templateId) : null);
93
-
94
- /* Extended values as a person reads them: a lookup by its EXTENDEDDET INFO1, a custom field as stored. */
95
- const masterMappings = mappings.filter((m) => m.part === "master");
96
- const lineMappings = mappings.filter((m) => m.part === "detail");
97
- const lookupIds = [];
98
- for (const s of both) {
99
- for (const m of masterMappings) if (m.kind === "lookup") lookupIds.push(s.master.ext[m.key]);
100
- for (const l of s.lines) for (const m of lineMappings) if (m.kind === "lookup") lookupIds.push(l.ext[m.key]);
101
- }
102
- const wantedIds = [...new Set(lookupIds.map(Number).filter((n) => Number.isInteger(n) && n > 0))];
103
- const infos =
104
- wantedIds.length === 0
105
- ? new Map()
106
- : new Map(
107
- ((await client.executeSql(`SELECT EXTENDEDDETID, INFO1 FROM EXTENDEDDET WHERE EXTENDEDDETID IN (${wantedIds.join(", ")})`)) ?? []).map((r) => [
108
- Number(r.extendeddetid),
109
- text(r.info1),
110
- ]),
111
- );
112
- const extValue = (m, raw) => {
113
- if (raw === null || raw === undefined || raw === "") return null;
114
- if (m.kind === "lookup") return infos.get(Number(raw)) ?? String(raw);
115
- return m.customType === 4 ? toDateOnly(raw) : raw;
116
- };
117
- const extendedOf = (source, list) => list.map((m) => ({ key: m.key, caption: m.caption, value: extValue(m, source[m.key]) }));
118
-
119
- const sideOf = (s, head) => {
120
- const lines = s.lines.map((l, i) => ({
121
- n: i + 1,
122
- account: named(accounts, l.glaccount),
123
- debit: l.debit,
124
- credit: l.credit,
125
- rate: l.rate,
126
- memo: l.memo,
127
- dept: named(depts, l.deptno),
128
- project: named(projects, l.projectno),
129
- subsidiary: named(persons, l.personno),
130
- extended: extendedOf(l.ext, lineMappings),
131
- }));
132
- return {
133
- ...head,
134
- master: { transdate: s.master.transdate, transdescription: s.master.transdescription, templateName: templateName(s.master.templateid), extended: extendedOf(s.master.ext, masterMappings) },
135
- lines,
136
- totals: { debit: round4(lines.reduce((t, l) => t + (l.debit ?? 0), 0)), credit: round4(lines.reduce((t, l) => t + (l.credit ?? 0), 0)) },
137
- };
138
- };
139
-
140
- const submission = sideOf(left, { id: row.id, number: row.number, status: row.status });
141
- const jv = right ? sideOf(right, { id: Number(row.finaJvId), number: jvDoc.master?.jvnumber ?? row.finaJvNumber ?? null }) : null;
142
-
143
- const differences = [];
144
- if (jv) {
145
- const differ = (field, a, b, extra = {}) => {
146
- if ((a ?? null) !== (b ?? null)) differences.push({ field, ...extra, submission: a ?? null, jv: b ?? null });
147
- };
148
- differ("transdate", submission.master.transdate, jv.master.transdate);
149
- differ("transdescription", submission.master.transdescription, jv.master.transdescription);
150
- differ("template", submission.master.templateName, jv.master.templateName);
151
- submission.master.extended.forEach((e, i) => differ(e.key, e.value, jv.master.extended[i]?.value, { caption: e.caption }));
152
- const count = Math.max(submission.lines.length, jv.lines.length);
153
- for (let i = 0; i < count; i++) {
154
- const a = submission.lines[i];
155
- const b = jv.lines[i];
156
- const line = i + 1;
157
- if (!b) {
158
- differences.push({ line, only: "submission" });
159
- continue;
160
- }
161
- if (!a) {
162
- differences.push({ line, only: "jv" });
163
- continue;
164
- }
165
- for (const field of ["account", "debit", "credit", "rate", "memo", "dept", "project", "subsidiary"]) differ(field, a[field], b[field], { line });
166
- a.extended.forEach((e, k) => differ(e.key, e.value, b.extended[k]?.value, { line, caption: e.caption }));
167
- }
168
- }
169
-
170
- return { submission, jv, jvState, differences };
171
- };
@@ -1,114 +0,0 @@
1
- /*
2
- * Doc 8 M7 — FINA$JV_SUBMISSION (migrations/postgres/1790800000000__fina_jv_submission.sql), engine/fina's only reader
3
- * and writer of its contents. The browser role reads the list's columns only; the JV itself (DOCUMENT) goes out through
4
- * fina/submission/get, which checks the FINA right.
5
- *
6
- * - The temporary number PJV/{YYYY}/{MM}/{seq:4} (reset yearly, A-10) is taken by biza_next_number IN the INSERT, so a
7
- * failed insert gives its number back and two at once get two numbers.
8
- * - Every change names the status it expects (`WHERE STATUS = …`), so an edit and a decision cannot both win, and a
9
- * second posting run cannot claim what the first one holds.
10
- */
11
- import { finaError } from "./errors.js";
12
- import { sqlString } from "./sql.js";
13
-
14
- const TABLE = "FINA$JV_SUBMISSION";
15
- const STATUSES = new Set(["MENUNGGU", "PROSES_POSTING", "DISETUJUI", "DITOLAK", "GAGAL_POSTING"]);
16
-
17
- const idOf = (value) => {
18
- const id = Number(value);
19
- if (value === null || value === undefined || !Number.isInteger(id) || id <= 0 || !/^\d+$/.test(String(value).trim())) {
20
- throw finaError("INVALID", `A JV submission id is a positive whole number, not ${JSON.stringify(value)}.`);
21
- }
22
- return id;
23
- };
24
- const statusOf = (value) => {
25
- if (!STATUSES.has(value)) throw new Error(`Not a JV submission status: ${JSON.stringify(value)}.`);
26
- return sqlString(value);
27
- };
28
- const text = (v) => (v === null || v === undefined || v === "" ? "NULL" : sqlString(v));
29
- const int = (v) => (v === null || v === undefined || v === "" || !Number.isInteger(Number(v)) ? "NULL" : String(Number(v)));
30
- const num = (v) => (v === null || v === undefined || v === "" || !Number.isFinite(Number(v)) ? "NULL" : String(Number(v)));
31
- const asNumber = (v) => (v === null || v === undefined ? null : Number(v));
32
-
33
- const PERIOD = "to_char(now(), 'YYYY')";
34
- const NUMBER = "'PJV/' || to_char(now(), 'YYYY') || '/' || to_char(now(), 'MM') || '/' || CASE WHEN N.V < 10000 THEN lpad(N.V::text, 4, '0') ELSE N.V::text END";
35
-
36
- const COLUMNS =
37
- "ID, SUB_NUMBER, STATUS, to_char(TRANSDATE, 'YYYY-MM-DD') AS TRANSDATE, TRANSDESCRIPTION, AMOUNT, TEMPLATEID, DOCUMENT, " +
38
- "WF_INSTANCE_ID, FINA_JVID, FINA_JVNUMBER, POST_ERROR, CREATED_BY, UPDATED_BY, DECIDED_BY";
39
-
40
- const parse = (raw) => {
41
- try {
42
- return typeof raw === "string" ? JSON.parse(raw) : (raw ?? null);
43
- } catch {
44
- return null;
45
- }
46
- };
47
-
48
- const rowOf = (r) => ({
49
- id: Number(r.id),
50
- number: r.sub_number,
51
- status: r.status,
52
- transdate: r.transdate ?? null,
53
- transdescription: r.transdescription ?? null,
54
- amount: asNumber(r.amount),
55
- templateid: asNumber(r.templateid),
56
- document: parse(r.document),
57
- instanceId: asNumber(r.wf_instance_id),
58
- finaJvId: asNumber(r.fina_jvid),
59
- finaJvNumber: r.fina_jvnumber ?? null,
60
- postError: r.post_error ?? null,
61
- createdBy: asNumber(r.created_by),
62
- updatedBy: asNumber(r.updated_by),
63
- decidedBy: asNumber(r.decided_by),
64
- });
65
-
66
- export const createSubmissionRepo = ({ exec }) => {
67
- const changed = async (sql) => ((await exec(sql)) ?? []).length > 0;
68
- const contents = (v) =>
69
- `TRANSDATE = ${text(v.transdate)}, TRANSDESCRIPTION = ${text(v.transdescription)}, AMOUNT = ${num(v.amount)}, TEMPLATEID = ${int(v.templateid)}, DOCUMENT = ${sqlString(JSON.stringify(v.document ?? {}))}`;
70
-
71
- return {
72
- insert: async ({ transdate, transdescription, amount, templateid, document, userId }) => {
73
- const rows = await exec(
74
- `WITH N AS (SELECT biza_next_number(${sqlString(TABLE)}, 'SUB_NUMBER', ${PERIOD}) AS V) ` +
75
- `INSERT INTO ${TABLE} (SUB_NUMBER, STATUS, TRANSDATE, TRANSDESCRIPTION, AMOUNT, TEMPLATEID, DOCUMENT, CREATED_BY) ` +
76
- `SELECT ${NUMBER}, 'MENUNGGU', ${text(transdate)}, ${text(transdescription)}, ${num(amount)}, ${int(templateid)}, ` +
77
- `${sqlString(JSON.stringify(document ?? {}))}, ${int(userId)} FROM N RETURNING ID, SUB_NUMBER`,
78
- );
79
- const r = rows?.[0];
80
- if (!r) throw new Error("The JV submission was not kept: the database answered no row.");
81
- return { id: Number(r.id), number: r.sub_number };
82
- },
83
-
84
- read: async (id) => {
85
- const rows = (await exec(`SELECT ${COLUMNS} FROM ${TABLE} WHERE ID = ${idOf(id)}`)) ?? [];
86
- return rows.length > 0 ? rowOf(rows[0]) : null;
87
- },
88
-
89
- update: async (id, values) =>
90
- changed(`UPDATE ${TABLE} SET ${contents(values)}, UPDATED_BY = ${int(values.updatedBy)}, UPDATED_AT = now() WHERE ID = ${idOf(id)} AND STATUS = 'MENUNGGU' RETURNING ID`),
91
-
92
- setInstance: async (id, instanceId) => {
93
- await exec(`UPDATE ${TABLE} SET WF_INSTANCE_ID = ${int(instanceId)} WHERE ID = ${idOf(id)}`);
94
- },
95
-
96
- claim: async (id) => changed(`UPDATE ${TABLE} SET STATUS = 'PROSES_POSTING', UPDATED_AT = now() WHERE ID = ${idOf(id)} AND STATUS = 'MENUNGGU' RETURNING ID`),
97
-
98
- decide: async (id, values, from) => {
99
- const set = [`STATUS = ${statusOf(values.status)}`, `DECIDED_BY = ${int(values.decidedBy)}`, "DECIDED_AT = now()", "UPDATED_AT = now()"];
100
- if ("finaJvId" in values) set.push(`FINA_JVID = ${int(values.finaJvId)}`);
101
- if ("finaJvNumber" in values) set.push(`FINA_JVNUMBER = ${text(values.finaJvNumber)}`);
102
- if ("postError" in values) set.push(`POST_ERROR = ${text(values.postError)}`);
103
- return changed(`UPDATE ${TABLE} SET ${set.join(", ")} WHERE ID = ${idOf(id)} AND STATUS = ${statusOf(from)} RETURNING ID`);
104
- },
105
-
106
- remove: async (id) => changed(`DELETE FROM ${TABLE} WHERE ID = ${idOf(id)} AND STATUS = 'MENUNGGU' RETURNING ID`),
107
-
108
- /* Doc 8 M7b (L-02): the submission a FINA JV came from — the newest, should one ever have been posted twice. */
109
- findByJv: async (jvId) => {
110
- const rows = (await exec(`SELECT ID, SUB_NUMBER FROM ${TABLE} WHERE FINA_JVID = ${idOf(jvId)} ORDER BY ID DESC LIMIT 1`)) ?? [];
111
- return rows.length > 0 ? { id: Number(rows[0].id), number: rows[0].sub_number } : null;
112
- },
113
- };
114
- };