e-arveldaja-mcp 0.19.0 → 0.20.0
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/CHANGELOG.md +21 -0
- package/dist/api/journals.api.d.ts +11 -0
- package/dist/api/journals.api.d.ts.map +1 -1
- package/dist/api/journals.api.js +13 -0
- package/dist/api/journals.api.js.map +1 -1
- package/dist/api/transactions.api.d.ts.map +1 -1
- package/dist/api/transactions.api.js +25 -0
- package/dist/api/transactions.api.js.map +1 -1
- package/dist/booking-guard.d.ts +148 -0
- package/dist/booking-guard.d.ts.map +1 -0
- package/dist/booking-guard.js +279 -0
- package/dist/booking-guard.js.map +1 -0
- package/dist/http-client.d.ts +9 -0
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +14 -1
- package/dist/http-client.js.map +1 -1
- package/dist/tools/bank-reconciliation.d.ts.map +1 -1
- package/dist/tools/bank-reconciliation.js +37 -32
- package/dist/tools/bank-reconciliation.js.map +1 -1
- package/dist/tools/crud/purchase-invoices.d.ts.map +1 -1
- package/dist/tools/crud/purchase-invoices.js +9 -0
- package/dist/tools/crud/purchase-invoices.js.map +1 -1
- package/dist/tools/currency-rounding.d.ts.map +1 -1
- package/dist/tools/currency-rounding.js +42 -12
- package/dist/tools/currency-rounding.js.map +1 -1
- package/dist/tools/inter-account-utils.d.ts +8 -0
- package/dist/tools/inter-account-utils.d.ts.map +1 -1
- package/dist/tools/inter-account-utils.js +8 -0
- package/dist/tools/inter-account-utils.js.map +1 -1
- package/dist/tools/lightyear-investments.d.ts.map +1 -1
- package/dist/tools/lightyear-investments.js +34 -46
- package/dist/tools/lightyear-investments.js.map +1 -1
- package/dist/tools/own-company-identity.d.ts +26 -0
- package/dist/tools/own-company-identity.d.ts.map +1 -0
- package/dist/tools/own-company-identity.js +37 -0
- package/dist/tools/own-company-identity.js.map +1 -0
- package/dist/tools/pdf-workflow.d.ts.map +1 -1
- package/dist/tools/pdf-workflow.js +54 -2
- package/dist/tools/pdf-workflow.js.map +1 -1
- package/dist/tools/receipt-inbox-booking.d.ts.map +1 -1
- package/dist/tools/receipt-inbox-booking.js +16 -2
- package/dist/tools/receipt-inbox-booking.js.map +1 -1
- package/dist/tools/supplier-resolution.d.ts.map +1 -1
- package/dist/tools/supplier-resolution.js +14 -0
- package/dist/tools/supplier-resolution.js.map +1 -1
- package/dist/tools/wise-import.d.ts.map +1 -1
- package/dist/tools/wise-import.js +27 -13
- package/dist/tools/wise-import.js.map +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.20.0] - 2026-07-10
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`TransactionsApi.confirm` now recovers from an ambiguous network failure on the register call.** Confirming a transaction issues `PATCH /transactions/{id}/register`, which creates a journal server-side, so a dropped connection or timeout is ambiguous — the registration may have committed. Previously such a failure ran the `clients_id` rollback (which would corrupt a *committed* journal's buyer/supplier) and threw, signalling a false failure that could prompt a duplicating retry. On a network error the method now re-reads the transaction (busting the stale cache) and checks its status: if `CONFIRMED` the registration landed, so it keeps `clients_id`, flushes the journal cache, and reports success (with no `created_object_id` — callers already record the sentinel journal id); if still `PROJECT` it rolls back and rethrows as before. A non-network HTTP status is propagated unchanged.
|
|
9
|
+
- **`BookingGuard.createJournalOnce` now recovers from ambiguous network failures (verify-then-retry).** A journal-create POST that fails with a *network* error (timeout / dropped connection, `HttpError.status === "network"`) is ambiguous — the RIK API signs only the request path, not the body, so it has no server-side idempotency and the write may or may not have committed. Rather than blindly retrying (risking a duplicate) or blindly failing (risking an orphan plus a user retry that duplicates), the guard busts the stale journals cache and re-scans the ledger by the create's `document_number` key: if the journal is found the ambiguous write committed, so it is recovered (`recovered: true`) and confirmed if it landed in `PROJECT`, with no retry; if it is not found the create is retried exactly once. A non-network HTTP status (4xx/5xx) is propagated unchanged, since the server saw and rejected the request. The generic http-client keeps its no-retry gate for non-idempotent methods — recovery lives at the guard level, the only place with a checkable key. `reconcile_currency_rounding` and the Lightyear booking sites inherit this automatically.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **Journal idempotency is now centralized in a single `BookingGuard` layer (`src/booking-guard.ts`).** The RIK API signs only the request path, not the body, so it offers no server-side idempotency — every booking tool had to hand-roll its own "did I already book this?" scan of the ledger, and subtle divergences between those copies (deleted-journal handling, in-run vs cross-run dedup, sentinel journal ids) were the dominant duplicate-booking bug class. `BookingGuard` loads one journal snapshot per run and exposes two lanes: Lane A for namespaced `document_number` keys (`FX:{id}`, `LY:{ref}`) with `find`/`record`/`createJournalOnce`, and Lane B for structural inter-account transfers (`sourceDim|targetDim|amount|date` with reference disambiguation and an optional nearest-first date window). Migrated `reconcile_currency_rounding` (Lane A), the Lightyear buy/sell/cash-equivalent/distribution booking sites (Lane A, preserving the legacy bare-reference date-cross-check), and the Wise and bank-reconciliation inter-account paths (Lane B). Behavior is preserved, with two incidental hardening improvements: `reconcile_currency_rounding` now treats a concurrently-created `FX:` journal as already-reconciled instead of double-posting, and Lightyear booking now dedups repeated references within a single CSV. Lane-A-only callers use the cheaper `listAll` (no per-journal posting fetches); Lane B loads postings only when bank dimensions are supplied.
|
|
13
|
+
|
|
14
|
+
## [0.19.1] - 2026-07-10
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Duplicate-booking and balance-drift hardening (whole-codebase review).** Seven high-severity correctness fixes, each with regression tests:
|
|
18
|
+
- **Non-idempotent HTTP requests are no longer retried after a network error or timeout.** A `POST`/`PATCH`/`DELETE` that times out or drops its connection is ambiguous — the server may already have committed the mutation — so retrying risked a duplicate invoice, journal, or transaction. Only `GET` and `PUT` (full replace) retry now; the 5xx retry path was already `GET`-only.
|
|
19
|
+
- **`update_purchase_invoice` re-sends the existing line items when the caller updates header fields only.** The API rejects an item-less PATCH with "Products/services are missing", so every metadata-only update (notes, dates, bank refs) previously failed; the handler now backfills `items` from the current invoice, while a caller that supplies items to change the lines keeps theirs.
|
|
20
|
+
- **The single-PDF flow now applies the same self-supplier defenses as the receipt batch.** `extract_pdf_invoice` resolves the active company's own VAT number and registry code and excludes them from the extracted supplier fields, and `resolve_supplier` threads them into resolution so the previously-dormant self-match guards fire — refusing to resolve or auto-create the buyer's own company as a supplier, which would otherwise book a purchase against self.
|
|
21
|
+
- **`reconcile_currency_rounding` FX-difference journals are now idempotent.** The paid-vs-booked residual does not clear when the `FX:{invoice_id}` journal is posted, so a second `execute` run re-detected the same difference and double-booked it; the tool now skips any invoice that already carries an `FX:` journal.
|
|
22
|
+
- **Inter-account reconciliation no longer confirms both legs of one transfer in a single run.** The in-run journal index was built once at the start and never refreshed, so confirming one leg left the opposite leg looking un-journalized and it was confirmed into a duplicate; each newly-created journal is now recorded into the index immediately.
|
|
23
|
+
- **Receipt auto-link skips cross-currency (base-amount-only) transaction matches.** When a match survived only on base-currency evidence, the transaction amount is in a different currency than the invoice gross, so auto-confirming posted the wrong distribution amount; the guard the sibling bank-reconciliation path already applied now covers the receipt path too, routing such matches to manual review.
|
|
24
|
+
- **Lightyear buy trades capitalize the trade platform fee into investment cost.** The fee was expensed instead of added to cost basis, contradicting the FIFO capital-gains report (which bakes the trade fee into cost basis) and stranding a residual on the investment account after every full sell; only the FX conversion fee — which the report excludes — is expensed.
|
|
25
|
+
|
|
5
26
|
## [0.19.0] - 2026-07-10
|
|
6
27
|
|
|
7
28
|
### Added
|
|
@@ -11,5 +11,16 @@ export declare class JournalsApi extends BaseResource<Journal> {
|
|
|
11
11
|
listAllWithPostings(): Promise<Journal[]>;
|
|
12
12
|
confirm(id: number): Promise<ApiResponse>;
|
|
13
13
|
invalidate(id: number): Promise<ApiResponse>;
|
|
14
|
+
/**
|
|
15
|
+
* Force-drop the journals list cache so the next `listAll()` /
|
|
16
|
+
* `listAllWithPostings()` re-reads from the server.
|
|
17
|
+
*
|
|
18
|
+
* `create()` only invalidates the cache *after* a successful POST, so a
|
|
19
|
+
* create that fails with a network error never clears it and the cached
|
|
20
|
+
* snapshot can still predate the ambiguous write. BookingGuard's
|
|
21
|
+
* verify-then-retry calls this before re-scanning to check whether the
|
|
22
|
+
* ambiguous journal actually committed.
|
|
23
|
+
*/
|
|
24
|
+
invalidateListCache(): void;
|
|
14
25
|
}
|
|
15
26
|
//# sourceMappingURL=journals.api.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journals.api.d.ts","sourceRoot":"","sources":["../../src/api/journals.api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAS,MAAM,oBAAoB,CAAC;AAEzD,qBAAa,WAAY,SAAQ,YAAY,CAAC,OAAO,CAAC;IACpD,YAAY,MAAM,EAAE,UAAU,EAE7B;IAED;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CA6B9C;IAEK,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAQ9C;IAEK,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAKjD;CAEF"}
|
|
1
|
+
{"version":3,"file":"journals.api.d.ts","sourceRoot":"","sources":["../../src/api/journals.api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAS,MAAM,oBAAoB,CAAC;AAEzD,qBAAa,WAAY,SAAQ,YAAY,CAAC,OAAO,CAAC;IACpD,YAAY,MAAM,EAAE,UAAU,EAE7B;IAED;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CA6B9C;IAEK,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAQ9C;IAEK,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAKjD;IAED;;;;;;;;;OASG;IACH,mBAAmB,IAAI,IAAI,CAE1B;CAEF"}
|
package/dist/api/journals.api.js
CHANGED
|
@@ -51,5 +51,18 @@ export class JournalsApi extends BaseResource {
|
|
|
51
51
|
this.invalidateCache("/transactions");
|
|
52
52
|
return result;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Force-drop the journals list cache so the next `listAll()` /
|
|
56
|
+
* `listAllWithPostings()` re-reads from the server.
|
|
57
|
+
*
|
|
58
|
+
* `create()` only invalidates the cache *after* a successful POST, so a
|
|
59
|
+
* create that fails with a network error never clears it and the cached
|
|
60
|
+
* snapshot can still predate the ambiguous write. BookingGuard's
|
|
61
|
+
* verify-then-retry calls this before re-scanning to check whether the
|
|
62
|
+
* ambiguous journal actually committed.
|
|
63
|
+
*/
|
|
64
|
+
invalidateListCache() {
|
|
65
|
+
this.invalidateCache();
|
|
66
|
+
}
|
|
54
67
|
}
|
|
55
68
|
//# sourceMappingURL=journals.api.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journals.api.js","sourceRoot":"","sources":["../../src/api/journals.api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAEzD,MAAM,OAAO,WAAY,SAAQ,YAAqB;IACpD,YAAY,MAAkB;QAC5B,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC7B,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,mBAAmB;QACvB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,QAAQ,kBAAkB,CAAC,CAAC;QACnE,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAY,QAAQ,CAAC,CAAC;QAC9C,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAE1B,MAAM,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC;QAC7B,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QAEjC,mEAAmE;QACnE,MAAM,YAAY,GAAa,EAAE,CAAC;QAClC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACpC,MAAM,OAAO,GAAG,GAAG,CAAC,CAAC,CAAE,CAAC;YACxB,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACvD,IAAI,OAAO,CAAC,EAAE,IAAI,IAAI;oBAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC/C,CAAC;QACH,CAAC;QAED,+DAA+D;QAC/D,MAAM,SAAS,GAAG,CAAC,CAAC;QACpB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC,IAAI,SAAS,EAAE,CAAC;YACxD,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,CAAC;YACnD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC,EAAG,CAAC,CAAC,CAAC,CAAC;YAC7E,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBACtC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAE,CAAC;YAC/B,CAAC;QACH,CAAC;QAED,KAAK,CAAC,mBAAmB,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;QACnD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,EAAU;QACtB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAc,aAAa,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;QACpF,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,2EAA2E;QAC3E,wEAAwE;QACxE,yDAAyD;QACzD,IAAI,CAAC,eAAe,CAAC,eAAe,CAAC,CAAC;QACtC,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,EAAU;QACzB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAc,aAAa,EAAE,aAAa,EAAE,EAAE,CAAC,CAAC;QACtF,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,eAAe,CAAC,eAAe,CAAC,CAAC;QACtC,OAAO,MAAM,CAAC;IAChB,CAAC;CAEF"}
|
|
1
|
+
{"version":3,"file":"journals.api.js","sourceRoot":"","sources":["../../src/api/journals.api.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transactions.api.d.ts","sourceRoot":"","sources":["../../src/api/transactions.api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"transactions.api.d.ts","sourceRoot":"","sources":["../../src/api/transactions.api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,KAAK,EAAE,WAAW,EAAE,uBAAuB,EAAgC,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACvH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,qBAAa,eAAgB,SAAQ,YAAY,CAAC,WAAW,CAAC;IAC5D,YAAY,MAAM,EAAE,UAAU,EAE7B;IAED;;;;;;OAMG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,uBAAuB,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CA6EzF;IAEK,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAOjD;CAEF"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HttpError } from "../http-client.js";
|
|
1
2
|
import { BaseResource } from "./base-resource.js";
|
|
2
3
|
export class TransactionsApi extends BaseResource {
|
|
3
4
|
constructor(client) {
|
|
@@ -46,6 +47,30 @@ export class TransactionsApi extends BaseResource {
|
|
|
46
47
|
return result;
|
|
47
48
|
}
|
|
48
49
|
catch (error) {
|
|
50
|
+
// A network error on the register PATCH is ambiguous: the registration
|
|
51
|
+
// creates a journal server-side, so a lost response may mean it actually
|
|
52
|
+
// committed. Re-read the transaction (busting the stale cache — the
|
|
53
|
+
// failed PATCH never reached its own invalidateCache) and check its
|
|
54
|
+
// status before deciding. If it is CONFIRMED the registration landed, so
|
|
55
|
+
// we must NOT roll back clients_id (that would corrupt the committed
|
|
56
|
+
// journal's buyer/supplier) and must report success instead of a false
|
|
57
|
+
// failure that invites a duplicating retry.
|
|
58
|
+
if (error instanceof HttpError && error.status === "network") {
|
|
59
|
+
this.invalidateCache();
|
|
60
|
+
let confirmed = false;
|
|
61
|
+
try {
|
|
62
|
+
confirmed = (await this.get(id)).status === "CONFIRMED";
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Re-read failed — fall through to the rollback + rethrow path.
|
|
66
|
+
}
|
|
67
|
+
if (confirmed) {
|
|
68
|
+
this.invalidateCache("/journals");
|
|
69
|
+
// The tx re-read does not carry the new journal id; callers already
|
|
70
|
+
// tolerate an absent created_object_id (recording the sentinel id).
|
|
71
|
+
return { code: 200, messages: ["Registration recovered after network error"] };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
49
74
|
if (clientsIdWasSet) {
|
|
50
75
|
try {
|
|
51
76
|
await this.update(id, { clients_id: null });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transactions.api.js","sourceRoot":"","sources":["../../src/api/transactions.api.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"transactions.api.js","sourceRoot":"","sources":["../../src/api/transactions.api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ApiContext } from "./tools/crud-tools.js";
|
|
2
|
+
import type { Journal } from "./types/api.js";
|
|
3
|
+
/**
|
|
4
|
+
* BookingGuard — a single-snapshot idempotency layer for journal creation.
|
|
5
|
+
*
|
|
6
|
+
* The RIK e-Financials API signs only the request path (not the body), so it
|
|
7
|
+
* offers no server-side idempotency: a retried or re-run POST creates a second
|
|
8
|
+
* journal. Every booking tool therefore has to detect "did I already book
|
|
9
|
+
* this?" against the existing ledger before writing. Historically each tool
|
|
10
|
+
* grew its own scan of `journals.listAll()`, its own `document_number`
|
|
11
|
+
* convention, and its own create/confirm/record dance — with subtle
|
|
12
|
+
* divergences (deleted-journal handling, in-run vs cross-run dedup, sentinel
|
|
13
|
+
* journal ids) that were the dominant source of duplicate-booking bugs.
|
|
14
|
+
*
|
|
15
|
+
* BookingGuard centralises that into one snapshot loaded per run, with two
|
|
16
|
+
* lanes:
|
|
17
|
+
*
|
|
18
|
+
* Lane A — namespaced `document_number` keys (`FX:{id}`, `LY:{ref}`, …).
|
|
19
|
+
* Exact-key dedup: one document_number ⇒ at most one live journal.
|
|
20
|
+
*
|
|
21
|
+
* Lane B — structural inter-account transfers keyed
|
|
22
|
+
* `sourceDim|targetDim|amount|date` with reference disambiguation.
|
|
23
|
+
* Delegates to the existing inter-account-utils index/matcher.
|
|
24
|
+
*
|
|
25
|
+
* `createJournalOnce` is the guarded write: find first, create with the
|
|
26
|
+
* document_number stamped, best-effort confirm, then record into the in-run
|
|
27
|
+
* index so a second call in the same run is also deduped.
|
|
28
|
+
*
|
|
29
|
+
* NOTE: the network-level verify-then-retry (re-scan after a network error to
|
|
30
|
+
* recover a journal that may have been created despite a failed response) is
|
|
31
|
+
* intentionally deferred to the http-client migration step. Today the
|
|
32
|
+
* http-client does not retry non-idempotent POSTs at all, so a POST either
|
|
33
|
+
* succeeds (we see the id) or throws (nothing created). The guard's find pass
|
|
34
|
+
* still protects against cross-run and in-run duplicates.
|
|
35
|
+
*/
|
|
36
|
+
export type DocNamespace = "FX" | "LY";
|
|
37
|
+
export interface DocKey {
|
|
38
|
+
ns: DocNamespace;
|
|
39
|
+
/** The identifier within the namespace — invoice id, Lightyear reference, … */
|
|
40
|
+
id: string;
|
|
41
|
+
}
|
|
42
|
+
/** "FX:123" / "LY:OR-EVN9C76R7A" */
|
|
43
|
+
export declare function formatDocNumber(key: DocKey): string;
|
|
44
|
+
/**
|
|
45
|
+
* Parse a raw `document_number` into a DocKey, or `undefined` when it does not
|
|
46
|
+
* carry a known namespace prefix (manual journals, legacy entries, …).
|
|
47
|
+
*
|
|
48
|
+
* Only the first ":" is treated as the separator, so a reference that itself
|
|
49
|
+
* contains a colon (unusual, but possible for external ids) round-trips.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseDocNumber(raw: string | null | undefined): DocKey | undefined;
|
|
52
|
+
export interface ExistingArtifact {
|
|
53
|
+
journal_id: number;
|
|
54
|
+
document_number: string;
|
|
55
|
+
registered: boolean;
|
|
56
|
+
is_deleted: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Liveness filter applied at `find` time.
|
|
60
|
+
* - `any` — every artifact, including deleted/invalidated ones.
|
|
61
|
+
* - `not_deleted` — exclude deleted/invalidated journals (default). A
|
|
62
|
+
* deleted FX/LY journal means the operator invalidated it
|
|
63
|
+
* to re-post, so it must NOT block re-booking.
|
|
64
|
+
* - `registered_only` — only confirmed, non-deleted journals.
|
|
65
|
+
*/
|
|
66
|
+
export type Liveness = "any" | "not_deleted" | "registered_only";
|
|
67
|
+
export interface InterAccountQuery {
|
|
68
|
+
sourceDim: number;
|
|
69
|
+
targetDim: number;
|
|
70
|
+
amount: number;
|
|
71
|
+
date: string;
|
|
72
|
+
/** Optional +/- day window around `date` (default 0 — exact date only). */
|
|
73
|
+
maxGapDays?: number;
|
|
74
|
+
/** Reference number for disambiguating same-amount/date/dims transfers. */
|
|
75
|
+
reference?: string | null;
|
|
76
|
+
}
|
|
77
|
+
export interface CreateOnceOptions {
|
|
78
|
+
/** Liveness used for the pre-create dedup check (default `not_deleted`). */
|
|
79
|
+
liveness?: Liveness;
|
|
80
|
+
/** Attempt to confirm/register the created journal (default true). */
|
|
81
|
+
confirm?: boolean;
|
|
82
|
+
}
|
|
83
|
+
export type CreateOnceResult = {
|
|
84
|
+
status: "created";
|
|
85
|
+
journal_id: number;
|
|
86
|
+
registered: boolean;
|
|
87
|
+
recovered?: boolean;
|
|
88
|
+
} | {
|
|
89
|
+
status: "duplicate";
|
|
90
|
+
journal_id: number;
|
|
91
|
+
};
|
|
92
|
+
export declare class BookingGuard {
|
|
93
|
+
/** The raw snapshot, exposed read-only for callers that still scan directly. */
|
|
94
|
+
readonly journals: readonly Journal[];
|
|
95
|
+
private readonly api;
|
|
96
|
+
private readonly ownDimensionIds;
|
|
97
|
+
private readonly docIndex;
|
|
98
|
+
private readonly interAccountIndex;
|
|
99
|
+
private constructor();
|
|
100
|
+
/**
|
|
101
|
+
* Load one snapshot of all journals (with postings, needed for Lane B) and
|
|
102
|
+
* build both indexes. `ownDimensionIds` is only required for Lane B
|
|
103
|
+
* inter-account matching — Lane A works without it.
|
|
104
|
+
*/
|
|
105
|
+
static load(api: ApiContext, opts?: {
|
|
106
|
+
ownDimensionIds?: Set<number>;
|
|
107
|
+
}): Promise<BookingGuard>;
|
|
108
|
+
/** First artifact for `key` passing `liveness` (default `not_deleted`). */
|
|
109
|
+
find(key: DocKey, liveness?: Liveness): ExistingArtifact | undefined;
|
|
110
|
+
/** Record a freshly-created journal into the in-run Lane A index. */
|
|
111
|
+
record(key: DocKey, journalId: number, meta?: {
|
|
112
|
+
registered?: boolean;
|
|
113
|
+
is_deleted?: boolean;
|
|
114
|
+
}): void;
|
|
115
|
+
/**
|
|
116
|
+
* Guarded journal write. Returns `duplicate` (with the existing journal id)
|
|
117
|
+
* when a live artifact already carries this key; otherwise creates the
|
|
118
|
+
* journal with the document_number stamped, best-effort confirms it (unless
|
|
119
|
+
* `confirm:false`), records it, and returns `created`.
|
|
120
|
+
*
|
|
121
|
+
* The caller's `payload.document_number` is ignored — the guard stamps it
|
|
122
|
+
* from `key` so the key and the stored document_number can never diverge.
|
|
123
|
+
*/
|
|
124
|
+
createJournalOnce(key: DocKey, payload: Omit<Partial<Journal>, "document_number">, opts?: CreateOnceOptions): Promise<CreateOnceResult>;
|
|
125
|
+
/**
|
|
126
|
+
* Scan a raw journal array for the first live (not-deleted) journal carrying
|
|
127
|
+
* `key`'s document_number. Used by the verify-then-retry path against a
|
|
128
|
+
* freshly-fetched snapshot, so it applies the same parse + `not_deleted`
|
|
129
|
+
* liveness that `find` uses without disturbing the in-memory index.
|
|
130
|
+
*/
|
|
131
|
+
private static findKeyInJournals;
|
|
132
|
+
/**
|
|
133
|
+
* Find an existing inter-account journal for the given transfer, or
|
|
134
|
+
* `undefined`. Delegates candidate selection to `findMatchingJournal`
|
|
135
|
+
* (reference disambiguation). When `maxGapDays > 0`, scans the exact date
|
|
136
|
+
* first, then outward day-by-day within the window.
|
|
137
|
+
*/
|
|
138
|
+
findInterAccount(q: InterAccountQuery): number | undefined;
|
|
139
|
+
/** Record a freshly-created inter-account journal into the Lane B index. */
|
|
140
|
+
recordInterAccount(q: Pick<InterAccountQuery, "sourceDim" | "targetDim" | "amount" | "date" | "reference">, journalId: number | undefined): void;
|
|
141
|
+
/**
|
|
142
|
+
* Candidate ISO dates around `date`, ordered nearest-first: the exact date,
|
|
143
|
+
* then -1/+1, -2/+2, … out to `gap` days. Uses UTC day arithmetic so the
|
|
144
|
+
* offsets never drift across DST or timezones.
|
|
145
|
+
*/
|
|
146
|
+
private candidateDates;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=booking-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-guard.d.ts","sourceRoot":"","sources":["../src/booking-guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { HttpError } from "./http-client.js";
|
|
2
|
+
import { roundMoney } from "./money.js";
|
|
3
|
+
import { buildInterAccountJournalIndex, findMatchingJournal, toUtcDay, UNKNOWN_JOURNAL_ID, } from "./tools/inter-account-utils.js";
|
|
4
|
+
/** "FX:123" / "LY:OR-EVN9C76R7A" */
|
|
5
|
+
export function formatDocNumber(key) {
|
|
6
|
+
return `${key.ns}:${key.id}`;
|
|
7
|
+
}
|
|
8
|
+
const KNOWN_NAMESPACES = ["FX", "LY"];
|
|
9
|
+
/**
|
|
10
|
+
* Parse a raw `document_number` into a DocKey, or `undefined` when it does not
|
|
11
|
+
* carry a known namespace prefix (manual journals, legacy entries, …).
|
|
12
|
+
*
|
|
13
|
+
* Only the first ":" is treated as the separator, so a reference that itself
|
|
14
|
+
* contains a colon (unusual, but possible for external ids) round-trips.
|
|
15
|
+
*/
|
|
16
|
+
export function parseDocNumber(raw) {
|
|
17
|
+
if (typeof raw !== "string")
|
|
18
|
+
return undefined;
|
|
19
|
+
const sep = raw.indexOf(":");
|
|
20
|
+
if (sep <= 0)
|
|
21
|
+
return undefined;
|
|
22
|
+
const ns = raw.slice(0, sep);
|
|
23
|
+
const id = raw.slice(sep + 1);
|
|
24
|
+
if (id === "")
|
|
25
|
+
return undefined;
|
|
26
|
+
if (!KNOWN_NAMESPACES.includes(ns))
|
|
27
|
+
return undefined;
|
|
28
|
+
return { ns: ns, id };
|
|
29
|
+
}
|
|
30
|
+
function passesLiveness(a, liveness) {
|
|
31
|
+
switch (liveness) {
|
|
32
|
+
case "any":
|
|
33
|
+
return true;
|
|
34
|
+
case "registered_only":
|
|
35
|
+
return a.registered && !a.is_deleted;
|
|
36
|
+
case "not_deleted":
|
|
37
|
+
default:
|
|
38
|
+
return !a.is_deleted;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function interAccountKey(sourceDim, targetDim, amount, date) {
|
|
42
|
+
return `${sourceDim}|${targetDim}|${roundMoney(amount)}|${date}`;
|
|
43
|
+
}
|
|
44
|
+
export class BookingGuard {
|
|
45
|
+
/** The raw snapshot, exposed read-only for callers that still scan directly. */
|
|
46
|
+
journals;
|
|
47
|
+
api;
|
|
48
|
+
ownDimensionIds;
|
|
49
|
+
// Lane A: document_number -> artifacts (arrays, since duplicates can exist).
|
|
50
|
+
docIndex = new Map();
|
|
51
|
+
// Lane B: bidirectional "sourceDim|targetDim|amount|date" -> journal entries.
|
|
52
|
+
interAccountIndex;
|
|
53
|
+
constructor(api, journals, ownDimensionIds) {
|
|
54
|
+
this.api = api;
|
|
55
|
+
this.journals = journals;
|
|
56
|
+
this.ownDimensionIds = ownDimensionIds;
|
|
57
|
+
this.interAccountIndex = buildInterAccountJournalIndex(journals, ownDimensionIds);
|
|
58
|
+
for (const j of journals) {
|
|
59
|
+
if (j.id == null)
|
|
60
|
+
continue;
|
|
61
|
+
const key = parseDocNumber(j.document_number);
|
|
62
|
+
if (!key)
|
|
63
|
+
continue;
|
|
64
|
+
const dn = formatDocNumber(key);
|
|
65
|
+
const artifact = {
|
|
66
|
+
journal_id: j.id,
|
|
67
|
+
document_number: dn,
|
|
68
|
+
registered: j.registered === true,
|
|
69
|
+
is_deleted: j.is_deleted === true,
|
|
70
|
+
};
|
|
71
|
+
const existing = this.docIndex.get(dn);
|
|
72
|
+
if (existing)
|
|
73
|
+
existing.push(artifact);
|
|
74
|
+
else
|
|
75
|
+
this.docIndex.set(dn, [artifact]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Load one snapshot of all journals (with postings, needed for Lane B) and
|
|
80
|
+
* build both indexes. `ownDimensionIds` is only required for Lane B
|
|
81
|
+
* inter-account matching — Lane A works without it.
|
|
82
|
+
*/
|
|
83
|
+
static async load(api, opts) {
|
|
84
|
+
const ownDimensionIds = opts?.ownDimensionIds ?? new Set();
|
|
85
|
+
// Lane B (inter-account) needs postings to find the two bank legs, and it
|
|
86
|
+
// is only meaningful when the caller supplies its own bank dimension ids.
|
|
87
|
+
// Lane A only reads document_number/id/registered/is_deleted, all of which
|
|
88
|
+
// the cheaper list endpoint carries — so a Lane-A-only caller avoids the
|
|
89
|
+
// per-journal posting fetches that listAllWithPostings performs.
|
|
90
|
+
const needsPostings = ownDimensionIds.size > 0;
|
|
91
|
+
const journals = needsPostings
|
|
92
|
+
? await api.journals.listAllWithPostings()
|
|
93
|
+
: await api.journals.listAll();
|
|
94
|
+
return new BookingGuard(api, journals, ownDimensionIds);
|
|
95
|
+
}
|
|
96
|
+
// ---- Lane A: namespaced document_number keys ---------------------------
|
|
97
|
+
/** First artifact for `key` passing `liveness` (default `not_deleted`). */
|
|
98
|
+
find(key, liveness = "not_deleted") {
|
|
99
|
+
const artifacts = this.docIndex.get(formatDocNumber(key));
|
|
100
|
+
if (!artifacts)
|
|
101
|
+
return undefined;
|
|
102
|
+
return artifacts.find(a => passesLiveness(a, liveness));
|
|
103
|
+
}
|
|
104
|
+
/** Record a freshly-created journal into the in-run Lane A index. */
|
|
105
|
+
record(key, journalId, meta) {
|
|
106
|
+
const dn = formatDocNumber(key);
|
|
107
|
+
const artifact = {
|
|
108
|
+
journal_id: journalId,
|
|
109
|
+
document_number: dn,
|
|
110
|
+
registered: meta?.registered ?? true,
|
|
111
|
+
is_deleted: meta?.is_deleted ?? false,
|
|
112
|
+
};
|
|
113
|
+
const existing = this.docIndex.get(dn);
|
|
114
|
+
if (existing)
|
|
115
|
+
existing.push(artifact);
|
|
116
|
+
else
|
|
117
|
+
this.docIndex.set(dn, [artifact]);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Guarded journal write. Returns `duplicate` (with the existing journal id)
|
|
121
|
+
* when a live artifact already carries this key; otherwise creates the
|
|
122
|
+
* journal with the document_number stamped, best-effort confirms it (unless
|
|
123
|
+
* `confirm:false`), records it, and returns `created`.
|
|
124
|
+
*
|
|
125
|
+
* The caller's `payload.document_number` is ignored — the guard stamps it
|
|
126
|
+
* from `key` so the key and the stored document_number can never diverge.
|
|
127
|
+
*/
|
|
128
|
+
async createJournalOnce(key, payload, opts) {
|
|
129
|
+
const liveness = opts?.liveness ?? "not_deleted";
|
|
130
|
+
const existing = this.find(key, liveness);
|
|
131
|
+
if (existing)
|
|
132
|
+
return { status: "duplicate", journal_id: existing.journal_id };
|
|
133
|
+
const stamped = { ...payload, document_number: formatDocNumber(key) };
|
|
134
|
+
const wantConfirm = opts?.confirm !== false;
|
|
135
|
+
let created;
|
|
136
|
+
try {
|
|
137
|
+
created = await this.api.journals.create(stamped);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
// Only a network error is ambiguous — the POST may or may not have
|
|
141
|
+
// committed. An HTTP status (4xx/5xx with a body) means the server saw
|
|
142
|
+
// and rejected the request, so nothing was created: propagate as-is.
|
|
143
|
+
if (!(err instanceof HttpError) || err.status !== "network")
|
|
144
|
+
throw err;
|
|
145
|
+
// The document_number is a checkable key, so re-scan the server to see
|
|
146
|
+
// whether the ambiguous write actually landed. create() only invalidates
|
|
147
|
+
// its cache on success, so bust the stale journals cache first — the
|
|
148
|
+
// snapshot could otherwise predate the write we are verifying.
|
|
149
|
+
this.api.journals.invalidateListCache();
|
|
150
|
+
const found = BookingGuard.findKeyInJournals(await this.api.journals.listAll(), key);
|
|
151
|
+
if (found) {
|
|
152
|
+
// The ambiguous write DID commit — recover its id, do NOT retry. create()
|
|
153
|
+
// does not auto-confirm, so the recovered journal is typically left in
|
|
154
|
+
// PROJECT; confirm it now (when wanted) exactly as a fresh create would.
|
|
155
|
+
let registered = found.registered;
|
|
156
|
+
if (!registered && wantConfirm) {
|
|
157
|
+
try {
|
|
158
|
+
await this.api.journals.confirm(found.journal_id);
|
|
159
|
+
registered = true;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// Leave the recovered journal in PROJECT for the operator to inspect.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
this.record(key, found.journal_id, { registered, is_deleted: false });
|
|
166
|
+
return { status: "created", journal_id: found.journal_id, registered, recovered: true };
|
|
167
|
+
}
|
|
168
|
+
// The write did not commit — safe to retry exactly once. A second
|
|
169
|
+
// network failure propagates; the key is not double-booked because the
|
|
170
|
+
// next run's find pass (or another recovery) catches it by its
|
|
171
|
+
// document_number.
|
|
172
|
+
created = await this.api.journals.create(stamped);
|
|
173
|
+
}
|
|
174
|
+
const journalId = created.created_object_id;
|
|
175
|
+
let registered = false;
|
|
176
|
+
if (journalId != null && wantConfirm) {
|
|
177
|
+
try {
|
|
178
|
+
await this.api.journals.confirm(journalId);
|
|
179
|
+
registered = true;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Leave the journal in PROJECT for the operator to inspect if confirm
|
|
183
|
+
// fails — mirrors the established currency-rounding / Lightyear path.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (journalId != null) {
|
|
187
|
+
this.record(key, journalId, { registered, is_deleted: false });
|
|
188
|
+
return { status: "created", journal_id: journalId, registered };
|
|
189
|
+
}
|
|
190
|
+
// The API accepted the create but returned no object id. We cannot dedup a
|
|
191
|
+
// future run against an unknown id, but we still record the key so a second
|
|
192
|
+
// call within THIS run is deduped.
|
|
193
|
+
this.record(key, UNKNOWN_JOURNAL_ID, { registered, is_deleted: false });
|
|
194
|
+
return { status: "created", journal_id: UNKNOWN_JOURNAL_ID, registered };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Scan a raw journal array for the first live (not-deleted) journal carrying
|
|
198
|
+
* `key`'s document_number. Used by the verify-then-retry path against a
|
|
199
|
+
* freshly-fetched snapshot, so it applies the same parse + `not_deleted`
|
|
200
|
+
* liveness that `find` uses without disturbing the in-memory index.
|
|
201
|
+
*/
|
|
202
|
+
static findKeyInJournals(journals, key) {
|
|
203
|
+
const target = formatDocNumber(key);
|
|
204
|
+
for (const j of journals) {
|
|
205
|
+
if (j.id == null)
|
|
206
|
+
continue;
|
|
207
|
+
const k = parseDocNumber(j.document_number);
|
|
208
|
+
if (!k || formatDocNumber(k) !== target)
|
|
209
|
+
continue;
|
|
210
|
+
if (j.is_deleted === true)
|
|
211
|
+
continue;
|
|
212
|
+
return {
|
|
213
|
+
journal_id: j.id,
|
|
214
|
+
document_number: target,
|
|
215
|
+
registered: j.registered === true,
|
|
216
|
+
is_deleted: false,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
// ---- Lane B: structural inter-account transfers ------------------------
|
|
222
|
+
/**
|
|
223
|
+
* Find an existing inter-account journal for the given transfer, or
|
|
224
|
+
* `undefined`. Delegates candidate selection to `findMatchingJournal`
|
|
225
|
+
* (reference disambiguation). When `maxGapDays > 0`, scans the exact date
|
|
226
|
+
* first, then outward day-by-day within the window.
|
|
227
|
+
*/
|
|
228
|
+
findInterAccount(q) {
|
|
229
|
+
const gap = Math.max(0, Math.floor(q.maxGapDays ?? 0));
|
|
230
|
+
for (const date of this.candidateDates(q.date, gap)) {
|
|
231
|
+
const key = interAccountKey(q.sourceDim, q.targetDim, q.amount, date);
|
|
232
|
+
const match = findMatchingJournal(this.interAccountIndex.get(key), q.reference);
|
|
233
|
+
if (match !== undefined)
|
|
234
|
+
return match;
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
/** Record a freshly-created inter-account journal into the Lane B index. */
|
|
239
|
+
recordInterAccount(q, journalId) {
|
|
240
|
+
const entry = {
|
|
241
|
+
journal_id: journalId ?? UNKNOWN_JOURNAL_ID,
|
|
242
|
+
document_number: q.reference ?? null,
|
|
243
|
+
};
|
|
244
|
+
const amount = roundMoney(q.amount);
|
|
245
|
+
const key1 = `${q.sourceDim}|${q.targetDim}|${amount}|${q.date}`;
|
|
246
|
+
const key2 = `${q.targetDim}|${q.sourceDim}|${amount}|${q.date}`;
|
|
247
|
+
for (const key of [key1, key2]) {
|
|
248
|
+
const existing = this.interAccountIndex.get(key);
|
|
249
|
+
if (existing)
|
|
250
|
+
existing.push(entry);
|
|
251
|
+
else
|
|
252
|
+
this.interAccountIndex.set(key, [entry]);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Candidate ISO dates around `date`, ordered nearest-first: the exact date,
|
|
257
|
+
* then -1/+1, -2/+2, … out to `gap` days. Uses UTC day arithmetic so the
|
|
258
|
+
* offsets never drift across DST or timezones.
|
|
259
|
+
*/
|
|
260
|
+
candidateDates(date, gap) {
|
|
261
|
+
if (gap === 0)
|
|
262
|
+
return [date];
|
|
263
|
+
const baseMs = toUtcDay(date); // UTC-midnight epoch millis
|
|
264
|
+
if (!Number.isFinite(baseMs))
|
|
265
|
+
return [date];
|
|
266
|
+
const DAY_MS = 86400000;
|
|
267
|
+
const dates = [date];
|
|
268
|
+
for (let d = 1; d <= gap; d++) {
|
|
269
|
+
dates.push(msToIso(baseMs - d * DAY_MS));
|
|
270
|
+
dates.push(msToIso(baseMs + d * DAY_MS));
|
|
271
|
+
}
|
|
272
|
+
return dates;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** Epoch millis at UTC midnight -> "YYYY-MM-DD". */
|
|
276
|
+
function msToIso(ms) {
|
|
277
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=booking-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-guard.js","sourceRoot":"","sources":["../src/booking-guard.ts"],"names":[],"mappings":"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"}
|
package/dist/http-client.d.ts
CHANGED
|
@@ -62,6 +62,15 @@ export declare class HttpClient {
|
|
|
62
62
|
private static recoveryActionsForConflict;
|
|
63
63
|
private static buildRecoveryAdvice;
|
|
64
64
|
private static isRetryableError;
|
|
65
|
+
/**
|
|
66
|
+
* Only idempotent methods may be retried after a network error / timeout.
|
|
67
|
+
* A timed-out or connection-dropped POST/PATCH/DELETE is ambiguous: the
|
|
68
|
+
* server may have already committed the mutation, so a blind retry risks a
|
|
69
|
+
* duplicate booking (invoice, journal, transaction). GET and PUT (full
|
|
70
|
+
* replace) are safe to repeat. Mirrors the GET-only gate in
|
|
71
|
+
* shouldRetryStatus for 5xx responses.
|
|
72
|
+
*/
|
|
73
|
+
private static isIdempotentMethod;
|
|
65
74
|
private static formatNetworkError;
|
|
66
75
|
request<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
|
|
67
76
|
get<T = unknown>(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-client.d.ts","sourceRoot":"","sources":["../src/http-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAI1C,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAErE;;;;;;;;GAQG;AACH,qBAAa,SAAU,SAAQ,KAAK;aAkBhB,MAAM,EAAE,MAAM,GAAG,SAAS;aAC1B,MAAM,EAAE,UAAU;aAClB,IAAI,EAAE,MAAM;IAnB9B;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,CAAC,EAAE,KAAK,CAAC;QAC5B,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC/B,GAAG,EAAE,MAAM,CAAC;KACb,CAAC,CAAC;IAEH,YACE,OAAO,EAAE,MAAM,EACC,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,EAC5B,OAAO,CAAC,EAAE;QACR,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,YAAY,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACrF,EAaF;CACF;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;CAChE;AAKD,qBAAa,UAAU;IAMnB,OAAO,CAAC,MAAM;aACE,cAAc;IAC9B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;IAPhC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAO;IAErC,YACU,MAAM,EAAE,MAAM,EACN,cAAc,SAAiB,EAC9B,YAAY,CAAC,GAAE,MAAM,IAAI,aAAA,EACxC;IAEJ,OAAO,CAAC,oBAAoB;YAId,oBAAoB;mBAcb,KAAK;IAI1B,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAIhC,OAAO,CAAC,MAAM,CAAC,eAAe;IAI9B,OAAO,CAAC,MAAM,CAAC,SAAS;IAOxB,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAmBhC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IAiCzC,OAAO,CAAC,MAAM,CAAC,4BAA4B;IAsB3C,OAAO,CAAC,MAAM,CAAC,0BAA0B;IAmBzC,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAqDlC,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAQ/B,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAU3B,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"http-client.d.ts","sourceRoot":"","sources":["../src/http-client.ts"],"names":[],"mappings":"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"}
|