@classytic/ledger 0.9.0 → 0.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/ledger",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Production-grade double-entry accounting engine for MongoDB — schemas, reports, tax, multi-tenant",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -90,12 +90,15 @@
90
90
  "license": "MIT",
91
91
  "repository": {
92
92
  "type": "git",
93
- "url": "git+https://github.com/classytic/accounting.git"
93
+ "url": "git+https://github.com/classytic/ledger.git"
94
94
  },
95
95
  "peerDependencies": {
96
96
  "@classytic/fin-io": ">=0.1.0",
97
- "@classytic/mongokit": ">=3.6.4",
98
- "mongoose": ">=9.4.1"
97
+ "@classytic/mongokit": ">=3.11.0",
98
+ "@classytic/primitives": ">=0.1.0",
99
+ "@classytic/repo-core": ">=0.2.0",
100
+ "mongoose": ">=9.4.1",
101
+ "zod": ">=4.0.0"
99
102
  },
100
103
  "peerDependenciesMeta": {
101
104
  "@classytic/fin-io": {
@@ -103,17 +106,20 @@
103
106
  }
104
107
  },
105
108
  "devDependencies": {
106
- "@biomejs/biome": "^2.4.10",
107
- "@classytic/fin-io": "file:../fin-io",
108
- "@classytic/mongokit": "^3.6.4",
109
+ "@biomejs/biome": "^2.4.12",
110
+ "@classytic/fin-io": ">=0.1.0",
111
+ "@classytic/mongokit": ">=3.11.0",
112
+ "@classytic/primitives": ">=0.1.0",
113
+ "@classytic/repo-core": ">=0.2.0",
109
114
  "@types/node": "^25.5.0",
110
- "@vitest/coverage-v8": "^3.2.4",
111
- "knip": "^6.3.0",
112
- "mongodb-memory-server": "^10.2.3",
115
+ "@vitest/coverage-v8": "^4.1.4",
116
+ "knip": "^6.4.1",
117
+ "mongodb-memory-server": "^11.0.1",
113
118
  "mongoose": "^9.4.1",
114
- "tsdown": "^0.21.5",
119
+ "tsdown": "^0.21.8",
115
120
  "typescript": "^5.7.0",
116
- "vitest": "^3.0.0"
121
+ "vitest": "^4.1.4",
122
+ "zod": "^4.3.6"
117
123
  },
