@accounter/server 0.0.9-alpha-20251231123714-6cdd9de71b4672d74ece5d34c438d162987b2c93 → 0.0.9-alpha-20251231163357-33d4c33fec5e21dad2e04d1f293f6248d2550588

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +27 -5
  2. package/README.md +66 -3
  3. package/dist/server/scripts/seed-admin-context.js +20 -25
  4. package/dist/server/scripts/seed-admin-context.js.map +1 -1
  5. package/dist/server/src/__tests__/db-bootstrap.test.js +7 -2
  6. package/dist/server/src/__tests__/db-bootstrap.test.js.map +1 -1
  7. package/dist/server/src/__tests__/factories/business.d.ts +1 -1
  8. package/dist/server/src/__tests__/factories/financial-account.d.ts +1 -1
  9. package/dist/server/src/__tests__/factories/index.test.js +1 -0
  10. package/dist/server/src/__tests__/factories/index.test.js.map +1 -1
  11. package/dist/server/src/__tests__/factories/tax-category.d.ts +1 -1
  12. package/dist/server/src/__tests__/factories/tax-category.js +1 -1
  13. package/dist/server/src/__tests__/factories/tax-category.js.map +1 -1
  14. package/dist/server/src/__tests__/factories/tax-category.test.js +8 -6
  15. package/dist/server/src/__tests__/factories/tax-category.test.js.map +1 -1
  16. package/dist/server/src/__tests__/helpers/fixture-loader.d.ts +1 -1
  17. package/dist/server/src/__tests__/helpers/fixture-loader.js +25 -52
  18. package/dist/server/src/__tests__/helpers/fixture-loader.js.map +1 -1
  19. package/dist/server/src/__tests__/helpers/migration-verification.d.ts +1 -1
  20. package/dist/server/src/__tests__/helpers/migration-verification.js.map +1 -1
  21. package/dist/server/src/__tests__/helpers/seed-helpers.business.test.js +4 -4
  22. package/dist/server/src/__tests__/helpers/seed-helpers.business.test.js.map +1 -1
  23. package/dist/server/src/__tests__/helpers/seed-helpers.d.ts +9 -9
  24. package/dist/server/src/__tests__/helpers/seed-helpers.js +57 -54
  25. package/dist/server/src/__tests__/helpers/seed-helpers.js.map +1 -1
  26. package/dist/server/src/__tests__/seed-admin-context.integration.test.js +2 -1
  27. package/dist/server/src/__tests__/seed-admin-context.integration.test.js.map +1 -1
  28. package/dist/server/src/demo-fixtures/__tests__/deterministic-uuid.test.js +3 -2
  29. package/dist/server/src/demo-fixtures/__tests__/deterministic-uuid.test.js.map +1 -1
  30. package/dist/server/src/demo-fixtures/__tests__/seed-and-validate.test.d.ts +1 -0
  31. package/dist/server/src/demo-fixtures/__tests__/seed-and-validate.test.js +69 -0
  32. package/dist/server/src/demo-fixtures/__tests__/seed-and-validate.test.js.map +1 -0
  33. package/dist/server/src/demo-fixtures/__tests__/use-case-registry.test.d.ts +1 -0
  34. package/dist/server/src/demo-fixtures/__tests__/use-case-registry.test.js +26 -0
  35. package/dist/server/src/demo-fixtures/__tests__/use-case-registry.test.js.map +1 -0
  36. package/dist/server/src/demo-fixtures/helpers/admin-context.d.ts +10 -0
  37. package/dist/server/src/demo-fixtures/helpers/admin-context.js +40 -0
  38. package/dist/server/src/demo-fixtures/helpers/admin-context.js.map +1 -0
  39. package/dist/server/src/demo-fixtures/helpers/placeholder.d.ts +45 -0
  40. package/dist/server/src/demo-fixtures/helpers/placeholder.js +50 -0
  41. package/dist/server/src/demo-fixtures/helpers/placeholder.js.map +1 -0
  42. package/dist/server/src/demo-fixtures/helpers/seed-exchange-rates.d.ts +21 -0
  43. package/dist/server/src/demo-fixtures/helpers/seed-exchange-rates.js +26 -0
  44. package/dist/server/src/demo-fixtures/helpers/seed-exchange-rates.js.map +1 -0
  45. package/dist/server/src/demo-fixtures/helpers/seed-vat.d.ts +20 -0
  46. package/dist/server/src/demo-fixtures/helpers/seed-vat.js +30 -0
  47. package/dist/server/src/demo-fixtures/helpers/seed-vat.js.map +1 -0
  48. package/dist/server/src/demo-fixtures/use-cases/equity/shareholder-dividend.d.ts +9 -0
  49. package/dist/server/src/demo-fixtures/use-cases/equity/shareholder-dividend.js +86 -0
  50. package/dist/server/src/demo-fixtures/use-cases/equity/shareholder-dividend.js.map +1 -0
  51. package/dist/server/src/demo-fixtures/use-cases/expenses/monthly-expense-foreign-currency.d.ts +12 -0
  52. package/dist/server/src/demo-fixtures/use-cases/expenses/monthly-expense-foreign-currency.js +375 -0
  53. package/dist/server/src/demo-fixtures/use-cases/expenses/monthly-expense-foreign-currency.js.map +1 -0
  54. package/dist/server/src/demo-fixtures/use-cases/income/client-payment-with-refund.d.ts +10 -0
  55. package/dist/server/src/demo-fixtures/use-cases/income/client-payment-with-refund.js +113 -0
  56. package/dist/server/src/demo-fixtures/use-cases/income/client-payment-with-refund.js.map +1 -0
  57. package/dist/server/src/demo-fixtures/use-cases/index.d.ts +41 -0
  58. package/dist/server/src/demo-fixtures/use-cases/index.js +50 -0
  59. package/dist/server/src/demo-fixtures/use-cases/index.js.map +1 -0
  60. package/dist/server/src/demo-fixtures/validate-demo-data.d.ts +1 -0
  61. package/dist/server/src/demo-fixtures/validate-demo-data.js +117 -0
  62. package/dist/server/src/demo-fixtures/validate-demo-data.js.map +1 -0
  63. package/dist/server/src/demo-fixtures/validators/ledger-validators.d.ts +349 -0
  64. package/dist/server/src/demo-fixtures/validators/ledger-validators.js +602 -0
  65. package/dist/server/src/demo-fixtures/validators/ledger-validators.js.map +1 -0
  66. package/dist/server/src/demo-fixtures/validators/ledger-validators.test.d.ts +1 -0
  67. package/dist/server/src/demo-fixtures/validators/ledger-validators.test.js +247 -0
  68. package/dist/server/src/demo-fixtures/validators/ledger-validators.test.js.map +1 -0
  69. package/dist/server/src/demo-fixtures/validators/types.d.ts +69 -0
  70. package/dist/server/src/demo-fixtures/validators/types.js +8 -0
  71. package/dist/server/src/demo-fixtures/validators/types.js.map +1 -0
  72. package/dist/server/src/fixtures/fixture-spec.d.ts +146 -0
  73. package/dist/server/src/fixtures/fixture-spec.js +2 -0
  74. package/dist/server/src/fixtures/fixture-spec.js.map +1 -0
  75. package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js +4 -0
  76. package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js.map +1 -1
  77. package/dist/server/src/modules/deel/resolvers/deel.resolvers.js +0 -3
  78. package/dist/server/src/modules/deel/resolvers/deel.resolvers.js.map +1 -1
  79. package/dist/server/src/modules/ledger/__tests__/ledger-scenario-a.integration.test.js +4 -3
  80. package/dist/server/src/modules/ledger/__tests__/ledger-scenario-a.integration.test.js.map +1 -1
  81. package/dist/server/src/modules/ledger/__tests__/ledger-scenario-b.integration.test.js +5 -3
  82. package/dist/server/src/modules/ledger/__tests__/ledger-scenario-b.integration.test.js.map +1 -1
  83. package/dist/server/src/shared/constants.d.ts +1 -0
  84. package/dist/server/src/shared/constants.js +1 -0
  85. package/dist/server/src/shared/constants.js.map +1 -1
  86. package/dist/server/src/shared/helpers/misc.js +2 -2
  87. package/dist/server/src/shared/helpers/misc.js.map +1 -1
  88. package/docs/demo-staging-guide.md +611 -0
  89. package/package.json +5 -2
  90. package/scripts/seed-admin-context.ts +22 -33
  91. package/src/__tests__/db-bootstrap.test.ts +9 -2
  92. package/src/__tests__/factories/business.ts +1 -1
  93. package/src/__tests__/factories/financial-account.ts +1 -1
  94. package/src/__tests__/factories/index.test.ts +1 -0
  95. package/src/__tests__/factories/tax-category.test.ts +8 -6
  96. package/src/__tests__/factories/tax-category.ts +2 -2
  97. package/src/__tests__/helpers/fixture-loader.ts +26 -61
  98. package/src/__tests__/helpers/migration-verification.ts +2 -2
  99. package/src/__tests__/helpers/seed-helpers.business.test.ts +4 -4
  100. package/src/__tests__/helpers/seed-helpers.ts +66 -75
  101. package/src/__tests__/seed-admin-context.integration.test.ts +2 -1
  102. package/src/demo-fixtures/__tests__/deterministic-uuid.test.ts +3 -2
  103. package/src/demo-fixtures/__tests__/seed-and-validate.test.ts +96 -0
  104. package/src/demo-fixtures/__tests__/use-case-registry.test.ts +27 -0
  105. package/src/demo-fixtures/helpers/admin-context.ts +59 -0
  106. package/src/demo-fixtures/helpers/placeholder.ts +50 -0
  107. package/src/demo-fixtures/helpers/seed-exchange-rates.ts +29 -0
  108. package/src/demo-fixtures/helpers/seed-vat.ts +35 -0
  109. package/src/demo-fixtures/use-cases/equity/shareholder-dividend.ts +88 -0
  110. package/src/demo-fixtures/use-cases/expenses/monthly-expense-foreign-currency.ts +377 -0
  111. package/src/demo-fixtures/use-cases/income/client-payment-with-refund.ts +115 -0
  112. package/src/demo-fixtures/use-cases/index.ts +52 -0
  113. package/src/demo-fixtures/validate-demo-data.ts +153 -0
  114. package/src/demo-fixtures/validators/README.md +190 -0
  115. package/src/demo-fixtures/validators/ledger-validators.test.ts +298 -0
  116. package/src/demo-fixtures/validators/ledger-validators.ts +711 -0
  117. package/src/demo-fixtures/validators/types.ts +83 -0
  118. package/src/fixtures/fixture-spec.ts +158 -0
  119. package/src/modules/charges-matcher/__tests__/single-match-integration.test.ts +6 -0
  120. package/src/modules/deel/resolvers/deel.resolvers.ts +0 -3
  121. package/src/modules/ledger/__tests__/ledger-scenario-a.integration.test.ts +4 -3
  122. package/src/modules/ledger/__tests__/ledger-scenario-b.integration.test.ts +6 -3
  123. package/src/shared/constants.ts +2 -0
  124. package/src/shared/helpers/misc.ts +2 -3
