@accounter/server 0.0.9-alpha-20251231123714-6cdd9de71b4672d74ece5d34c438d162987b2c93 → 0.0.9-alpha-20251231171312-67d2850c6eacafb65d42d987f3d49dca3c0f8bdd
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 +27 -5
- package/README.md +66 -3
- package/dist/green-invoice-graphql/src/mesh-artifacts/index.d.ts +1 -1
- package/dist/green-invoice-graphql/src/mesh-artifacts/index.js +2 -2
- package/dist/green-invoice-graphql/src/mesh-artifacts/index.js.map +1 -1
- package/dist/server/scripts/seed-admin-context.js +20 -25
- package/dist/server/scripts/seed-admin-context.js.map +1 -1
- package/dist/server/src/__tests__/db-bootstrap.test.js +7 -2
- package/dist/server/src/__tests__/db-bootstrap.test.js.map +1 -1
- package/dist/server/src/__tests__/factories/business.d.ts +1 -1
- package/dist/server/src/__tests__/factories/financial-account.d.ts +1 -1
- package/dist/server/src/__tests__/factories/index.test.js +1 -0
- package/dist/server/src/__tests__/factories/index.test.js.map +1 -1
- package/dist/server/src/__tests__/factories/tax-category.d.ts +1 -1
- package/dist/server/src/__tests__/factories/tax-category.js +1 -1
- package/dist/server/src/__tests__/factories/tax-category.js.map +1 -1
- package/dist/server/src/__tests__/factories/tax-category.test.js +8 -6
- package/dist/server/src/__tests__/factories/tax-category.test.js.map +1 -1
- package/dist/server/src/__tests__/helpers/fixture-loader.d.ts +1 -1
- package/dist/server/src/__tests__/helpers/fixture-loader.js +25 -52
- package/dist/server/src/__tests__/helpers/fixture-loader.js.map +1 -1
- package/dist/server/src/__tests__/helpers/migration-verification.d.ts +1 -1
- package/dist/server/src/__tests__/helpers/migration-verification.js.map +1 -1
- package/dist/server/src/__tests__/helpers/seed-helpers.business.test.js +4 -4
- package/dist/server/src/__tests__/helpers/seed-helpers.business.test.js.map +1 -1
- package/dist/server/src/__tests__/helpers/seed-helpers.d.ts +9 -9
- package/dist/server/src/__tests__/helpers/seed-helpers.js +57 -54
- package/dist/server/src/__tests__/helpers/seed-helpers.js.map +1 -1
- package/dist/server/src/__tests__/seed-admin-context.integration.test.js +2 -1
- package/dist/server/src/__tests__/seed-admin-context.integration.test.js.map +1 -1
- package/dist/server/src/demo-fixtures/__tests__/deterministic-uuid.test.js +3 -2
- package/dist/server/src/demo-fixtures/__tests__/deterministic-uuid.test.js.map +1 -1
- package/dist/server/src/demo-fixtures/__tests__/seed-and-validate.test.d.ts +1 -0
- package/dist/server/src/demo-fixtures/__tests__/seed-and-validate.test.js +69 -0
- package/dist/server/src/demo-fixtures/__tests__/seed-and-validate.test.js.map +1 -0
- package/dist/server/src/demo-fixtures/__tests__/use-case-registry.test.d.ts +1 -0
- package/dist/server/src/demo-fixtures/__tests__/use-case-registry.test.js +26 -0
- package/dist/server/src/demo-fixtures/__tests__/use-case-registry.test.js.map +1 -0
- package/dist/server/src/demo-fixtures/helpers/admin-context.d.ts +10 -0
- package/dist/server/src/demo-fixtures/helpers/admin-context.js +40 -0
- package/dist/server/src/demo-fixtures/helpers/admin-context.js.map +1 -0
- package/dist/server/src/demo-fixtures/helpers/placeholder.d.ts +45 -0
- package/dist/server/src/demo-fixtures/helpers/placeholder.js +50 -0
- package/dist/server/src/demo-fixtures/helpers/placeholder.js.map +1 -0
- package/dist/server/src/demo-fixtures/helpers/seed-exchange-rates.d.ts +21 -0
- package/dist/server/src/demo-fixtures/helpers/seed-exchange-rates.js +26 -0
- package/dist/server/src/demo-fixtures/helpers/seed-exchange-rates.js.map +1 -0
- package/dist/server/src/demo-fixtures/helpers/seed-vat.d.ts +20 -0
- package/dist/server/src/demo-fixtures/helpers/seed-vat.js +30 -0
- package/dist/server/src/demo-fixtures/helpers/seed-vat.js.map +1 -0
- package/dist/server/src/demo-fixtures/use-cases/equity/shareholder-dividend.d.ts +9 -0
- package/dist/server/src/demo-fixtures/use-cases/equity/shareholder-dividend.js +86 -0
- package/dist/server/src/demo-fixtures/use-cases/equity/shareholder-dividend.js.map +1 -0
- package/dist/server/src/demo-fixtures/use-cases/expenses/monthly-expense-foreign-currency.d.ts +12 -0
- package/dist/server/src/demo-fixtures/use-cases/expenses/monthly-expense-foreign-currency.js +375 -0
- package/dist/server/src/demo-fixtures/use-cases/expenses/monthly-expense-foreign-currency.js.map +1 -0
- package/dist/server/src/demo-fixtures/use-cases/income/client-payment-with-refund.d.ts +10 -0
- package/dist/server/src/demo-fixtures/use-cases/income/client-payment-with-refund.js +113 -0
- package/dist/server/src/demo-fixtures/use-cases/income/client-payment-with-refund.js.map +1 -0
- package/dist/server/src/demo-fixtures/use-cases/index.d.ts +41 -0
- package/dist/server/src/demo-fixtures/use-cases/index.js +50 -0
- package/dist/server/src/demo-fixtures/use-cases/index.js.map +1 -0
- package/dist/server/src/demo-fixtures/validate-demo-data.d.ts +1 -0
- package/dist/server/src/demo-fixtures/validate-demo-data.js +117 -0
- package/dist/server/src/demo-fixtures/validate-demo-data.js.map +1 -0
- package/dist/server/src/demo-fixtures/validators/ledger-validators.d.ts +349 -0
- package/dist/server/src/demo-fixtures/validators/ledger-validators.js +602 -0
- package/dist/server/src/demo-fixtures/validators/ledger-validators.js.map +1 -0
- package/dist/server/src/demo-fixtures/validators/ledger-validators.test.d.ts +1 -0
- package/dist/server/src/demo-fixtures/validators/ledger-validators.test.js +247 -0
- package/dist/server/src/demo-fixtures/validators/ledger-validators.test.js.map +1 -0
- package/dist/server/src/demo-fixtures/validators/types.d.ts +69 -0
- package/dist/server/src/demo-fixtures/validators/types.js +8 -0
- package/dist/server/src/demo-fixtures/validators/types.js.map +1 -0
- package/dist/server/src/fixtures/fixture-spec.d.ts +146 -0
- package/dist/server/src/fixtures/fixture-spec.js +2 -0
- package/dist/server/src/fixtures/fixture-spec.js.map +1 -0
- package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js +4 -0
- package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js.map +1 -1
- package/dist/server/src/modules/ledger/__tests__/ledger-scenario-a.integration.test.js +4 -3
- package/dist/server/src/modules/ledger/__tests__/ledger-scenario-a.integration.test.js.map +1 -1
- package/dist/server/src/modules/ledger/__tests__/ledger-scenario-b.integration.test.js +5 -3
- package/dist/server/src/modules/ledger/__tests__/ledger-scenario-b.integration.test.js.map +1 -1
- package/dist/server/src/shared/constants.d.ts +1 -0
- package/dist/server/src/shared/constants.js +1 -0
- package/dist/server/src/shared/constants.js.map +1 -1
- package/dist/server/src/shared/helpers/misc.js +2 -2
- package/dist/server/src/shared/helpers/misc.js.map +1 -1
- package/docs/demo-staging-guide.md +611 -0
- package/package.json +5 -2
- package/scripts/seed-admin-context.ts +22 -33
- package/src/__tests__/db-bootstrap.test.ts +9 -2
- package/src/__tests__/factories/business.ts +1 -1
- package/src/__tests__/factories/financial-account.ts +1 -1
- package/src/__tests__/factories/index.test.ts +1 -0
- package/src/__tests__/factories/tax-category.test.ts +8 -6
- package/src/__tests__/factories/tax-category.ts +2 -2
- package/src/__tests__/helpers/fixture-loader.ts +26 -61
- package/src/__tests__/helpers/migration-verification.ts +2 -2
- package/src/__tests__/helpers/seed-helpers.business.test.ts +4 -4
- package/src/__tests__/helpers/seed-helpers.ts +66 -75
- package/src/__tests__/seed-admin-context.integration.test.ts +2 -1
- package/src/demo-fixtures/__tests__/deterministic-uuid.test.ts +3 -2
- package/src/demo-fixtures/__tests__/seed-and-validate.test.ts +96 -0
- package/src/demo-fixtures/__tests__/use-case-registry.test.ts +27 -0
- package/src/demo-fixtures/helpers/admin-context.ts +59 -0
- package/src/demo-fixtures/helpers/placeholder.ts +50 -0
- package/src/demo-fixtures/helpers/seed-exchange-rates.ts +29 -0
- package/src/demo-fixtures/helpers/seed-vat.ts +35 -0
- package/src/demo-fixtures/use-cases/equity/shareholder-dividend.ts +88 -0
- package/src/demo-fixtures/use-cases/expenses/monthly-expense-foreign-currency.ts +377 -0
- package/src/demo-fixtures/use-cases/income/client-payment-with-refund.ts +115 -0
- package/src/demo-fixtures/use-cases/index.ts +52 -0
- package/src/demo-fixtures/validate-demo-data.ts +153 -0
- package/src/demo-fixtures/validators/README.md +190 -0
- package/src/demo-fixtures/validators/ledger-validators.test.ts +298 -0
- package/src/demo-fixtures/validators/ledger-validators.ts +711 -0
- package/src/demo-fixtures/validators/types.ts +83 -0
- package/src/fixtures/fixture-spec.ts +158 -0
- package/src/modules/charges-matcher/__tests__/single-match-integration.test.ts +6 -0
- package/src/modules/ledger/__tests__/ledger-scenario-a.integration.test.ts +4 -3
- package/src/modules/ledger/__tests__/ledger-scenario-b.integration.test.ts +6 -3
- package/src/shared/constants.ts +2 -0
- 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
|
+
});
|