118
124
  "engines": {
119
125
  "node": ">=22"
@@ -121,8 +127,11 @@
121
127
  "scripts": {
122
128
  "build": "tsdown",
123
129
  "dev": "tsdown --watch",
124
- "test": "vitest run",
125
- "test:watch": "vitest",
130
+ "test": "vitest run --project unit --project integration",
131
+ "test:unit": "vitest run --project unit",
132
+ "test:integration": "vitest run --project integration",
133
+ "test:all": "vitest run",
134
+ "test:watch": "vitest --project unit --project integration",
126
135
  "test:coverage": "vitest run --coverage",
127
136
  "typecheck": "tsc --noEmit",
128
137
  "lint": "biome check src/",
@@ -1,132 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- //#region src/events/event-constants.ts
3
- /**
4
- * Ledger domain event names.
5
- *
6
- * Convention: `ledger:<resource>.<verb>` — matches the catalog/revenue/flow
7
- * namespace pattern so hosts can subscribe with a single glob
8
- * (`ledger:entry.*`, `ledger:*`).
9
- */
10
- const LEDGER_EVENTS = {
11
- ENTRY_CREATED: "ledger:entry.created",
12
- ENTRY_POSTED: "ledger:entry.posted",
13
- ENTRY_UNPOSTED: "ledger:entry.unposted",
14
- ENTRY_ARCHIVED: "ledger:entry.archived",
15
- ENTRY_DUPLICATED: "ledger:entry.duplicated",
16
- ENTRY_REVERSED: "ledger:entry.reversed",
17
- ACCOUNT_SEEDED: "ledger:account.seeded",
18
- ACCOUNT_BULK_CREATED: "ledger:account.bulk-created",
19
- JOURNAL_SEEDED: "ledger:journal.seeded",
20
- RECONCILIATION_MATCHED: "ledger:reconciliation.matched",
21
- RECONCILIATION_UNMATCHED: "ledger:reconciliation.unmatched"
22
- };
23
- //#endregion
24
- //#region src/events/helpers.ts
25
- /**
26
- * Event creation helper.
27
- *
28
- * Builds a well-formed DomainEvent with auto-generated `meta.id` + `meta.timestamp`.
29
- * Threads optional context fields (userId, organizationId, correlationId) that
30
- * hosts rely on for audit trails and distributed tracing.
31
- */
32
- function toIdString(value) {
33
- if (value == null) return void 0;
34
- if (typeof value === "string") return value;
35
- if (typeof value === "number" || typeof value === "bigint") return String(value);
36
- const obj = value;
37
- if (typeof obj.toHexString === "function") return obj.toHexString();
38
- if (typeof obj.toString === "function") {
39
- const s = obj.toString();
40
- return s === "[object Object]" ? void 0 : s;
41
- }
42
- }
43
- function createEvent(type, payload, ctx, meta) {
44
- return {
45
- type,
46
- payload,
47
- meta: {
48
- id: randomUUID(),
49
- timestamp: /* @__PURE__ */ new Date(),
50
- userId: toIdString(ctx?.actorId),
51
- organizationId: toIdString(ctx?.organizationId),
52
- correlationId: ctx?.correlationId ?? ctx?.traceId,
53
- ...meta
54
- }
55
- };
56
- }
57
- //#endregion
58
- //#region src/events/in-process-bus.ts
59
- var InProcessLedgerBus = class {
60
- name = "in-process-ledger";
61
- handlers = /* @__PURE__ */ new Map();
62
- logger;
63
- constructor(options) {
64
- this.logger = options?.logger ?? console;
65
- }
66
- async publish(event) {
67
- const exact = this.handlers.get(event.type) ?? /* @__PURE__ */ new Set();
68
- const wildcard = this.handlers.get("*") ?? /* @__PURE__ */ new Set();
69
- const pattern = /* @__PURE__ */ new Set();
70
- for (const [p, handlers] of this.handlers.entries()) if (p.endsWith(".*")) {
71
- const prefix = p.slice(0, -2);
72
- if (event.type.startsWith(`${prefix}.`)) for (const h of handlers) pattern.add(h);
73
- } else if (p.endsWith(":*")) {
74
- const prefix = p.slice(0, -2);
75
- if (event.type.startsWith(`${prefix}:`)) for (const h of handlers) pattern.add(h);
76
- }
77
- const all = new Set([
78
- ...exact,
79
- ...wildcard,
80
- ...pattern
81
- ]);
82
- for (const handler of all) try {
83
- await handler(event);
84
- } catch (err) {
85
- this.logger.error(`[InProcessLedgerBus] Handler error for ${event.type}:`, err);
86
- }
87
- }
88
- async publishMany(events) {
89
- const results = /* @__PURE__ */ new Map();
90
- for (const event of events) try {
91
- await this.publish(event);
92
- results.set(event.meta.id, null);
93
- } catch (err) {
94
- results.set(event.meta.id, err instanceof Error ? err : new Error(String(err)));
95
- }
96
- return results;
97
- }
98
- async subscribe(pattern, handler) {
99
- if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
100
- this.handlers.get(pattern)?.add(handler);
101
- return () => {
102
- const set = this.handlers.get(pattern);
103
- if (set) {
104
- set.delete(handler);
105
- if (set.size === 0) this.handlers.delete(pattern);
106
- }
107
- };
108
- }
109
- async close() {
110
- this.handlers.clear();
111
- }
112
- };
113
- //#endregion
114
- //#region src/events/outbox-store.ts
115
- /**
116
- * Thrown by a store when `acknowledge` / `fail` is called by a consumer that
117
- * does not own the event's current lease.
118
- */
119
- var OutboxOwnershipError = class extends Error {
120
- eventId;
121
- attemptedBy;
122
- currentOwner;
123
- constructor(eventId, attemptedBy, currentOwner) {
124
- super(`Outbox ownership mismatch for event "${eventId}": attempted by "${attemptedBy}", current owner is "${currentOwner ?? "none"}". The lease may have expired and been reclaimed by another worker.`);
125
- this.name = "OutboxOwnershipError";
126
- this.eventId = eventId;
127
- this.attemptedBy = attemptedBy;
128
- this.currentOwner = currentOwner;
129
- }
130
- };
131
- //#endregion
132
- export { LEDGER_EVENTS as i, InProcessLedgerBus as n, createEvent as r, OutboxOwnershipError as t };
@@ -1,249 +0,0 @@
1
- //#region src/events/event-constants.d.ts
2
- /**
3
- * Ledger domain event names.
4
- *
5
- * Convention: `ledger:<resource>.<verb>` — matches the catalog/revenue/flow
6
- * namespace pattern so hosts can subscribe with a single glob
7
- * (`ledger:entry.*`, `ledger:*`).
8
- */
9
- declare const LEDGER_EVENTS: {
10
- readonly ENTRY_CREATED: "ledger:entry.created";
11
- readonly ENTRY_POSTED: "ledger:entry.posted";
12
- readonly ENTRY_UNPOSTED: "ledger:entry.unposted";
13
- readonly ENTRY_ARCHIVED: "ledger:entry.archived";
14
- readonly ENTRY_DUPLICATED: "ledger:entry.duplicated";
15
- readonly ENTRY_REVERSED: "ledger:entry.reversed";
16
- readonly ACCOUNT_SEEDED: "ledger:account.seeded";
17
- readonly ACCOUNT_BULK_CREATED: "ledger:account.bulk-created";
18
- readonly JOURNAL_SEEDED: "ledger:journal.seeded";
19
- readonly RECONCILIATION_MATCHED: "ledger:reconciliation.matched";
20
- readonly RECONCILIATION_UNMATCHED: "ledger:reconciliation.unmatched";
21
- };
22
- type LedgerEventName = (typeof LEDGER_EVENTS)[keyof typeof LEDGER_EVENTS];
23
- //#endregion
24
- //#region src/events/event-payloads.d.ts
25
- /**
26
- * Typed payload definitions for ledger domain events.
27
- */
28
- interface EntryCreatedPayload {
29
- entryId: unknown;
30
- journalType?: string;
31
- state: string;
32
- referenceNumber?: string;
33
- idempotencyKey?: string;
34
- organizationId?: unknown;
35
- }
36
- interface EntryPostedPayload {
37
- entryId: unknown;
38
- referenceNumber?: string;
39
- postedBy?: unknown;
40
- totalDebit: number;
41
- totalCredit: number;
42
- organizationId?: unknown;
43
- }
44
- interface EntryUnpostedPayload {
45
- entryId: unknown;
46
- unpostedBy?: unknown;
47
- organizationId?: unknown;
48
- }
49
- interface EntryArchivedPayload {
50
- entryId: unknown;
51
- archivedBy?: unknown;
52
- organizationId?: unknown;
53
- }
54
- interface EntryDuplicatedPayload {
55
- sourceEntryId: unknown;
56
- duplicateEntryId: unknown;
57
- organizationId?: unknown;
58
- }
59
- interface EntryReversedPayload {
60
- originalEntryId: unknown;
61
- reversalEntryId: unknown;
62
- reversalDate: Date;
63
- reversedBy?: unknown;
64
- organizationId?: unknown;
65
- }
66
- interface AccountSeededPayload {
67
- created: number;
68
- skipped: number;
69
- organizationId?: unknown;
70
- }
71
- interface AccountBulkCreatedPayload {
72
- created: number;
73
- skipped: number;
74
- errors: number;
75
- organizationId?: unknown;
76
- }
77
- interface JournalSeededPayload {
78
- created: number;
79
- skipped: number;
80
- organizationId?: unknown;
81
- }
82
- interface ReconciliationMatchedPayload {
83
- matchingNumber: string;
84
- account: unknown;
85
- itemCount: number;
86
- debitTotal: number;
87
- creditTotal: number;
88
- isFullReconcile: boolean;
89
- currency: string | null;
90
- organizationId?: unknown;
91
- }
92
- interface ReconciliationUnmatchedPayload {
93
- matchingNumber: string;
94
- itemCount: number;
95
- organizationId?: unknown;
96
- }
97
- //#endregion
98
- //#region src/events/transport.d.ts
99
- /**
100
- * EventTransport — structurally identical to @classytic/arc's EventTransport.
101
- *
102
- * DO NOT import from @classytic/arc at runtime. TypeScript structural typing
103
- * means any arc transport (Memory, Redis, BullMQ, Kafka) is assignable to this
104
- * interface with zero adapter code, as long as the shape matches exactly.
105
- *
106
- * See: packages/arc/src/events/EventTransport.ts
107
- */
108
- interface DomainEvent<T = unknown> {
109
- type: string;
110
- payload: T;
111
- meta: {
112
- id: string;
113
- timestamp: Date;
114
- resource?: string;
115
- resourceId?: string;
116
- userId?: string;
117
- organizationId?: string;
118
- correlationId?: string;
119
- };
120
- }
121
- type EventHandler<T = unknown> = (event: DomainEvent<T>) => void | Promise<void>;
122
- /**
123
- * Per-event outcome returned by `EventTransport.publishMany`.
124
- * Key is `event.meta.id`; value is `null` for success or `Error` for per-event failure.
125
- */
126
- type PublishManyResult = ReadonlyMap<string, Error | null>;
127
- interface EventLogger {
128
- warn(message: string, ...args: unknown[]): void;
129
- error(message: string, ...args: unknown[]): void;
130
- }
131
- interface EventTransport {
132
- readonly name: string;
133
- publish(event: DomainEvent): Promise<void>;
134
- publishMany?(events: readonly DomainEvent[]): Promise<PublishManyResult>;
135
- subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
136
- close?(): Promise<void>;
137
- }
138
- //#endregion
139
- //#region src/events/helpers.d.ts
140
- /** Minimal context shape for event metadata. */
141
- interface EventContext {
142
- actorId?: unknown;
143
- organizationId?: unknown;
144
- correlationId?: string;
145
- traceId?: string;
146
- }
147
- declare function createEvent<T>(type: string, payload: T, ctx?: EventContext, meta?: Partial<DomainEvent['meta']>): DomainEvent<T>;
148
- //#endregion
149
- //#region src/events/in-process-bus.d.ts
150
- interface InProcessLedgerBusOptions {
151
- logger?: EventLogger;
152
- }
153
- declare class InProcessLedgerBus implements EventTransport {
154
- readonly name = "in-process-ledger";
155
- private handlers;
156
- private logger;
157
- constructor(options?: InProcessLedgerBusOptions);
158
- publish(event: DomainEvent): Promise<void>;
159
- publishMany(events: readonly DomainEvent[]): Promise<PublishManyResult>;
160
- subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
161
- close(): Promise<void>;
162
- }
163
- //#endregion
164
- //#region src/events/outbox-store.d.ts
165
- /** Options passed to `OutboxStore.save` for richer write semantics. */
166
- interface OutboxWriteOptions {
167
- /** Host-provided DB session/transaction handle for atomic writes. */
168
- readonly session?: unknown;
169
- /** Earliest time the event should be visible to relay workers. */
170
- readonly visibleAt?: Date;
171
- /** Idempotency key — stores that support it should dedupe on this. */
172
- readonly dedupeKey?: string;
173
- /** Partition/routing key for sharded transports (Kafka, Redis Streams). */
174
- readonly partitionKey?: string;
175
- /** Arbitrary headers propagated to the transport layer. */
176
- readonly headers?: Readonly<Record<string, string>>;
177
- }
178
- /** Options for lease-based work claim. */
179
- interface OutboxClaimOptions {
180
- readonly limit?: number;
181
- readonly consumerId?: string;
182
- readonly leaseMs?: number;
183
- readonly types?: readonly string[];
184
- }
185
- /** Options for `OutboxStore.acknowledge`. */
186
- interface OutboxAcknowledgeOptions {
187
- readonly consumerId?: string;
188
- }
189
- /** Options for `OutboxStore.fail`. */
190
- interface OutboxFailOptions {
191
- readonly consumerId?: string;
192
- readonly retryAt?: Date;
193
- readonly deadLetter?: boolean;
194
- }
195
- /** Normalized error info passed to `OutboxStore.fail`. */
196
- interface OutboxErrorInfo {
197
- readonly message: string;
198
- readonly code?: string;
199
- }
200
- /**
201
- * Thrown by a store when `acknowledge` / `fail` is called by a consumer that
202
- * does not own the event's current lease.
203
- */
204
- declare class OutboxOwnershipError extends Error {
205
- readonly eventId: string;
206
- readonly attemptedBy: string;
207
- readonly currentOwner: string | null;
208
- constructor(eventId: string, attemptedBy: string, currentOwner: string | null);
209
- }
210
- /**
211
- * Durable storage contract for the transactional outbox pattern.
212
- *
213
- * **Required**: `save`, `getPending`, `acknowledge`.
214
- * **Optional** (stores opt-in): `claimPending`, `fail`, `purge`.
215
- *
216
- * Structurally identical to arc's `OutboxStore` — assignable in both
217
- * directions via TypeScript structural typing.
218
- */
219
- interface OutboxStore {
220
- /**
221
- * Save event to outbox (typically inside a business transaction via `options.session`).
222
- * MUST reject events missing `type` or `meta.id` — throw rather than persist.
223
- */
224
- save(event: DomainEvent, options?: OutboxWriteOptions): Promise<void>;
225
- /**
226
- * Get pending (unrelayed) events, FIFO ordered.
227
- * Multi-worker deployments should prefer `claimPending` if supported.
228
- */
229
- getPending(limit: number): Promise<DomainEvent[]>;
230
- /**
231
- * Atomically claim pending events with a lease.
232
- * Two concurrent callers MUST never receive overlapping events.
233
- */
234
- claimPending?(options?: OutboxClaimOptions): Promise<DomainEvent[]>;
235
- /**
236
- * Mark event as successfully relayed. Unknown eventId is a no-op (idempotent).
237
- * Ownership mismatch MUST throw `OutboxOwnershipError`.
238
- */
239
- acknowledge(eventId: string, options?: OutboxAcknowledgeOptions): Promise<void>;
240
- /**
241
- * Record a relay failure. Enables retry scheduling and dead-letter flow.
242
- * Ownership mismatch MUST throw `OutboxOwnershipError`.
243
- */
244
- fail?(eventId: string, error: OutboxErrorInfo, options?: OutboxFailOptions): Promise<void>;
245
- /** Purge old delivered events (older than `olderThanMs`). Returns count. */
246
- purge?(olderThanMs: number): Promise<number>;
247
- }
248
- //#endregion
249
- export { EntryReversedPayload as C, ReconciliationUnmatchedPayload as D, ReconciliationMatchedPayload as E, LEDGER_EVENTS as O, EntryPostedPayload as S, JournalSeededPayload as T, AccountBulkCreatedPayload as _, OutboxOwnershipError as a, EntryCreatedPayload as b, InProcessLedgerBus as c, createEvent as d, DomainEvent as f, PublishManyResult as g, EventTransport as h, OutboxFailOptions as i, LedgerEventName as k, InProcessLedgerBusOptions as l, EventLogger as m, OutboxClaimOptions as n, OutboxStore as o, EventHandler as p, OutboxErrorInfo as r, OutboxWriteOptions as s, OutboxAcknowledgeOptions as t, EventContext as u, AccountSeededPayload as v, EntryUnpostedPayload as w, EntryDuplicatedPayload as x, EntryArchivedPayload as y };