@happyvertical/smrt-ledgers 0.30.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/dist/index.js ADDED
@@ -0,0 +1,1172 @@
1
+ import { ObjectRegistry, smrt, SmrtHierarchical, SmrtCollection, foreignKey, SmrtObject } from "@happyvertical/smrt-core";
2
+ import { definePrompt, resolvePrompt } from "@happyvertical/smrt-prompts";
3
+ import { tenantId, TenantScoped } from "@happyvertical/smrt-tenancy";
4
+ import { BALANCE_EPSILON } from "./types.js";
5
+ ObjectRegistry.registerPackageManifest(
6
+ new URL("./manifest.json", import.meta.url)
7
+ );
8
+ const smrtLedgersJournalSummarizePrompt = definePrompt({
9
+ key: "smrtLedgers.journal.summarize",
10
+ template: `Summarize this accounting journal entry:
11
+ Number: {journalNumber}
12
+ Date: {journalDate}
13
+ Description: {journalDescription}
14
+ Status: {journalStatus}
15
+ Total: {journalTotal}
16
+ Entries: {entryCount}
17
+ Balanced: {journalBalanced}`,
18
+ editable: {
19
+ template: true,
20
+ profile: true,
21
+ model: true,
22
+ params: true
23
+ }
24
+ });
25
+ function promptMessageOptions(ai) {
26
+ return {
27
+ ...ai.params || {},
28
+ ...ai.model ? { model: ai.model } : {},
29
+ ...typeof ai.temperature === "number" ? { temperature: ai.temperature } : {},
30
+ ...typeof ai.maxTokens === "number" ? { maxTokens: ai.maxTokens } : {}
31
+ };
32
+ }
33
+ var __defProp$2 = Object.defineProperty;
34
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
35
+ var __decorateClass$2 = (decorators, target, key, kind) => {
36
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
37
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
38
+ if (decorator = decorators[i])
39
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
40
+ if (kind && result) __defProp$2(target, key, result);
41
+ return result;
42
+ };
43
+ let Account = class extends SmrtHierarchical {
44
+ tenantId = null;
45
+ /**
46
+ * Account number (e.g., "1000", "5030")
47
+ */
48
+ number = "";
49
+ /**
50
+ * Account name (e.g., "Cash", "Coffee Expense")
51
+ */
52
+ name = "";
53
+ /**
54
+ * Account description
55
+ */
56
+ description = "";
57
+ /**
58
+ * Account type - one of the 5 core types
59
+ */
60
+ type = "asset";
61
+ // parentId inherited from SmrtHierarchical (null = top-level)
62
+ /**
63
+ * Whether the account is active
64
+ */
65
+ active = true;
66
+ /**
67
+ * Extensible metadata
68
+ */
69
+ metadata = {};
70
+ constructor(options = {}) {
71
+ super(options);
72
+ if (options.number !== void 0) this.number = options.number;
73
+ if (options.name !== void 0) this.name = options.name;
74
+ if (options.description !== void 0)
75
+ this.description = options.description;
76
+ if (options.type !== void 0) this.type = options.type;
77
+ if (options.parentId !== void 0) this.parentId = options.parentId;
78
+ if (options.active !== void 0) this.active = options.active;
79
+ if (options.metadata !== void 0) this.metadata = options.metadata;
80
+ }
81
+ /**
82
+ * Check if this is a top-level account (no parent)
83
+ */
84
+ isTopLevel() {
85
+ return this.parentId === null;
86
+ }
87
+ /**
88
+ * Check if this is a debit-normal account (Asset, Expense)
89
+ * Debit-normal accounts increase with debits
90
+ */
91
+ isDebitNormal() {
92
+ return this.type === "asset" || this.type === "expense";
93
+ }
94
+ /**
95
+ * Check if this is a credit-normal account (Liability, Equity, Revenue)
96
+ * Credit-normal accounts increase with credits
97
+ */
98
+ isCreditNormal() {
99
+ return this.type === "liability" || this.type === "equity" || this.type === "revenue";
100
+ }
101
+ // Hierarchy traversal (getParent / getChildren / getAncestors /
102
+ // getDescendants / getHierarchy / moveTo) provided by SmrtHierarchical.
103
+ // Account-specific helpers (getFullPath / getBalance / createChild /
104
+ // toTreeNode) remain below.
105
+ /**
106
+ * Get full account path (e.g., "Assets > Current > Cash")
107
+ */
108
+ async getFullPath() {
109
+ const ancestors = await this.getAncestors();
110
+ const names = [...ancestors.map((a) => a.name), this.name];
111
+ return names.join(" > ");
112
+ }
113
+ /**
114
+ * Build tree node for this account and its children
115
+ */
116
+ async toTreeNode() {
117
+ const children = await this.getChildren();
118
+ const childNodes = await Promise.all(children.map((c) => c.toTreeNode()));
119
+ return {
120
+ account: this,
121
+ children: childNodes
122
+ };
123
+ }
124
+ /**
125
+ * Get the current balance of this account
126
+ */
127
+ async getBalance(asOfDate) {
128
+ if (!this.id) return 0;
129
+ const { JournalEntryCollection: JournalEntryCollection2 } = await Promise.resolve().then(() => JournalEntries);
130
+ const collection = await JournalEntryCollection2.create(
131
+ this.options
132
+ );
133
+ return await collection.getAccountBalance(this.id, asOfDate);
134
+ }
135
+ /**
136
+ * Create a sub-account under this account
137
+ */
138
+ async createChild(options) {
139
+ if (!this.id) {
140
+ throw new Error("Account must be saved before creating children");
141
+ }
142
+ const { AccountCollection: AccountCollection2 } = await Promise.resolve().then(() => Accounts);
143
+ const collection = await AccountCollection2.create(this.options);
144
+ const account = await collection.create({
145
+ ...options,
146
+ parentId: this.id,
147
+ type: this.type
148
+ // Inherit type from parent
149
+ });
150
+ await account.save();
151
+ return account;
152
+ }
153
+ };
154
+ __decorateClass$2([
155
+ tenantId({ nullable: true })
156
+ ], Account.prototype, "tenantId", 2);
157
+ Account = __decorateClass$2([
158
+ TenantScoped({ mode: "optional" }),
159
+ smrt({
160
+ api: { include: ["list", "get", "create", "update", "delete"] },
161
+ mcp: { include: ["list", "get", "create"] },
162
+ cli: true
163
+ })
164
+ ], Account);
165
+ class AccountCollection extends SmrtCollection {
166
+ static _itemClass = Account;
167
+ /**
168
+ * Find account by number
169
+ *
170
+ * @param number - Account number
171
+ * @returns Account or null
172
+ */
173
+ async findByNumber(number) {
174
+ const accounts = await this.list({
175
+ where: { number },
176
+ limit: 1
177
+ });
178
+ return accounts[0] || null;
179
+ }
180
+ /**
181
+ * Find accounts by type
182
+ *
183
+ * @param type - Account type
184
+ * @returns Array of accounts
185
+ */
186
+ async findByType(type) {
187
+ return await this.list({
188
+ where: { type },
189
+ orderBy: "number ASC"
190
+ });
191
+ }
192
+ /**
193
+ * Find all active accounts
194
+ *
195
+ * @returns Array of active accounts
196
+ */
197
+ async findActive() {
198
+ return await this.list({
199
+ where: { active: true },
200
+ orderBy: "number ASC"
201
+ });
202
+ }
203
+ /**
204
+ * Find top-level accounts (no parent)
205
+ *
206
+ * @returns Array of top-level accounts
207
+ */
208
+ async findTopLevel() {
209
+ return await this.list({
210
+ where: { parentId: null },
211
+ orderBy: "number ASC"
212
+ });
213
+ }
214
+ /**
215
+ * Find direct children of an account
216
+ *
217
+ * @param parentId - Parent account ID
218
+ * @returns Array of child accounts
219
+ */
220
+ async findChildren(parentId) {
221
+ return await this.list({
222
+ where: { parentId },
223
+ orderBy: "number ASC"
224
+ });
225
+ }
226
+ /**
227
+ * Get the complete account tree
228
+ *
229
+ * @returns AccountTree structure
230
+ */
231
+ async getTree() {
232
+ const allAccounts = await this.list({
233
+ orderBy: "number ASC"
234
+ });
235
+ const accountMap = /* @__PURE__ */ new Map();
236
+ const childrenMap = /* @__PURE__ */ new Map();
237
+ for (const account of allAccounts) {
238
+ if (account.id) {
239
+ accountMap.set(account.id, account);
240
+ }
241
+ if (!childrenMap.has(account.parentId || "")) {
242
+ childrenMap.set(account.parentId || "", []);
243
+ }
244
+ childrenMap.get(account.parentId || "")?.push(account);
245
+ }
246
+ const buildNode = (account) => {
247
+ const children = (account.id ? childrenMap.get(account.id) : []) || [];
248
+ return {
249
+ account,
250
+ children: children.map(buildNode)
251
+ };
252
+ };
253
+ const roots = childrenMap.get("") || [];
254
+ return {
255
+ roots: roots.map(buildNode)
256
+ };
257
+ }
258
+ /**
259
+ * Get or create an account by number
260
+ *
261
+ * @param number - Account number
262
+ * @param defaults - Default values if creating
263
+ * @returns Account
264
+ */
265
+ async getOrCreateByNumber(number, defaults = {}) {
266
+ const existing = await this.findByNumber(number);
267
+ if (existing) {
268
+ return existing;
269
+ }
270
+ const account = await this.create({
271
+ number,
272
+ name: defaults.name || number,
273
+ description: defaults.description || "",
274
+ type: defaults.type || "asset",
275
+ parentId: defaults.parentId || null
276
+ });
277
+ await account.save();
278
+ return account;
279
+ }
280
+ /**
281
+ * Find accounts grouped by type
282
+ *
283
+ * @returns Record of type to accounts array
284
+ */
285
+ async groupByType() {
286
+ const accounts = await this.findActive();
287
+ const grouped = {
288
+ asset: [],
289
+ liability: [],
290
+ equity: [],
291
+ revenue: [],
292
+ expense: []
293
+ };
294
+ for (const account of accounts) {
295
+ grouped[account.type].push(account);
296
+ }
297
+ return grouped;
298
+ }
299
+ /**
300
+ * Get all descendants of an account recursively
301
+ *
302
+ * @param accountId - Account ID
303
+ * @returns Array of all descendant accounts
304
+ */
305
+ async getDescendants(accountId) {
306
+ const descendants = [];
307
+ const queue = [accountId];
308
+ while (queue.length > 0) {
309
+ const currentId = queue.shift();
310
+ if (!currentId) continue;
311
+ const children = await this.findChildren(currentId);
312
+ for (const child of children) {
313
+ descendants.push(child);
314
+ if (child.id) {
315
+ queue.push(child.id);
316
+ }
317
+ }
318
+ }
319
+ return descendants;
320
+ }
321
+ // ─────────────────────────────────────────────────────────────────────────────
322
+ // Tenant Helper Methods
323
+ // ─────────────────────────────────────────────────────────────────────────────
324
+ /**
325
+ * Find all accounts belonging to a specific tenant
326
+ *
327
+ * @param tenantId - Tenant ID
328
+ * @returns Array of tenant's accounts
329
+ */
330
+ async findByTenant(tenantId2) {
331
+ return this.list({ where: { tenantId: tenantId2 } });
332
+ }
333
+ /**
334
+ * Find all global accounts (no tenant association)
335
+ *
336
+ * @returns Array of global accounts
337
+ */
338
+ async findGlobal() {
339
+ return this.list({ where: { tenantId: null } });
340
+ }
341
+ /**
342
+ * Find accounts for a tenant plus all global accounts
343
+ *
344
+ * @param tenantId - Tenant ID
345
+ * @returns Array of tenant's accounts and global accounts
346
+ */
347
+ async findWithGlobals(tenantId2) {
348
+ return this.query(
349
+ `SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`,
350
+ [tenantId2]
351
+ );
352
+ }
353
+ }
354
+ const Accounts = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
355
+ __proto__: null,
356
+ AccountCollection
357
+ }, Symbol.toStringTag, { value: "Module" }));
358
+ var __defProp$1 = Object.defineProperty;
359
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
360
+ var __decorateClass$1 = (decorators, target, key, kind) => {
361
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
362
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
363
+ if (decorator = decorators[i])
364
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
365
+ if (kind && result) __defProp$1(target, key, result);
366
+ return result;
367
+ };
368
+ let JournalEntry = class extends SmrtObject {
369
+ tenantId = null;
370
+ journalId = "";
371
+ accountId = "";
372
+ /**
373
+ * Debit amount (left side)
374
+ */
375
+ debit = 0;
376
+ /**
377
+ * Credit amount (right side)
378
+ */
379
+ credit = 0;
380
+ /**
381
+ * Currency code (e.g., "USD", "CAD", "EUR")
382
+ */
383
+ currency = "USD";
384
+ /**
385
+ * Exchange rate to base currency
386
+ */
387
+ exchangeRate = 1;
388
+ /**
389
+ * Line memo/description
390
+ */
391
+ memo = "";
392
+ /**
393
+ * Extensible metadata
394
+ */
395
+ metadata = {};
396
+ constructor(options = {}) {
397
+ super(options);
398
+ if (options.journalId !== void 0) this.journalId = options.journalId;
399
+ if (options.accountId !== void 0) this.accountId = options.accountId;
400
+ if (options.debit !== void 0) this.debit = options.debit;
401
+ if (options.credit !== void 0) this.credit = options.credit;
402
+ if (options.currency !== void 0) this.currency = options.currency;
403
+ if (options.exchangeRate !== void 0)
404
+ this.exchangeRate = options.exchangeRate;
405
+ if (options.memo !== void 0) this.memo = options.memo;
406
+ if (options.metadata !== void 0) this.metadata = options.metadata;
407
+ }
408
+ /**
409
+ * Validate entry before save
410
+ */
411
+ async validateBeforeSave() {
412
+ await super.validateBeforeSave();
413
+ if (this.debit < 0) {
414
+ throw new Error("Debit amount cannot be negative");
415
+ }
416
+ if (this.credit < 0) {
417
+ throw new Error("Credit amount cannot be negative");
418
+ }
419
+ if (this.debit > 0 && this.credit > 0) {
420
+ throw new Error("Entry cannot have both debit and credit amounts");
421
+ }
422
+ if (this.debit === 0 && this.credit === 0) {
423
+ throw new Error("Entry must have either a debit or credit amount");
424
+ }
425
+ if (this.exchangeRate <= 0) {
426
+ throw new Error("Exchange rate must be positive");
427
+ }
428
+ }
429
+ /**
430
+ * Check if this is a debit entry
431
+ */
432
+ isDebit() {
433
+ return this.debit > 0;
434
+ }
435
+ /**
436
+ * Check if this is a credit entry
437
+ */
438
+ isCredit() {
439
+ return this.credit > 0;
440
+ }
441
+ /**
442
+ * Get the entry amount (positive value regardless of debit/credit)
443
+ */
444
+ getAmount() {
445
+ return this.debit > 0 ? this.debit : this.credit;
446
+ }
447
+ /**
448
+ * Get the amount in base currency
449
+ */
450
+ getBaseAmount() {
451
+ return this.getAmount() * this.exchangeRate;
452
+ }
453
+ /**
454
+ * Get the parent journal
455
+ */
456
+ async getJournal() {
457
+ if (!this.journalId) return null;
458
+ const { JournalCollection: JournalCollection2 } = await Promise.resolve().then(() => Journals);
459
+ const collection = await JournalCollection2.create(this.options);
460
+ return await collection.get({ id: this.journalId });
461
+ }
462
+ /**
463
+ * Get the account
464
+ */
465
+ async getAccount() {
466
+ if (!this.accountId) return null;
467
+ const { AccountCollection: AccountCollection2 } = await Promise.resolve().then(() => Accounts);
468
+ const collection = await AccountCollection2.create(this.options);
469
+ return await collection.get({ id: this.accountId });
470
+ }
471
+ /**
472
+ * Get a formatted description of this entry
473
+ */
474
+ async getDescription() {
475
+ const account = await this.getAccount();
476
+ const accountName = account?.name || "Unknown Account";
477
+ const type = this.isDebit() ? "DR" : "CR";
478
+ const amount = this.getAmount().toFixed(2);
479
+ return `${type} ${accountName}: $${amount}${this.memo ? ` - ${this.memo}` : ""}`;
480
+ }
481
+ };
482
+ __decorateClass$1([
483
+ tenantId({ nullable: true })
484
+ ], JournalEntry.prototype, "tenantId", 2);
485
+ __decorateClass$1([
486
+ foreignKey("Journal")
487
+ ], JournalEntry.prototype, "journalId", 2);
488
+ __decorateClass$1([
489
+ foreignKey("Account")
490
+ ], JournalEntry.prototype, "accountId", 2);
491
+ JournalEntry = __decorateClass$1([
492
+ TenantScoped({ mode: "optional" }),
493
+ smrt({
494
+ api: { include: ["list", "get"] },
495
+ // Created via Journal, not directly
496
+ mcp: { include: ["list", "get"] },
497
+ cli: true
498
+ })
499
+ ], JournalEntry);
500
+ class JournalEntryCollection extends SmrtCollection {
501
+ static _itemClass = JournalEntry;
502
+ /**
503
+ * Find entries by journal
504
+ *
505
+ * @param journalId - Journal ID
506
+ * @returns Array of entries
507
+ */
508
+ async findByJournal(journalId) {
509
+ return await this.list({
510
+ where: { journalId }
511
+ });
512
+ }
513
+ /**
514
+ * Find entries by account
515
+ *
516
+ * @param accountId - Account ID
517
+ * @returns Array of entries
518
+ */
519
+ async findByAccount(accountId) {
520
+ return await this.list({
521
+ where: { accountId }
522
+ });
523
+ }
524
+ /**
525
+ * Get account balance
526
+ *
527
+ * For debit-normal accounts (Asset, Expense): balance = debits - credits
528
+ * For credit-normal accounts (Liability, Equity, Revenue): balance = credits - debits
529
+ *
530
+ * @param accountId - Account ID
531
+ * @param asOfDate - Optional date to calculate balance as of
532
+ * @returns Account balance
533
+ */
534
+ async getAccountBalance(accountId, asOfDate) {
535
+ const { JournalCollection: JournalCollection2 } = await Promise.resolve().then(() => Journals);
536
+ const journalCollection = await JournalCollection2.create(
537
+ this.options
538
+ );
539
+ const { AccountCollection: AccountCollection2 } = await Promise.resolve().then(() => Accounts);
540
+ const accountCollection = await AccountCollection2.create(
541
+ this.options
542
+ );
543
+ const account = await accountCollection.get({ id: accountId });
544
+ if (!account) {
545
+ throw new Error(`Account not found: ${accountId}`);
546
+ }
547
+ const allEntries = await this.findByAccount(accountId);
548
+ let totalDebits = 0;
549
+ let totalCredits = 0;
550
+ for (const entry of allEntries) {
551
+ const journal = await journalCollection.get({ id: entry.journalId });
552
+ if (!journal) continue;
553
+ if (!journal.isPosted()) continue;
554
+ if (asOfDate && journal.date > asOfDate) continue;
555
+ totalDebits += entry.debit;
556
+ totalCredits += entry.credit;
557
+ }
558
+ if (account.isDebitNormal()) {
559
+ return totalDebits - totalCredits;
560
+ } else {
561
+ return totalCredits - totalDebits;
562
+ }
563
+ }
564
+ /**
565
+ * Get trial balance
566
+ *
567
+ * @param asOfDate - Optional date for trial balance
568
+ * @returns Array of trial balance rows
569
+ */
570
+ async getTrialBalance(asOfDate) {
571
+ const { AccountCollection: AccountCollection2 } = await Promise.resolve().then(() => Accounts);
572
+ const accountCollection = await AccountCollection2.create(
573
+ this.options
574
+ );
575
+ const accounts = await accountCollection.findActive();
576
+ const rows = [];
577
+ for (const account of accounts) {
578
+ if (!account.id) continue;
579
+ const balance = await this.getAccountBalance(account.id, asOfDate);
580
+ if (Math.abs(balance) < BALANCE_EPSILON) continue;
581
+ rows.push({
582
+ accountId: account.id,
583
+ accountNumber: account.number,
584
+ accountName: account.name,
585
+ accountType: account.type,
586
+ debitBalance: account.isDebitNormal() && balance > 0 ? balance : 0,
587
+ creditBalance: account.isCreditNormal() && balance > 0 ? balance : 0
588
+ });
589
+ }
590
+ rows.sort((a, b) => a.accountNumber.localeCompare(b.accountNumber));
591
+ return rows;
592
+ }
593
+ /**
594
+ * Get total debits and credits for a date range
595
+ *
596
+ * @param start - Start date
597
+ * @param end - End date
598
+ * @returns Object with totalDebits and totalCredits
599
+ */
600
+ async getTotalsForDateRange(start, end) {
601
+ const { JournalCollection: JournalCollection2 } = await Promise.resolve().then(() => Journals);
602
+ const journalCollection = await JournalCollection2.create(
603
+ this.options
604
+ );
605
+ const journals = await journalCollection.findByDateRange(start, end);
606
+ let totalDebits = 0;
607
+ let totalCredits = 0;
608
+ for (const journal of journals) {
609
+ if (!journal.isPosted()) continue;
610
+ const entries = await this.findByJournal(journal.id);
611
+ for (const entry of entries) {
612
+ totalDebits += entry.debit;
613
+ totalCredits += entry.credit;
614
+ }
615
+ }
616
+ return { totalDebits, totalCredits };
617
+ }
618
+ /**
619
+ * Get entries for multiple accounts
620
+ *
621
+ * @param accountIds - Array of account IDs
622
+ * @returns Array of entries
623
+ */
624
+ async findByAccounts(accountIds) {
625
+ if (accountIds.length === 0) {
626
+ return [];
627
+ }
628
+ return await this.list({
629
+ where: { accountId: accountIds }
630
+ });
631
+ }
632
+ /**
633
+ * Get the running balance for an account (list of entries with running total)
634
+ *
635
+ * @param accountId - Account ID
636
+ * @returns Array of entries with running balance
637
+ */
638
+ async getAccountLedger(accountId) {
639
+ const { JournalCollection: JournalCollection2 } = await Promise.resolve().then(() => Journals);
640
+ const { AccountCollection: AccountCollection2 } = await Promise.resolve().then(() => Accounts);
641
+ const journalCollection = await JournalCollection2.create(
642
+ this.options
643
+ );
644
+ const accountCollection = await AccountCollection2.create(
645
+ this.options
646
+ );
647
+ const account = await accountCollection.get({ id: accountId });
648
+ if (!account) {
649
+ throw new Error(`Account not found: ${accountId}`);
650
+ }
651
+ const entries = await this.findByAccount(accountId);
652
+ const ledger = [];
653
+ let runningBalance = 0;
654
+ const entriesWithJournals = await Promise.all(
655
+ entries.map(async (entry) => ({
656
+ entry,
657
+ journal: await journalCollection.get({ id: entry.journalId })
658
+ }))
659
+ );
660
+ entriesWithJournals.sort((a, b) => {
661
+ if (!a.journal || !b.journal) return 0;
662
+ return a.journal.date.getTime() - b.journal.date.getTime();
663
+ });
664
+ for (const { entry, journal } of entriesWithJournals) {
665
+ if (!journal || !journal.isPosted()) continue;
666
+ if (account.isDebitNormal()) {
667
+ runningBalance += entry.debit - entry.credit;
668
+ } else {
669
+ runningBalance += entry.credit - entry.debit;
670
+ }
671
+ ledger.push({ entry, runningBalance });
672
+ }
673
+ return ledger;
674
+ }
675
+ // ─────────────────────────────────────────────────────────────────────────────
676
+ // Tenant Helper Methods
677
+ // ─────────────────────────────────────────────────────────────────────────────
678
+ /**
679
+ * Find all journal entries belonging to a specific tenant
680
+ *
681
+ * @param tenantId - Tenant ID
682
+ * @returns Array of tenant's journal entries
683
+ */
684
+ async findByTenant(tenantId2) {
685
+ return this.list({ where: { tenantId: tenantId2 } });
686
+ }
687
+ /**
688
+ * Find all global journal entries (no tenant association)
689
+ *
690
+ * @returns Array of global journal entries
691
+ */
692
+ async findGlobal() {
693
+ return this.list({ where: { tenantId: null } });
694
+ }
695
+ /**
696
+ * Find journal entries for a tenant plus all global entries
697
+ *
698
+ * @param tenantId - Tenant ID
699
+ * @returns Array of tenant's entries and global entries
700
+ */
701
+ async findWithGlobals(tenantId2) {
702
+ return this.query(
703
+ `SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`,
704
+ [tenantId2]
705
+ );
706
+ }
707
+ }
708
+ const JournalEntries = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
709
+ __proto__: null,
710
+ JournalEntryCollection
711
+ }, Symbol.toStringTag, { value: "Module" }));
712
+ var __defProp = Object.defineProperty;
713
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
714
+ var __decorateClass = (decorators, target, key, kind) => {
715
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
716
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
717
+ if (decorator = decorators[i])
718
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
719
+ if (kind && result) __defProp(target, key, result);
720
+ return result;
721
+ };
722
+ let Journal = class extends SmrtObject {
723
+ tenantId = null;
724
+ /**
725
+ * Journal number (auto-generated sequence, e.g., "JNL-0001")
726
+ */
727
+ number = "";
728
+ /**
729
+ * Transaction date
730
+ */
731
+ date = /* @__PURE__ */ new Date();
732
+ /**
733
+ * Description of the financial event
734
+ */
735
+ description = "";
736
+ /**
737
+ * Source module that created this journal (e.g., "smrt-commerce", "manual")
738
+ */
739
+ sourceModule = "";
740
+ /**
741
+ * External reference (e.g., order ID, invoice number)
742
+ */
743
+ sourceRef = null;
744
+ /**
745
+ * Journal status: draft, posted, voided
746
+ */
747
+ status = "draft";
748
+ /**
749
+ * When the journal was posted (finalized)
750
+ */
751
+ postedAt = null;
752
+ /**
753
+ * When the journal was voided
754
+ */
755
+ voidedAt = null;
756
+ /**
757
+ * Reason for voiding (if voided)
758
+ */
759
+ voidReason = null;
760
+ /**
761
+ * Extensible metadata
762
+ */
763
+ metadata = {};
764
+ constructor(options = {}) {
765
+ super(options);
766
+ if (options.number !== void 0) this.number = options.number;
767
+ if (options.date !== void 0) this.date = options.date;
768
+ if (options.description !== void 0)
769
+ this.description = options.description;
770
+ if (options.sourceModule !== void 0)
771
+ this.sourceModule = options.sourceModule;
772
+ if (options.sourceRef !== void 0) this.sourceRef = options.sourceRef;
773
+ if (options.status !== void 0) this.status = options.status;
774
+ if (options.postedAt !== void 0) this.postedAt = options.postedAt;
775
+ if (options.voidedAt !== void 0) this.voidedAt = options.voidedAt;
776
+ if (options.voidReason !== void 0) this.voidReason = options.voidReason;
777
+ if (options.metadata !== void 0) this.metadata = options.metadata;
778
+ }
779
+ /**
780
+ * Check if journal is in draft status
781
+ */
782
+ isDraft() {
783
+ return this.status === "draft";
784
+ }
785
+ /**
786
+ * Check if journal has been posted
787
+ */
788
+ isPosted() {
789
+ return this.status === "posted";
790
+ }
791
+ /**
792
+ * Check if journal has been voided
793
+ */
794
+ isVoided() {
795
+ return this.status === "voided";
796
+ }
797
+ /**
798
+ * Check if journal can be modified
799
+ */
800
+ isEditable() {
801
+ return this.status === "draft";
802
+ }
803
+ /**
804
+ * Get all entries for this journal
805
+ */
806
+ async getEntries() {
807
+ if (!this.id) return [];
808
+ const { JournalEntryCollection: JournalEntryCollection2 } = await Promise.resolve().then(() => JournalEntries);
809
+ const collection = await JournalEntryCollection2.create(
810
+ this.options
811
+ );
812
+ return await collection.findByJournal(this.id);
813
+ }
814
+ /**
815
+ * Calculate total debits
816
+ */
817
+ async getTotalDebits() {
818
+ const entries = await this.getEntries();
819
+ return entries.reduce((sum, entry) => sum + entry.debit, 0);
820
+ }
821
+ /**
822
+ * Calculate total credits
823
+ */
824
+ async getTotalCredits() {
825
+ const entries = await this.getEntries();
826
+ return entries.reduce((sum, entry) => sum + entry.credit, 0);
827
+ }
828
+ /**
829
+ * Check if journal entries are balanced (debits = credits)
830
+ */
831
+ async isBalanced() {
832
+ const debits = await this.getTotalDebits();
833
+ const credits = await this.getTotalCredits();
834
+ return Math.abs(debits - credits) < BALANCE_EPSILON;
835
+ }
836
+ /**
837
+ * Add an entry to this journal (only if draft)
838
+ */
839
+ async addEntry(data) {
840
+ if (!this.id) {
841
+ throw new Error("Journal must be saved before adding entries");
842
+ }
843
+ if (!this.isEditable()) {
844
+ throw new Error("Cannot add entries to a posted or voided journal");
845
+ }
846
+ const { JournalEntryCollection: JournalEntryCollection2 } = await Promise.resolve().then(() => JournalEntries);
847
+ const collection = await JournalEntryCollection2.create(
848
+ this.options
849
+ );
850
+ const entry = await collection.create({
851
+ journalId: this.id,
852
+ accountId: data.accountId,
853
+ debit: data.debit || 0,
854
+ credit: data.credit || 0,
855
+ currency: data.currency || "USD",
856
+ exchangeRate: data.exchangeRate || 1,
857
+ memo: data.memo || ""
858
+ });
859
+ await entry.save();
860
+ }
861
+ /**
862
+ * Post the journal (finalize and make immutable)
863
+ */
864
+ async post() {
865
+ if (!this.isDraft()) {
866
+ throw new Error("Only draft journals can be posted");
867
+ }
868
+ const balanced = await this.isBalanced();
869
+ if (!balanced) {
870
+ const debits = await this.getTotalDebits();
871
+ const credits = await this.getTotalCredits();
872
+ throw new Error(
873
+ `Journal is not balanced. Debits: ${debits}, Credits: ${credits}`
874
+ );
875
+ }
876
+ const entries = await this.getEntries();
877
+ if (entries.length === 0) {
878
+ throw new Error("Journal must have at least one entry");
879
+ }
880
+ this.status = "posted";
881
+ this.postedAt = /* @__PURE__ */ new Date();
882
+ await this.save();
883
+ }
884
+ /**
885
+ * Void the journal (mark as cancelled with reason)
886
+ */
887
+ async void(reason) {
888
+ if (this.isVoided()) {
889
+ throw new Error("Journal is already voided");
890
+ }
891
+ this.status = "voided";
892
+ this.voidedAt = /* @__PURE__ */ new Date();
893
+ this.voidReason = reason;
894
+ await this.save();
895
+ }
896
+ /**
897
+ * AI-powered: Generate a summary of this journal.
898
+ *
899
+ * Uses the `smrtLedgers.journal.summarize` prompt registered via
900
+ * `@happyvertical/smrt-prompts`, allowing tenant- or instance-level
901
+ * overrides of the template, model, and parameters at runtime.
902
+ *
903
+ * Only non-PII journal fields (number, date, description, status, balanced
904
+ * flag) plus aggregate totals and entry count are sent to the AI provider.
905
+ * Internal foreign-key fields (tenantId, sourceRef, individual entry
906
+ * account IDs) and the extensible `metadata` blob are intentionally
907
+ * excluded.
908
+ *
909
+ * @returns Generated summary text
910
+ */
911
+ async summarize() {
912
+ const entries = await this.getEntries();
913
+ const debits = await this.getTotalDebits();
914
+ const balanced = await this.isBalanced();
915
+ const db = this.options.db ?? this.options.persistence;
916
+ const resolvedPrompt = await resolvePrompt(
917
+ smrtLedgersJournalSummarizePrompt.key,
918
+ {
919
+ db,
920
+ tenantId: this.tenantId,
921
+ variables: {
922
+ journalNumber: this.number || "",
923
+ journalDate: this.date.toISOString().split("T")[0],
924
+ journalDescription: this.description || "",
925
+ journalStatus: this.status || "",
926
+ // Currency prefix folded into the value rather than the template
927
+ // — see the `smrtLedgersJournalSummarizePrompt` comment block.
928
+ journalTotal: `$${debits.toFixed(2)}`,
929
+ entryCount: String(entries.length),
930
+ journalBalanced: balanced ? "Yes" : "No"
931
+ }
932
+ }
933
+ );
934
+ const ai = await this.getAiClient();
935
+ const response = await ai.message(
936
+ resolvedPrompt.text,
937
+ promptMessageOptions(resolvedPrompt.ai)
938
+ );
939
+ return response.trim();
940
+ }
941
+ };
942
+ __decorateClass([
943
+ tenantId({ nullable: true })
944
+ ], Journal.prototype, "tenantId", 2);
945
+ Journal = __decorateClass([
946
+ TenantScoped({ mode: "optional" }),
947
+ smrt({
948
+ api: { include: ["list", "get", "create"] },
949
+ // No update/delete - immutable after posting
950
+ mcp: { include: ["list", "get", "create"] },
951
+ cli: true
952
+ })
953
+ ], Journal);
954
+ class JournalCollection extends SmrtCollection {
955
+ static _itemClass = Journal;
956
+ /**
957
+ * Generate next journal number
958
+ *
959
+ * Uses timestamp and random component to avoid collisions
960
+ * in concurrent environments without relying on shared state.
961
+ */
962
+ generateJournalNumber() {
963
+ const timestamp = Date.now().toString(36);
964
+ const randomPart = Math.random().toString(36).slice(2, 6);
965
+ return `JNL-${timestamp}-${randomPart}`;
966
+ }
967
+ /**
968
+ * Find journal by number
969
+ *
970
+ * @param number - Journal number
971
+ * @returns Journal or null
972
+ */
973
+ async findByNumber(number) {
974
+ const journals = await this.list({
975
+ where: { number },
976
+ limit: 1
977
+ });
978
+ return journals[0] || null;
979
+ }
980
+ /**
981
+ * Find journals by date range
982
+ *
983
+ * @param start - Start date
984
+ * @param end - End date
985
+ * @returns Array of journals
986
+ */
987
+ async findByDateRange(start, end) {
988
+ return await this.list({
989
+ where: {
990
+ "date >=": start.toISOString(),
991
+ "date <=": end.toISOString()
992
+ },
993
+ orderBy: "date ASC"
994
+ });
995
+ }
996
+ /**
997
+ * Find journals by source module
998
+ *
999
+ * @param sourceModule - Source module name
1000
+ * @returns Array of journals
1001
+ */
1002
+ async findBySource(sourceModule) {
1003
+ return await this.list({
1004
+ where: { sourceModule },
1005
+ orderBy: "date DESC"
1006
+ });
1007
+ }
1008
+ /**
1009
+ * Find journals by status
1010
+ *
1011
+ * @param status - Journal status
1012
+ * @returns Array of journals
1013
+ */
1014
+ async findByStatus(status) {
1015
+ return await this.list({
1016
+ where: { status },
1017
+ orderBy: "date DESC"
1018
+ });
1019
+ }
1020
+ /**
1021
+ * Find draft journals
1022
+ *
1023
+ * @returns Array of draft journals
1024
+ */
1025
+ async findDrafts() {
1026
+ return await this.findByStatus("draft");
1027
+ }
1028
+ /**
1029
+ * Find posted journals
1030
+ *
1031
+ * @returns Array of posted journals
1032
+ */
1033
+ async findPosted() {
1034
+ return await this.findByStatus("posted");
1035
+ }
1036
+ /**
1037
+ * Create a complete journal with entries
1038
+ *
1039
+ * @param data - Journal data with entries
1040
+ * @returns Created journal
1041
+ */
1042
+ async createWithEntries(data) {
1043
+ let totalDebits = 0;
1044
+ let totalCredits = 0;
1045
+ for (const entry of data.entries) {
1046
+ totalDebits += entry.debit || 0;
1047
+ totalCredits += entry.credit || 0;
1048
+ }
1049
+ if (Math.abs(totalDebits - totalCredits) >= BALANCE_EPSILON) {
1050
+ throw new Error(
1051
+ `Entries are not balanced. Debits: ${totalDebits}, Credits: ${totalCredits}`
1052
+ );
1053
+ }
1054
+ if (data.entries.length === 0) {
1055
+ throw new Error("Journal must have at least one entry");
1056
+ }
1057
+ const journalNumber = await this.generateJournalNumber();
1058
+ const journal = await this.create({
1059
+ number: journalNumber,
1060
+ date: data.date || /* @__PURE__ */ new Date(),
1061
+ description: data.description,
1062
+ sourceModule: data.sourceModule || "manual",
1063
+ sourceRef: data.sourceRef || null,
1064
+ metadata: data.metadata || {}
1065
+ });
1066
+ await journal.save();
1067
+ for (const entryData of data.entries) {
1068
+ await journal.addEntry(entryData);
1069
+ }
1070
+ return journal;
1071
+ }
1072
+ /**
1073
+ * Post a journal by ID
1074
+ *
1075
+ * @param journalId - Journal ID
1076
+ * @returns Posted journal
1077
+ */
1078
+ async post(journalId) {
1079
+ const journal = await this.get({ id: journalId });
1080
+ if (!journal) {
1081
+ throw new Error(`Journal not found: ${journalId}`);
1082
+ }
1083
+ await journal.post();
1084
+ return journal;
1085
+ }
1086
+ /**
1087
+ * Void a journal by ID
1088
+ *
1089
+ * @param journalId - Journal ID
1090
+ * @param reason - Reason for voiding
1091
+ * @returns Voided journal
1092
+ */
1093
+ async void(journalId, reason) {
1094
+ const journal = await this.get({ id: journalId });
1095
+ if (!journal) {
1096
+ throw new Error(`Journal not found: ${journalId}`);
1097
+ }
1098
+ await journal.void(reason);
1099
+ return journal;
1100
+ }
1101
+ /**
1102
+ * Find journals by source reference
1103
+ *
1104
+ * @param sourceRef - External reference
1105
+ * @returns Array of journals
1106
+ */
1107
+ async findBySourceRef(sourceRef) {
1108
+ return await this.list({
1109
+ where: { sourceRef },
1110
+ orderBy: "date DESC"
1111
+ });
1112
+ }
1113
+ /**
1114
+ * Get journals for a specific month
1115
+ *
1116
+ * @param year - Year
1117
+ * @param month - Month (1-12)
1118
+ * @returns Array of journals
1119
+ */
1120
+ async findByMonth(year, month) {
1121
+ const start = new Date(year, month - 1, 1);
1122
+ const end = new Date(year, month, 0, 23, 59, 59, 999);
1123
+ return await this.findByDateRange(start, end);
1124
+ }
1125
+ // ─────────────────────────────────────────────────────────────────────────────
1126
+ // Tenant Helper Methods
1127
+ // ─────────────────────────────────────────────────────────────────────────────
1128
+ /**
1129
+ * Find all journals belonging to a specific tenant
1130
+ *
1131
+ * @param tenantId - Tenant ID
1132
+ * @returns Array of tenant's journals
1133
+ */
1134
+ async findByTenant(tenantId2) {
1135
+ return this.list({ where: { tenantId: tenantId2 } });
1136
+ }
1137
+ /**
1138
+ * Find all global journals (no tenant association)
1139
+ *
1140
+ * @returns Array of global journals
1141
+ */
1142
+ async findGlobal() {
1143
+ return this.list({ where: { tenantId: null } });
1144
+ }
1145
+ /**
1146
+ * Find journals for a tenant plus all global journals
1147
+ *
1148
+ * @param tenantId - Tenant ID
1149
+ * @returns Array of tenant's journals and global journals
1150
+ */
1151
+ async findWithGlobals(tenantId2) {
1152
+ return this.query(
1153
+ `SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`,
1154
+ [tenantId2]
1155
+ );
1156
+ }
1157
+ }
1158
+ const Journals = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1159
+ __proto__: null,
1160
+ JournalCollection
1161
+ }, Symbol.toStringTag, { value: "Module" }));
1162
+ export {
1163
+ Account,
1164
+ AccountCollection,
1165
+ Journal,
1166
+ JournalCollection,
1167
+ JournalEntry,
1168
+ JournalEntryCollection,
1169
+ promptMessageOptions,
1170
+ smrtLedgersJournalSummarizePrompt
1171
+ };
1172
+ //# sourceMappingURL=index.js.map