@@ -0,0 +1,190 @@
1
+ # Ledger Validators - Overview and purpose
2
+
3
+ This module provides a practical, production-like validation system for double-entry ledger data
4
+ seeded as part of the demo fixtures. It verifies that generated ledger records are coherent,
5
+ balanced, and conform to business and data integrity rules.
6
+
7
+ The main entry point is `validateLedgerRecords(records, expectedRecordCount, context)` which runs
8
+ all validations (FR1–FR10) and returns an array of human-readable error messages. An empty array
9
+ means the ledger data passed validation.
10
+
11
+ ## Architecture - File structure and responsibilities
12
+
13
+ ```
14
+ packages/server/src/demo-fixtures/
15
+ ├── validate-demo-data.ts # Script that connects to DB and runs validation per use-case
16
+ └── validators/
17
+ ├── ledger-validators.ts # Core validators (FR1–FR10) + small utilities
18
+ └── types.ts # Shared types: LedgerRecord, ValidationContext, EntityBalance
19
+ ```
20
+
21
+ - `validate-demo-data.ts`
22
+ - Queries demo DB for ledger records per use-case
23
+ - Builds a `ValidationContext`
24
+ - Calls `validateLedgerRecords(...)` and reports results
25
+ - `validators/ledger-validators.ts`
26
+ - Contains all rule validators and a master function to run them all
27
+ - Utilities: `parseAmount`, `isBalanced`
28
+ - `validators/types.ts`
29
+ - Shared TypeScript interfaces used across validators
30
+
31
+ ## Validation Rules
32
+
33
+ All validators return `string[]` of error messages. No errors = rule passed.
34
+
35
+ 1. FR1: Per-Record Internal Balance
36
+ - Debit total equals credit total per record within tolerance.
37
+ - Formula: `(debit1 + debit2) == (credit1 + credit2)`.
38
+ - Error: `{use_case_id} - Record {i} ({record_id}): internal imbalance (debit={X}, credit={Y})`.
39
+
40
+ 2. FR2: Aggregate Balance Validation
41
+ - Total debits equal total credits across all records in the set.
42
+ - Error: `{use_case_id}: aggregate ledger not balanced (debit X, credit Y)`.
43
+
44
+ 3. FR3: Entity-Level Balance Validation
45
+ - Each entity’s net position across all records must balance to ~0.
46
+ - Error: `{use_case_id}: Entity {entity_id} unbalanced (net={amount})`.
47
+
48
+ 4. FR4: Orphaned Amount Detection
49
+ - Amount columns must have a corresponding entity; secondary pairs should be both null or both
50
+ set.
51
+ - Error: `{use_case_id} - Record {i} ({record_id}): orphaned amount in {column} ...`.
52
+
53
+ 5. FR5: Positive Amount Validation
54
+ - All amount fields must be `≥ 0`.
55
+ - Error: `{use_case_id} - Record {i} ({record_id}): negative amount in {column} ({value})`.
56
+
57
+ 6. FR6: Foreign Currency Validation
58
+ - Non-ILS currency requires foreign amounts; ILS must not have foreign amounts; basic FX sanity
59
+ check.
60
+ - Error examples:
61
+ - `foreign currency (...) but no foreign amounts`
62
+ - `local currency (...) but has foreign amounts`
63
+ - `suspicious exchange rate in {field} (rate=...)`
64
+
65
+ 7. FR7: Date Validation
66
+ - `invoice_date` and `value_date` required, valid, and within range (2020–2030).
67
+ - Error examples: `missing invoice_date`, `invalid value_date`,
68
+ `invoice_date out of range (...)`.
69
+
70
+ 8. FR8: Record Count Validation
71
+ - Validates record count against the expected count for the use-case.
72
+ - Error: `{use_case_id}: ledger record count mismatch (expected X, got Y)`.
73
+
74
+ 9. FR9: Multi-Use-Case Validation
75
+ - Run validations for all use-cases with expectations; aggregate all errors.
76
+
77
+ 10. FR10: Empty Ledger Detection
78
+ - Each record must have a non-zero total (debit + credit > 0).
79
+ - Error: `{use_case_id} - Record {i} ({record_id}): empty record (all amounts zero)`.
80
+
81
+ ## Usage Example
82
+
83
+ ```ts
84
+ import { validateLedgerRecords } from './validators/ledger-validators.js'
85
+ import type { LedgerRecord, ValidationContext } from './validators/types.js'
86
+
87
+ const context: ValidationContext = {
88
+ useCaseId: 'example-use-case',
89
+ defaultCurrency: 'ILS',
90
+ tolerance: 0.005
91
+ }
92
+
93
+ const records: LedgerRecord[] = [
94
+ {
95
+ id: 'rec-1',
96
+ charge_id: 'charge-1',
97
+ owner_id: 'owner-1',
98
+
99
+ debit_entity1: 'entity-1',
100
+ debit_local_amount1: '100.00',
101
+ debit_foreign_amount1: null,
102
+
103
+ debit_entity2: null,
104
+ debit_local_amount2: null,
105
+ debit_foreign_amount2: null,
106
+
107
+ credit_entity1: 'entity-2',
108
+ credit_local_amount1: '100.00',
109
+ credit_foreign_amount1: null,
110
+
111
+ credit_entity2: null,
112
+ credit_local_amount2: null,
113
+ credit_foreign_amount2: null,
114
+
115
+ currency: 'ILS',
116
+ invoice_date: new Date('2024-01-01') as any,
117
+ value_date: new Date('2024-01-01') as any,
118
+ description: 'Demo',
119
+ reference1: null,
120
+ locked: false
121
+ }
122
+ ]
123
+
124
+ const expectedRecordCount = 1
125
+ const errors = validateLedgerRecords(records, expectedRecordCount, context)
126
+
127
+ if (errors.length === 0) {
128
+ console.log('✅ Ledger valid')
129
+ } else {
130
+ console.error('❌ Validation failed:', errors)
131
+ }
132
+ ```
133
+
134
+ ## Adding New Validators
135
+
136
+ - Create a validator function following the existing pattern:
137
+
138
+ ```ts
139
+ /**
140
+ * Validates specific rule X
141
+ */
142
+ export function validateRuleX(records: LedgerRecord[], context: ValidationContext): string[] {
143
+ const errors: string[] = []
144
+ // ... push error strings when violations are found
145
+ return errors
146
+ }
147
+ ```
148
+
149
+ - Add your validator to the master function order in `validateLedgerRecords(...)`.
150
+ - Write unit tests in `validators/ledger-validators.test.ts` (positive + negative cases).
151
+ - Update this README (Rules list, examples if helpful).
152
+
153
+ ## Testing
154
+
155
+ Run just the validators tests:
156
+
157
+ ```bash
158
+ yarn vitest run packages/server/src/demo-fixtures/validators
159
+ ```
160
+
161
+ Run the whole test suite:
162
+
163
+ ```bash
164
+ yarn test
165
+ ```
166
+
167
+ Optionally run the demo validation script end-to-end (requires DB + seeded data):
168
+
169
+ ```bash
170
+ # Setup and seed (example commands may vary by project scripts)
171
+ yarn db:test:setup
172
+ yarn seed:staging-demo
173
+
174
+ # Run validation
175
+ tsx packages/server/src/demo-fixtures/validate-demo-data.ts
176
+ ```
177
+
178
+ ## Error Message Format
179
+
180
+ All errors follow a standardized, actionable format so you can quickly locate the problem:
181
+
182
+ ```
183
+ {use_case_id} - Record {index} ({record_id}): {specific_issue} ({details})
184
+ ```
185
+
186
+ - `use_case_id`: Which scenario/use-case produced the record(s)
187
+ - `index`: Zero-based index within the current record set
188
+ - `record_id`: The specific ledger record ID for pinpointing in DB/logs
189
+ - `specific_issue`: Human-readable summary (e.g., "internal imbalance")
190
+ - `details`: Helpful numeric context (e.g., debit/credit totals, rates, fields)
@@ -0,0 +1,298 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ isBalanced,
4
+ parseAmount,
5
+ validateDates,
6
+ validateEntityBalance,
7
+ validateForeignCurrency,
8
+ validateLedgerRecords,
9
+ validateNoOrphanedAmounts,
10
+ validatePositiveAmounts,
11
+ validateRecordInternalBalance,
12
+ } from './ledger-validators';
13
+ import type { LedgerRecord, ValidationContext } from './types';
14
+
15
+ const mockContext: ValidationContext = {
16
+ useCaseId: 'test-case',
17
+ defaultCurrency: 'ILS',
18
+ tolerance: 0.005,
19
+ };
20
+
21
+ function createMockRecord(overrides: Partial<LedgerRecord> = {}): LedgerRecord {
22
+ return {
23
+ id: 'rec-1',
24
+ charge_id: 'charge-1',
25
+ owner_id: 'owner-1',
26
+ debit_entity1: null,
27
+ debit_local_amount1: null,
28
+ debit_foreign_amount1: null,
29
+ debit_entity2: null,
30
+ debit_local_amount2: null,
31
+ debit_foreign_amount2: null,
32
+ credit_entity1: null,
33
+ credit_local_amount1: null,
34
+ credit_foreign_amount1: null,
35
+ credit_entity2: null,
36
+ credit_local_amount2: null,
37
+ credit_foreign_amount2: null,
38
+ currency: 'ILS',
39
+ invoice_date: new Date('2024-01-01'),
40
+ value_date: new Date('2024-01-01'),
41
+ description: null,
42
+ reference1: null,
43
+ locked: false,
44
+ ...overrides,
45
+ };
46
+ }
47
+
48
+ describe('parseAmount', () => {
49
+ it('should parse valid number strings', () => {
50
+ expect(parseAmount('100.50')).toBe(100.5);
51
+ expect(parseAmount('0')).toBe(0);
52
+ });
53
+
54
+ it('should handle null and undefined', () => {
55
+ expect(parseAmount(null)).toBe(0);
56
+ expect(parseAmount(undefined)).toBe(0);
57
+ });
58
+
59
+ it('should handle invalid strings', () => {
60
+ expect(parseAmount('invalid')).toBe(0);
61
+ expect(parseAmount('')).toBe(0);
62
+ });
63
+ });
64
+
65
+ describe('isBalanced', () => {
66
+ it('should return true for equal values', () => {
67
+ expect(isBalanced(100, 100)).toBe(true);
68
+ });
69
+
70
+ it('should return true for values within tolerance', () => {
71
+ expect(isBalanced(100, 100.005, 0.005)).toBe(true);
72
+ });
73
+
74
+ it('should return false for values outside tolerance', () => {
75
+ expect(isBalanced(100, 102, 0.005)).toBe(false);
76
+ });
77
+ });
78
+
79
+ describe('validateRecordInternalBalance', () => {
80
+ it('should pass for balanced record', () => {
81
+ const record = createMockRecord({
82
+ debit_entity1: 'entity-1',
83
+ debit_local_amount1: '100.00',
84
+ credit_entity1: 'entity-2',
85
+ credit_local_amount1: '100.00',
86
+ });
87
+
88
+ const errors = validateRecordInternalBalance([record], mockContext);
89
+ expect(errors).toHaveLength(0);
90
+ });
91
+
92
+ it('should fail for unbalanced record', () => {
93
+ const record = createMockRecord({
94
+ debit_local_amount1: '100.00',
95
+ credit_local_amount1: '99.00',
96
+ });
97
+
98
+ const errors = validateRecordInternalBalance([record], mockContext);
99
+ expect(errors).toHaveLength(1);
100
+ expect(errors[0]).toContain('internal imbalance');
101
+ expect(errors[0]).toContain('debit=100.00');
102
+ expect(errors[0]).toContain('credit=99.00');
103
+ });
104
+
105
+ it('should detect empty records', () => {
106
+ const record = createMockRecord({
107
+ // All amounts null/zero
108
+ });
109
+
110
+ const errors = validateRecordInternalBalance([record], mockContext);
111
+ expect(errors.length).toBeGreaterThan(0);
112
+ expect(errors[0]).toContain('empty record');
113
+ });
114
+ });
115
+
116
+ describe('validateEntityBalance', () => {
117
+ it('should pass when all entities balance to zero', () => {
118
+ const records = [
119
+ createMockRecord({
120
+ debit_entity1: 'entity-1',
121
+ debit_local_amount1: '100.00',
122
+ credit_entity1: 'entity-2',
123
+ credit_local_amount1: '100.00',
124
+ }),
125
+ createMockRecord({
126
+ id: 'rec-2',
127
+ debit_entity1: 'entity-2',
128
+ debit_local_amount1: '100.00',
129
+ credit_entity1: 'entity-1',
130
+ credit_local_amount1: '100.00',
131
+ }),
132
+ ];
133
+
134
+ const errors = validateEntityBalance(records, mockContext);
135
+ expect(errors).toHaveLength(0);
136
+ });
137
+
138
+ it('should fail when entity has unbalanced position', () => {
139
+ const record = createMockRecord({
140
+ debit_entity1: 'entity-1',
141
+ debit_local_amount1: '100.00',
142
+ credit_entity1: 'entity-2',
143
+ credit_local_amount1: '100.00',
144
+ });
145
+
146
+ const errors = validateEntityBalance([record], mockContext);
147
+ expect(errors.length).toBeGreaterThan(0);
148
+ expect(errors[0]).toContain('unbalanced');
149
+ expect(errors[0]).toContain('entity-1');
150
+ });
151
+ });
152
+
153
+ describe('validateNoOrphanedAmounts', () => {
154
+ it('should fail when amount exists without entity', () => {
155
+ const record = createMockRecord({
156
+ debit_entity1: null,
157
+ debit_local_amount1: '100.00',
158
+ });
159
+
160
+ const errors = validateNoOrphanedAmounts([record], mockContext);
161
+ expect(errors.length).toBeGreaterThan(0);
162
+ expect(errors[0]).toContain('orphaned amount');
163
+ });
164
+
165
+ it('should pass when amounts have entities', () => {
166
+ const record = createMockRecord({
167
+ debit_entity1: 'entity-1',
168
+ debit_local_amount1: '100.00',
169
+ credit_entity1: 'entity-2',
170
+ credit_local_amount1: '100.00',
171
+ });
172
+
173
+ const errors = validateNoOrphanedAmounts([record], mockContext);
174
+ expect(errors).toHaveLength(0);
175
+ });
176
+ });
177
+
178
+ describe('validatePositiveAmounts', () => {
179
+ it('should fail for negative amounts', () => {
180
+ const record = createMockRecord({
181
+ debit_local_amount1: '-100.00',
182
+ });
183
+
184
+ const errors = validatePositiveAmounts([record], mockContext);
185
+ expect(errors.length).toBeGreaterThan(0);
186
+ expect(errors[0]).toContain('negative amount');
187
+ });
188
+
189
+ it('should pass for positive amounts', () => {
190
+ const record = createMockRecord({
191
+ debit_local_amount1: '100.00',
192
+ credit_local_amount1: '100.00',
193
+ });
194
+
195
+ const errors = validatePositiveAmounts([record], mockContext);
196
+ expect(errors).toHaveLength(0);
197
+ });
198
+ });
199
+
200
+ describe('validateForeignCurrency', () => {
201
+ it('should require foreign amounts for non-ILS currency', () => {
202
+ const record = createMockRecord({
203
+ currency: 'USD',
204
+ debit_local_amount1: '350.00',
205
+ debit_foreign_amount1: null,
206
+ });
207
+
208
+ const errors = validateForeignCurrency([record], mockContext);
209
+ expect(errors.length).toBeGreaterThan(0);
210
+ expect(errors[0]).toContain('no foreign amounts');
211
+ });
212
+
213
+ it('should reject foreign amounts for ILS currency', () => {
214
+ const record = createMockRecord({
215
+ currency: 'ILS',
216
+ debit_local_amount1: '100.00',
217
+ debit_foreign_amount1: '100.00',
218
+ });
219
+
220
+ const errors = validateForeignCurrency([record], mockContext);
221
+ expect(errors.length).toBeGreaterThan(0);
222
+ expect(errors[0]).toContain('has foreign amounts');
223
+ });
224
+
225
+ it('should detect suspicious exchange rates', () => {
226
+ const record = createMockRecord({
227
+ currency: 'USD',
228
+ debit_local_amount1: '1000.00',
229
+ debit_foreign_amount1: '10.00', // Rate = 100
230
+ });
231
+
232
+ const errors = validateForeignCurrency([record], mockContext);
233
+ expect(errors.length).toBeGreaterThan(0);
234
+ expect(errors[0]).toContain('suspicious exchange rate');
235
+ });
236
+ });
237
+
238
+ describe('validateDates', () => {
239
+ it('should fail for missing dates', () => {
240
+ const record = createMockRecord({
241
+ invoice_date: null as unknown as Date,
242
+ });
243
+
244
+ const errors = validateDates([record], mockContext);
245
+ expect(errors.length).toBeGreaterThan(0);
246
+ expect(errors[0]).toContain('missing invoice_date');
247
+ });
248
+
249
+ it('should fail for dates out of range', () => {
250
+ const record = createMockRecord({
251
+ invoice_date: new Date('1999-01-01'),
252
+ });
253
+
254
+ const errors = validateDates([record], mockContext);
255
+ expect(errors.length).toBeGreaterThan(0);
256
+ expect(errors[0]).toContain('out of range');
257
+ });
258
+ });
259
+
260
+ describe('validateLedgerRecords', () => {
261
+ it('should pass when all validations succeed', () => {
262
+ // Create two records that balance entities:
263
+ // Record 1: entity-1 debits 100, entity-2 credits 100
264
+ // Record 2: entity-1 credits 100, entity-2 debits 100
265
+ // This ensures each entity has equal debits and credits
266
+ const record1 = createMockRecord({
267
+ debit_entity1: 'entity-1',
268
+ debit_local_amount1: '100.00',
269
+ credit_entity1: 'entity-2',
270
+ credit_local_amount1: '100.00',
271
+ });
272
+
273
+ const record2 = createMockRecord({
274
+ debit_entity1: 'entity-2',
275
+ debit_local_amount1: '100.00',
276
+ credit_entity1: 'entity-1',
277
+ credit_local_amount1: '100.00',
278
+ });
279
+
280
+ const errors = validateLedgerRecords([record1, record2], 2, mockContext);
281
+ expect(errors).toHaveLength(0);
282
+ });
283
+
284
+ it('should collect errors from multiple validators', () => {
285
+ const record = createMockRecord({
286
+ debit_local_amount1: '100.00',
287
+ credit_local_amount1: '99.00', // Imbalance
288
+ debit_entity1: null, // Orphaned amount
289
+ invoice_date: null as unknown as Date, // Missing date
290
+ });
291
+
292
+ const errors = validateLedgerRecords([record], 1, mockContext);
293
+ expect(errors.length).toBeGreaterThan(1); // Multiple errors
294
+ expect(errors.some(e => e.includes('imbalance'))).toBe(true);
295
+ expect(errors.some(e => e.includes('orphaned'))).toBe(true);
296
+ expect(errors.some(e => e.includes('missing invoice_date'))).toBe(true);
297
+ });
298
+ });