@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.
- package/CHANGELOG.md +27 -5
- package/README.md +66 -3
- 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/deel/resolvers/deel.resolvers.js +0 -3
- package/dist/server/src/modules/deel/resolvers/deel.resolvers.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/deel/resolvers/deel.resolvers.ts +0 -3
- 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
|
@@ -44,37 +44,22 @@ export async function seedAdminCore(client: PoolClient): Promise<{ adminEntityId
|
|
|
44
44
|
console.log('Creating admin business entity...');
|
|
45
45
|
|
|
46
46
|
// First check if admin entity already exists (by name and type, ignoring owner_id for admin)
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
});
|
|
64
|
-
adminEntityId = id;
|
|
65
|
-
console.log(`✅ Admin entity: ${adminEntityId}`);
|
|
66
|
-
|
|
67
|
-
// Create corresponding business record
|
|
68
|
-
await ensureBusinessForEntity(client, adminEntityId);
|
|
69
|
-
console.log('✅ Admin business record created');
|
|
70
|
-
|
|
71
|
-
// Update owner_id to self
|
|
72
|
-
await client.query(
|
|
73
|
-
`UPDATE accounter_schema.financial_entities SET owner_id = $1 WHERE id = $1`,
|
|
74
|
-
[adminEntityId],
|
|
75
|
-
);
|
|
76
|
-
console.log('✅ Admin entity owner_id set to self');
|
|
77
|
-
}
|
|
47
|
+
const { id } = await ensureFinancialEntity(client, {
|
|
48
|
+
name: 'Admin Business',
|
|
49
|
+
type: 'business',
|
|
50
|
+
});
|
|
51
|
+
const adminEntityId = id;
|
|
52
|
+
console.log(`✅ Admin entity: ${adminEntityId}`);
|
|
53
|
+
|
|
54
|
+
// Create corresponding business record
|
|
55
|
+
await ensureBusinessForEntity(client, adminEntityId);
|
|
56
|
+
console.log('✅ Admin business record created');
|
|
57
|
+
|
|
58
|
+
// Update owner_id to self
|
|
59
|
+
await client.query(`UPDATE accounter_schema.financial_entities SET owner_id = $1 WHERE id = $1`, [
|
|
60
|
+
adminEntityId,
|
|
61
|
+
]);
|
|
62
|
+
console.log('✅ Admin entity owner_id set to self');
|
|
78
63
|
|
|
79
64
|
// 2. Create authority businesses
|
|
80
65
|
const authorities = {
|
|
@@ -88,9 +73,10 @@ export async function seedAdminCore(client: PoolClient): Promise<{ adminEntityId
|
|
|
88
73
|
const { id } = await ensureFinancialEntity(client, {
|
|
89
74
|
name,
|
|
90
75
|
type: 'business',
|
|
76
|
+
ownerId: adminEntityId,
|
|
91
77
|
});
|
|
92
78
|
authorityBusinessIds[name] = id;
|
|
93
|
-
await ensureBusinessForEntity(client, id, {
|
|
79
|
+
await ensureBusinessForEntity(client, id, { isDocumentsOptional: true });
|
|
94
80
|
}
|
|
95
81
|
console.log(`✅ Created ${authorities.businesses.length} authority businesses`);
|
|
96
82
|
|
|
@@ -101,6 +87,7 @@ export async function seedAdminCore(client: PoolClient): Promise<{ adminEntityId
|
|
|
101
87
|
const { id } = await ensureFinancialEntity(client, {
|
|
102
88
|
name,
|
|
103
89
|
type: 'tax_category',
|
|
90
|
+
ownerId: adminEntityId,
|
|
104
91
|
});
|
|
105
92
|
authorityTaxCategoryIds[name] = id;
|
|
106
93
|
await ensureTaxCategoryForEntity(client, id);
|
|
@@ -129,6 +116,7 @@ export async function seedAdminCore(client: PoolClient): Promise<{ adminEntityId
|
|
|
129
116
|
const { id } = await ensureFinancialEntity(client, {
|
|
130
117
|
name,
|
|
131
118
|
type: 'tax_category',
|
|
119
|
+
ownerId: adminEntityId,
|
|
132
120
|
});
|
|
133
121
|
generalTaxCategoryIds[name] = id;
|
|
134
122
|
await ensureTaxCategoryForEntity(client, id);
|
|
@@ -149,6 +137,7 @@ export async function seedAdminCore(client: PoolClient): Promise<{ adminEntityId
|
|
|
149
137
|
const { id } = await ensureFinancialEntity(client, {
|
|
150
138
|
name,
|
|
151
139
|
type: 'tax_category',
|
|
140
|
+
ownerId: adminEntityId,
|
|
152
141
|
});
|
|
153
142
|
crossYearTaxCategoryIds[name] = id;
|
|
154
143
|
await ensureTaxCategoryForEntity(client, id);
|
|
@@ -199,7 +188,7 @@ export async function seedAdminCore(client: PoolClient): Promise<{ adminEntityId
|
|
|
199
188
|
const values = Object.values(context);
|
|
200
189
|
|
|
201
190
|
await client.query(
|
|
202
|
-
`INSERT INTO accounter_schema.user_context (${columns}) VALUES (${placeholders})`,
|
|
191
|
+
`INSERT INTO accounter_schema.user_context (${columns}) VALUES (${placeholders}) ON CONFLICT (owner_id) DO NOTHING`,
|
|
203
192
|
values,
|
|
204
193
|
);
|
|
205
194
|
console.log('✅ user_context created');
|
|
@@ -3,6 +3,7 @@ import { TestDatabase, isPoolHealthy } from './helpers/db-setup.js';
|
|
|
3
3
|
import { assertLatestMigrationApplied } from './helpers/migration-verification.js';
|
|
4
4
|
import { seedAdminCore } from '../../scripts/seed-admin-context.js';
|
|
5
5
|
import { qualifyTable } from './helpers/test-db-config.js';
|
|
6
|
+
import { buildAdminContextFromDb } from './helpers/admin-context-builder.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Smoke test for DB test harness
|
|
@@ -67,8 +68,12 @@ describe('DB Test Harness Bootstrap', () => {
|
|
|
67
68
|
it('has expected tax categories', async () =>
|
|
68
69
|
db.withTransaction(async client => {
|
|
69
70
|
await seedAdminCore(client);
|
|
71
|
+
const adminContext = await buildAdminContextFromDb(client);
|
|
70
72
|
const result = await client.query(
|
|
71
|
-
`SELECT COUNT(
|
|
73
|
+
`SELECT COUNT(tc.*) FROM ${qualifyTable('tax_categories')} tc
|
|
74
|
+
LEFT JOIN ${qualifyTable('financial_entities')} fe USING (id)
|
|
75
|
+
WHERE owner_id = $1`,
|
|
76
|
+
[adminContext.defaultAdminBusinessId]
|
|
72
77
|
);
|
|
73
78
|
const count = parseInt(result.rows[0].count, 10);
|
|
74
79
|
expect(count).toBe(EXPECTED_TAX_CATEGORIES);
|
|
@@ -77,8 +82,10 @@ describe('DB Test Harness Bootstrap', () => {
|
|
|
77
82
|
it('has user_context after seeding', async () =>
|
|
78
83
|
db.withTransaction(async client => {
|
|
79
84
|
await seedAdminCore(client);
|
|
85
|
+
const adminContext = await buildAdminContextFromDb(client);
|
|
80
86
|
const result = await client.query(
|
|
81
|
-
`SELECT owner_id, vat_business_id FROM ${qualifyTable('user_context')} LIMIT 1`,
|
|
87
|
+
`SELECT owner_id, vat_business_id FROM ${qualifyTable('user_context')} WHERE owner_id = $1 LIMIT 1`,
|
|
88
|
+
[adminContext.defaultAdminBusinessId]
|
|
82
89
|
);
|
|
83
90
|
expect(result.rows).toHaveLength(1);
|
|
84
91
|
expect(result.rows[0].owner_id).toBeDefined();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { makeUUIDLegacy } from '../../demo-fixtures/helpers/deterministic-uuid.js';
|
|
2
2
|
import { CountryCode } from '../../shared/enums.js';
|
|
3
|
-
import { FixtureBusinesses } from '
|
|
3
|
+
import { FixtureBusinesses } from '../helpers/fixture-types.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Business factory for test fixtures
|
|
@@ -2,7 +2,7 @@ import type {
|
|
|
2
2
|
financial_account_type,
|
|
3
3
|
} from '../../modules/financial-accounts/__generated__/financial-accounts.types.js';
|
|
4
4
|
import { makeUUIDLegacy } from '../../demo-fixtures/helpers/deterministic-uuid.js';
|
|
5
|
-
import { FixtureAccounts } from '
|
|
5
|
+
import { FixtureAccounts } from '../helpers/fixture-types.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Valid financial account types
|
|
@@ -45,6 +45,7 @@ describe('Factory Integration', () => {
|
|
|
45
45
|
const taxCategoryId = makeUUID('tax-category', 'tax-expense');
|
|
46
46
|
const taxCategory = createTaxCategory({
|
|
47
47
|
id: taxCategoryId,
|
|
48
|
+
name: 'Expense Tax',
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
const accountId = makeUUID('financial-account', 'bank-account');
|
|
@@ -6,7 +6,7 @@ import { createTaxCategory } from './tax-category.js';
|
|
|
6
6
|
describe('Factory: Tax Category', () => {
|
|
7
7
|
describe('createTaxCategory', () => {
|
|
8
8
|
it('should create tax category with default values', () => {
|
|
9
|
-
const category = createTaxCategory();
|
|
9
|
+
const category = createTaxCategory({name: 'Default Category'});
|
|
10
10
|
|
|
11
11
|
// Required field with default
|
|
12
12
|
expect(category.id).toBeDefined();
|
|
@@ -20,8 +20,8 @@ describe('Factory: Tax Category', () => {
|
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it('should generate unique IDs by default', () => {
|
|
23
|
-
const category1 = createTaxCategory();
|
|
24
|
-
const category2 = createTaxCategory();
|
|
23
|
+
const category1 = createTaxCategory({name: 'Category 1'});
|
|
24
|
+
const category2 = createTaxCategory({name: 'Category 2'});
|
|
25
25
|
|
|
26
26
|
expect(category1.id).not.toBe(category2.id);
|
|
27
27
|
});
|
|
@@ -50,7 +50,7 @@ describe('Factory: Tax Category', () => {
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
it('should preserve all required fields', () => {
|
|
53
|
-
const category = createTaxCategory();
|
|
53
|
+
const category = createTaxCategory({name: 'Default Category'});
|
|
54
54
|
|
|
55
55
|
// Verify structure matches expected pgtyped interface
|
|
56
56
|
expect(category).toHaveProperty('id');
|
|
@@ -61,6 +61,7 @@ describe('Factory: Tax Category', () => {
|
|
|
61
61
|
it('should handle tax-excluded categories', () => {
|
|
62
62
|
const category = createTaxCategory({
|
|
63
63
|
taxExcluded: true,
|
|
64
|
+
name: 'Tax Exempt',
|
|
64
65
|
});
|
|
65
66
|
|
|
66
67
|
expect(category.taxExcluded).toBe(true);
|
|
@@ -78,14 +79,15 @@ describe('Factory: Tax Category', () => {
|
|
|
78
79
|
it('should allow explicit null overrides', () => {
|
|
79
80
|
const category = createTaxCategory({
|
|
80
81
|
hashavshevetName: null,
|
|
82
|
+
name: 'No Integration',
|
|
81
83
|
});
|
|
82
84
|
|
|
83
85
|
expect(category.hashavshevetName).toBeNull();
|
|
84
86
|
});
|
|
85
87
|
|
|
86
88
|
it('should create deterministic categories with seed', () => {
|
|
87
|
-
const category1 = createTaxCategory({ id: makeUUID('tax-category', 'default-category') });
|
|
88
|
-
const category2 = createTaxCategory({ id: makeUUID('tax-category', 'default-category') });
|
|
89
|
+
const category1 = createTaxCategory({ id: makeUUID('tax-category', 'default-category'), name: 'Default Category' });
|
|
90
|
+
const category2 = createTaxCategory({ id: makeUUID('tax-category', 'default-category'), name: 'Default Category' });
|
|
89
91
|
|
|
90
92
|
expect(category1.id).toBe(category2.id);
|
|
91
93
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { makeUUIDLegacy } from '../../demo-fixtures/helpers/deterministic-uuid.js';
|
|
2
|
-
import { FixtureTaxCategories } from '
|
|
2
|
+
import { FixtureTaxCategories } from '../helpers/fixture-types.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Tax category factory for test fixtures
|
|
@@ -41,7 +41,7 @@ export function createTaxCategory(
|
|
|
41
41
|
id: defaultId,
|
|
42
42
|
// Intelligent name defaulting: use provided id, or use generated UUID
|
|
43
43
|
// This ensures display name is always meaningful even when only id is specified
|
|
44
|
-
name: overrides?.id ?? defaultId,
|
|
44
|
+
name: overrides?.name ?? overrides?.id ?? defaultId,
|
|
45
45
|
hashavshevetName: null,
|
|
46
46
|
taxExcluded: false,
|
|
47
47
|
...overrides,
|
|
@@ -13,6 +13,8 @@ import type { Fixture } from './fixture-types.js';
|
|
|
13
13
|
import { assertValidFixture } from './fixture-validation.js';
|
|
14
14
|
import { qualifyTable } from './test-db-config.js';
|
|
15
15
|
import { makeUUID, makeUUIDLegacy } from '../../demo-fixtures/helpers/deterministic-uuid.js';
|
|
16
|
+
import { UUID_REGEX } from '../../shared/constants.js';
|
|
17
|
+
import { ensureBusinessForEntity, ensureFinancialEntity, ensureTaxCategoryForEntity } from './seed-helpers.js';
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Custom error for fixture insertion failures
|
|
@@ -118,6 +120,7 @@ export type FixtureIdMapping = Map<string, string>;
|
|
|
118
120
|
export async function insertFixture(
|
|
119
121
|
client: PoolClient | Client,
|
|
120
122
|
fixture: Fixture,
|
|
123
|
+
adminBusinessId?: string,
|
|
121
124
|
): Promise<FixtureIdMapping> {
|
|
122
125
|
// Validate fixture before insertion
|
|
123
126
|
assertValidFixture(fixture);
|
|
@@ -153,49 +156,19 @@ export async function insertFixture(
|
|
|
153
156
|
for (const business of fixture.businesses!.businesses) {
|
|
154
157
|
// Insert financial entity first (type='business')
|
|
155
158
|
// Field mapping: business.name used for display (required); hebrewName is legacy/optional
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
const {id: entityId} = await ensureFinancialEntity(client, {
|
|
160
|
+
id: business.id,
|
|
161
|
+
name: business.name || business.id!,
|
|
162
|
+
type: 'business',
|
|
163
|
+
ownerId: adminBusinessId,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const {id: businessId, ...options } = business;
|
|
163
167
|
|
|
164
168
|
// Insert business details - note: vat_number column maps to governmentId field
|
|
165
|
-
await client
|
|
166
|
-
`INSERT INTO ${qualifyTable('businesses')} (
|
|
167
|
-
id, hebrew_name, address, city, zip_code, email, website, phone_number, vat_number,
|
|
168
|
-
exempt_dealer, suggestion_data, optional_vat, country,
|
|
169
|
-
pcn874_record_type_override, can_settle_with_receipt, no_invoices_required
|
|
170
|
-
)
|
|
171
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
|
172
|
-
ON CONFLICT (id) DO NOTHING`,
|
|
173
|
-
[
|
|
174
|
-
business.id,
|
|
175
|
-
business.hebrewName,
|
|
176
|
-
business.address,
|
|
177
|
-
business.city,
|
|
178
|
-
business.zipCode,
|
|
179
|
-
business.email,
|
|
180
|
-
business.website,
|
|
181
|
-
business.phoneNumber,
|
|
182
|
-
business.governmentId, // Maps to vat_number column
|
|
183
|
-
business.exemptDealer ?? false,
|
|
184
|
-
business.suggestions ?? null,
|
|
185
|
-
business.optionalVat ?? false,
|
|
186
|
-
business.country ?? 'ISR',
|
|
187
|
-
business.pcn874RecordTypeOverride ?? null,
|
|
188
|
-
business.isReceiptEnough ?? false,
|
|
189
|
-
business.isDocumentsOptional ?? false,
|
|
190
|
-
],
|
|
191
|
-
);
|
|
169
|
+
await ensureBusinessForEntity(client, entityId, options)
|
|
192
170
|
|
|
193
|
-
|
|
194
|
-
idMapping.set(business.id!, entityResult.rows[0].id);
|
|
195
|
-
} else {
|
|
196
|
-
// Entity already existed, map to itself
|
|
197
|
-
idMapping.set(business.id!, business.id!);
|
|
198
|
-
}
|
|
171
|
+
idMapping.set(business.id!, entityId);
|
|
199
172
|
}
|
|
200
173
|
});
|
|
201
174
|
}
|
|
@@ -205,29 +178,21 @@ export async function insertFixture(
|
|
|
205
178
|
await executeSavepointSection('tax_categories', async () => {
|
|
206
179
|
for (const taxCategory of fixture.taxCategories!.taxCategories) {
|
|
207
180
|
// Insert financial entity first
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
);
|
|
181
|
+
const {id: entityId} = await ensureFinancialEntity(client, {
|
|
182
|
+
id: taxCategory.id,
|
|
183
|
+
name: taxCategory.name || taxCategory.id!,
|
|
184
|
+
type: 'tax_category',
|
|
185
|
+
ownerId: adminBusinessId,
|
|
186
|
+
});
|
|
215
187
|
|
|
216
188
|
// Insert tax category details
|
|
217
|
-
await client
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
ON CONFLICT (id) DO NOTHING`,
|
|
223
|
-
[taxCategory.id, taxCategory.hashavshevetName, taxCategory.taxExcluded ?? false],
|
|
224
|
-
);
|
|
189
|
+
await ensureTaxCategoryForEntity(client, entityId, {
|
|
190
|
+
name: taxCategory.name,
|
|
191
|
+
hashavshevetName: taxCategory.hashavshevetName,
|
|
192
|
+
taxExcluded: taxCategory.taxExcluded,
|
|
193
|
+
});
|
|
225
194
|
|
|
226
|
-
|
|
227
|
-
idMapping.set(taxCategory.id!, entityResult.rows[0].id);
|
|
228
|
-
} else {
|
|
229
|
-
idMapping.set(taxCategory.id!, taxCategory.id!);
|
|
230
|
-
}
|
|
195
|
+
idMapping.set(taxCategory.id!, entityId);
|
|
231
196
|
}
|
|
232
197
|
});
|
|
233
198
|
}
|
|
@@ -328,7 +293,7 @@ export async function insertFixture(
|
|
|
328
293
|
for (const transaction of fixture.transactions!.transactions) {
|
|
329
294
|
// If account_id looks like an account_number (not a UUID), look up the actual UUID
|
|
330
295
|
let accountId = transaction.account_id;
|
|
331
|
-
if (accountId && !accountId.match(
|
|
296
|
+
if (accountId && !accountId.match(UUID_REGEX)) {
|
|
332
297
|
// account_id is actually an account_number, look up the UUID
|
|
333
298
|
const accountResult = await client.query(
|
|
334
299
|
`SELECT id FROM ${qualifyTable('financial_accounts')} WHERE account_number = $1`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Client, Pool, PoolClient } from 'pg';
|
|
1
|
+
import { type Client, Pool, type PoolClient } from 'pg';
|
|
2
2
|
import { LATEST_MIGRATION_NAME } from '../../../../migrations/src/run-pg-migrations.js';
|
|
3
3
|
|
|
4
4
|
export { LATEST_MIGRATION_NAME };
|
|
@@ -71,4 +71,4 @@ export async function assertLatestMigrationApplied(
|
|
|
71
71
|
`Migration check failed for ${result.latestMigrationName}. Run: yarn workspace @accounter/migrations migration:run`,
|
|
72
72
|
);
|
|
73
73
|
}
|
|
74
|
-
}
|
|
74
|
+
}
|
|
@@ -70,7 +70,7 @@ describe('ensureBusinessForEntity', () => {
|
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
// Ensure business with noInvoicesRequired set to true
|
|
73
|
-
await ensureBusinessForEntity(client, entityId, {
|
|
73
|
+
await ensureBusinessForEntity(client, entityId, { isDocumentsOptional: true });
|
|
74
74
|
|
|
75
75
|
// Verify business has correct option set
|
|
76
76
|
const result = await client.query(
|
|
@@ -112,11 +112,11 @@ describe('ensureBusinessForEntity', () => {
|
|
|
112
112
|
type: 'business',
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
// Create business with
|
|
116
|
-
await ensureBusinessForEntity(client, entityId, {
|
|
115
|
+
// Create business with isDocumentsOptional = true
|
|
116
|
+
await ensureBusinessForEntity(client, entityId, { isDocumentsOptional: true });
|
|
117
117
|
|
|
118
118
|
// Call again with different options (should be no-op)
|
|
119
|
-
await ensureBusinessForEntity(client, entityId, {
|
|
119
|
+
await ensureBusinessForEntity(client, entityId, { isDocumentsOptional: false });
|
|
120
120
|
|
|
121
121
|
// Verify original value is preserved
|
|
122
122
|
const result = await client.query(
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import type { PoolClient } from 'pg';
|
|
1
|
+
import type { Client, PoolClient } from 'pg';
|
|
2
2
|
import { qualifyTable } from './test-db-config.js';
|
|
3
3
|
import { EntityValidationError, SeedError } from './seed-errors.js';
|
|
4
|
+
import { makeUUID } from '../factories/index.js';
|
|
5
|
+
import { UUID_REGEX } from '../../shared/constants.js';
|
|
6
|
+
import type {FixtureBusinesses, FixtureTaxCategories} from './fixture-types.js';
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Valid financial entity types based on database schema
|
|
@@ -13,6 +16,7 @@ export type FinancialEntityType = 'business' | 'tax_category' | 'tag';
|
|
|
13
16
|
const VALID_ENTITY_TYPES: readonly FinancialEntityType[] = ['business', 'tax_category', 'tag'];
|
|
14
17
|
|
|
15
18
|
export interface EnsureFinancialEntityParams {
|
|
19
|
+
id?: string;
|
|
16
20
|
name: string;
|
|
17
21
|
type: FinancialEntityType;
|
|
18
22
|
ownerId?: string;
|
|
@@ -68,10 +72,10 @@ export interface FinancialEntityResult {
|
|
|
68
72
|
* ```
|
|
69
73
|
*/
|
|
70
74
|
export async function ensureFinancialEntity(
|
|
71
|
-
client: PoolClient,
|
|
75
|
+
client: PoolClient | Client,
|
|
72
76
|
params: EnsureFinancialEntityParams,
|
|
73
77
|
): Promise<FinancialEntityResult> {
|
|
74
|
-
const { name, type, ownerId } = params;
|
|
78
|
+
const { name, type, ownerId, id: originId } = params;
|
|
75
79
|
|
|
76
80
|
// Validate inputs
|
|
77
81
|
const validationErrors: string[] = [];
|
|
@@ -91,41 +95,32 @@ export async function ensureFinancialEntity(
|
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
try {
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const existingResult = await client.query<{ id: string }>(
|
|
105
|
-
selectQuery,
|
|
106
|
-
[name, type, ownerId ?? null],
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
if (existingResult.rows.length > 0) {
|
|
110
|
-
return { id: existingResult.rows[0].id };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Insert new entity
|
|
98
|
+
// Generate deterministic UUID based on type, name, and ownerId for idempotency
|
|
99
|
+
// Same (type, name, ownerId) always generates same ID, ensuring multiple calls are safe
|
|
100
|
+
// Include ownerId in the composite key so different owners get different IDs
|
|
101
|
+
const compositeKey = ownerId ? `${name}:owner=${ownerId}` : name;
|
|
102
|
+
const consistentId = originId ?? makeUUID(type, compositeKey);
|
|
103
|
+
|
|
104
|
+
// Use atomic INSERT...ON CONFLICT on PRIMARY KEY (id) to handle concurrent inserts
|
|
105
|
+
// If the deterministic ID already exists (from a previous call or concurrent insert),
|
|
106
|
+
// the conflict handler will safely return the existing row
|
|
114
107
|
const insertQuery = `
|
|
115
|
-
INSERT INTO ${qualifyTable('financial_entities')} (name, type, owner_id)
|
|
116
|
-
VALUES ($1, $2, $3)
|
|
108
|
+
INSERT INTO ${qualifyTable('financial_entities')} (id, name, type, owner_id)
|
|
109
|
+
VALUES ($1, $2, $3, $4)
|
|
110
|
+
ON CONFLICT (id) DO UPDATE
|
|
111
|
+
SET id = EXCLUDED.id -- No-op update, just to return the existing id
|
|
117
112
|
RETURNING id
|
|
118
113
|
`;
|
|
119
114
|
|
|
120
|
-
const
|
|
115
|
+
const result = await client.query<{ id: string }>(
|
|
121
116
|
insertQuery,
|
|
122
|
-
[name, type, ownerId ?? null],
|
|
117
|
+
[consistentId, name, type, ownerId ?? null],
|
|
123
118
|
);
|
|
124
119
|
|
|
125
|
-
const row =
|
|
120
|
+
const row = result.rows[0];
|
|
126
121
|
if (!row) {
|
|
127
122
|
throw new SeedError(
|
|
128
|
-
'INSERT returned no rows',
|
|
123
|
+
'INSERT...ON CONFLICT returned no rows',
|
|
129
124
|
{ name, type, ownerId },
|
|
130
125
|
);
|
|
131
126
|
}
|
|
@@ -144,9 +139,7 @@ export async function ensureFinancialEntity(
|
|
|
144
139
|
}
|
|
145
140
|
}
|
|
146
141
|
|
|
147
|
-
export
|
|
148
|
-
noInvoicesRequired?: boolean;
|
|
149
|
-
}
|
|
142
|
+
export type EnsureBusinessForEntityOptions = Partial<Omit<FixtureBusinesses['businesses'][number], 'id'>>
|
|
150
143
|
|
|
151
144
|
/**
|
|
152
145
|
* Ensure a business row exists for a given financial entity id (idempotent)
|
|
@@ -190,13 +183,12 @@ export interface EnsureBusinessForEntityOptions {
|
|
|
190
183
|
* ```
|
|
191
184
|
*/
|
|
192
185
|
export async function ensureBusinessForEntity(
|
|
193
|
-
client: PoolClient,
|
|
186
|
+
client: PoolClient | Client,
|
|
194
187
|
entityId: string,
|
|
195
188
|
options?: EnsureBusinessForEntityOptions,
|
|
196
189
|
): Promise<void> {
|
|
197
190
|
// Validate entityId format (basic UUID check)
|
|
198
|
-
|
|
199
|
-
if (!uuidPattern.test(entityId)) {
|
|
191
|
+
if (!UUID_REGEX.test(entityId)) {
|
|
200
192
|
throw new EntityValidationError(
|
|
201
193
|
'Business',
|
|
202
194
|
['entityId must be a valid UUID'],
|
|
@@ -223,27 +215,36 @@ export async function ensureBusinessForEntity(
|
|
|
223
215
|
);
|
|
224
216
|
}
|
|
225
217
|
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
FROM ${qualifyTable('businesses')}
|
|
230
|
-
WHERE id = $1
|
|
231
|
-
LIMIT 1
|
|
232
|
-
`;
|
|
233
|
-
|
|
234
|
-
const existingResult = await client.query(selectQuery, [entityId]);
|
|
235
|
-
|
|
236
|
-
if (existingResult.rows.length > 0) {
|
|
237
|
-
return; // Business already exists, preserve existing values
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Insert new business
|
|
218
|
+
// Use atomic INSERT...ON CONFLICT on PRIMARY KEY (id) to handle concurrent inserts
|
|
219
|
+
// If the deterministic ID already exists (from a previous call or concurrent insert),
|
|
220
|
+
// the conflict handler will safely return the existing row
|
|
241
221
|
const insertQuery = `
|
|
242
|
-
INSERT INTO ${qualifyTable('businesses')} (
|
|
243
|
-
|
|
222
|
+
INSERT INTO ${qualifyTable('businesses')} (
|
|
223
|
+
id, hebrew_name, address, city, zip_code, email, website, phone_number, vat_number,
|
|
224
|
+
exempt_dealer, suggestion_data, optional_vat, country,
|
|
225
|
+
pcn874_record_type_override, can_settle_with_receipt, no_invoices_required
|
|
226
|
+
)
|
|
227
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
|
228
|
+
ON CONFLICT (id) DO NOTHING
|
|
244
229
|
`;
|
|
245
230
|
|
|
246
|
-
await client.query(insertQuery, [
|
|
231
|
+
await client.query(insertQuery, [
|
|
232
|
+
entityId,
|
|
233
|
+
options?.hebrewName,
|
|
234
|
+
options?.address,
|
|
235
|
+
options?.city,
|
|
236
|
+
options?.zipCode,
|
|
237
|
+
options?.email,
|
|
238
|
+
options?.website,
|
|
239
|
+
options?.phoneNumber,
|
|
240
|
+
options?.governmentId, // Maps to vat_number column
|
|
241
|
+
options?.exemptDealer ?? false,
|
|
242
|
+
options?.suggestions ?? null,
|
|
243
|
+
options?.optionalVat ?? false,
|
|
244
|
+
options?.country ?? 'ISR',
|
|
245
|
+
options?.pcn874RecordTypeOverride ?? null,
|
|
246
|
+
options?.isReceiptEnough ?? false,
|
|
247
|
+
options?.isDocumentsOptional ?? false]);
|
|
247
248
|
} catch (error) {
|
|
248
249
|
if (error instanceof EntityValidationError || error instanceof SeedError) {
|
|
249
250
|
throw error;
|
|
@@ -257,7 +258,7 @@ export async function ensureBusinessForEntity(
|
|
|
257
258
|
}
|
|
258
259
|
}
|
|
259
260
|
|
|
260
|
-
export
|
|
261
|
+
export type EnsureTaxCategoryForEntityOptions = Partial<Omit<FixtureTaxCategories['taxCategories'][number], 'id'>> & {
|
|
261
262
|
sortCode?: number;
|
|
262
263
|
}
|
|
263
264
|
|
|
@@ -307,13 +308,12 @@ export interface EnsureTaxCategoryForEntityOptions {
|
|
|
307
308
|
* ```
|
|
308
309
|
*/
|
|
309
310
|
export async function ensureTaxCategoryForEntity(
|
|
310
|
-
client: PoolClient,
|
|
311
|
+
client: PoolClient | Client,
|
|
311
312
|
entityId: string,
|
|
312
313
|
options?: EnsureTaxCategoryForEntityOptions,
|
|
313
314
|
): Promise<void> {
|
|
314
315
|
// Validate entityId format (basic UUID check)
|
|
315
|
-
|
|
316
|
-
if (!uuidPattern.test(entityId)) {
|
|
316
|
+
if (!UUID_REGEX.test(entityId)) {
|
|
317
317
|
throw new EntityValidationError(
|
|
318
318
|
'TaxCategory',
|
|
319
319
|
['entityId must be a valid UUID'],
|
|
@@ -340,27 +340,18 @@ export async function ensureTaxCategoryForEntity(
|
|
|
340
340
|
);
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
FROM ${qualifyTable('tax_categories')}
|
|
347
|
-
WHERE id = $1
|
|
348
|
-
LIMIT 1
|
|
349
|
-
`;
|
|
350
|
-
|
|
351
|
-
const existingResult = await client.query(selectQuery, [entityId]);
|
|
352
|
-
|
|
353
|
-
if (existingResult.rows.length > 0) {
|
|
354
|
-
return; // Tax category already exists, preserve existing values
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Insert new tax category
|
|
343
|
+
// Use atomic INSERT...ON CONFLICT on PRIMARY KEY (id) to handle concurrent inserts
|
|
344
|
+
// If the deterministic ID already exists (from a previous call or concurrent insert),
|
|
345
|
+
// the conflict handler will safely return the existing row
|
|
358
346
|
const insertQuery = `
|
|
359
|
-
INSERT INTO ${qualifyTable('tax_categories')} (
|
|
360
|
-
|
|
347
|
+
INSERT INTO ${qualifyTable('tax_categories')} (
|
|
348
|
+
id, hashavshevet_name, tax_excluded
|
|
349
|
+
)
|
|
350
|
+
VALUES ($1, $2, $3)
|
|
351
|
+
ON CONFLICT (id) DO NOTHING
|
|
361
352
|
`;
|
|
362
353
|
|
|
363
|
-
await client.query(insertQuery, [entityId]);
|
|
354
|
+
await client.query(insertQuery, [entityId, options?.hashavshevetName, options?.taxExcluded ?? false]);
|
|
364
355
|
} catch (error) {
|
|
365
356
|
if (error instanceof EntityValidationError || error instanceof SeedError) {
|
|
366
357
|
throw error;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, beforeAll, afterAll } from 'vitest';
|
|
2
2
|
import { TestDatabase } from './helpers/db-setup.js';
|
|
3
3
|
import { seedAdminCore } from '../../scripts/seed-admin-context.js';
|
|
4
|
+
import { UUID_REGEX } from '../shared/constants.js';
|
|
4
5
|
|
|
5
6
|
describe('seedAdminCore integration', () => {
|
|
6
7
|
let db: TestDatabase;
|
|
@@ -22,7 +23,7 @@ describe('seedAdminCore integration', () => {
|
|
|
22
23
|
// Verify admin entity exists
|
|
23
24
|
expect(adminEntityId).toBeTruthy();
|
|
24
25
|
expect(adminEntityId).toMatch(
|
|
25
|
-
|
|
26
|
+
UUID_REGEX
|
|
26
27
|
);
|
|
27
28
|
|
|
28
29
|
// Verify admin financial entity
|