@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
@@ -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 existingAdmin = await client.query(
48
- `SELECT id FROM accounter_schema.financial_entities
49
- WHERE name = $1 AND type = $2
50
- ORDER BY created_at ASC
51
- LIMIT 1`,
52
- ['Admin Business', 'business'],
53
- );
54
-
55
- let adminEntityId: string;
56
- if (existingAdmin.rows.length > 0) {
57
- adminEntityId = existingAdmin.rows[0].id;
58
- console.log(`ℹ️ Admin entity already exists: ${adminEntityId}`);
59
- } else {
60
- const { id } = await ensureFinancialEntity(client, {
61
- name: 'Admin Business',
62
- type: 'business',
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, { noInvoicesRequired: true });
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(*) FROM ${qualifyTable('tax_categories')}`,
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 '__tests__/helpers/fixture-types.js';
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 '__tests__/helpers/fixture-types.js';
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 '__tests__/helpers/fixture-types.js';
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 entityResult = await client.query(
157
- `INSERT INTO ${qualifyTable('financial_entities')} (id, name, type)
158
- VALUES ($1, $2, 'business')
159
- ON CONFLICT (id) DO NOTHING
160
- RETURNING id`,
161
- [business.id, business.name || business.id],
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.query(
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
- if (entityResult.rows.length > 0) {
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 entityResult = await client.query(
209
- `INSERT INTO ${qualifyTable('financial_entities')} (id, name, type)
210
- VALUES ($1, $2, 'tax_category')
211
- ON CONFLICT (id) DO NOTHING
212
- RETURNING id`,
213
- [taxCategory.id, taxCategory.hashavshevetName || taxCategory.id],
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.query(
218
- `INSERT INTO ${qualifyTable('tax_categories')} (
219
- id, hashavshevet_name, tax_excluded
220
- )
221
- VALUES ($1, $2, $3)
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
- if (entityResult.rows.length > 0) {
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(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
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, { noInvoicesRequired: true });
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 noInvoicesRequired = true
116
- await ensureBusinessForEntity(client, entityId, { noInvoicesRequired: true });
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, { noInvoicesRequired: false });
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
- // Check if entity already exists
95
- const selectQuery = `
96
- SELECT id
97
- FROM ${qualifyTable('financial_entities')}
98
- WHERE name = $1
99
- AND type = $2
100
- AND (owner_id = $3 OR (owner_id IS NULL AND $3 IS NULL))
101
- LIMIT 1
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 insertResult = await client.query<{ id: string }>(
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 = insertResult.rows[0];
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 interface EnsureBusinessForEntityOptions {
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
- const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
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
- // Check if business already exists
227
- const selectQuery = `
228
- SELECT 1
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')} (id, no_invoices_required)
243
- VALUES ($1, $2)
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, [entityId, options?.noInvoicesRequired ?? false]);
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 interface EnsureTaxCategoryForEntityOptions {
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
- const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
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
- // Check if tax category already exists
344
- const selectQuery = `
345
- SELECT 1
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')} (id)
360
- VALUES ($1)
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
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
26
+ UUID_REGEX
26
27
  );
27
28
 
28
29
  // Verify admin financial entity