@open-mercato/core 0.8.1-develop.7239.1.d90c352076 → 0.8.1-develop.7241.1.fad7b2ce2a

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 (26) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/dbFixtures.js +4 -0
  3. package/dist/helpers/integration/dbFixtures.js.map +2 -2
  4. package/dist/modules/customers/index.js +1 -0
  5. package/dist/modules/customers/index.js.map +2 -2
  6. package/dist/modules/query_index/lib/coverage.js +84 -15
  7. package/dist/modules/query_index/lib/coverage.js.map +2 -2
  8. package/dist/modules/query_index/subscribers/delete_one.js.map +2 -2
  9. package/dist/modules/query_index/subscribers/upsert_one.js.map +2 -2
  10. package/dist/modules/sales/data/entities.js.map +2 -2
  11. package/dist/modules/sales/migrations/Migration20260825120000_sales_document_sequences_backing_sequences.js +49 -0
  12. package/dist/modules/sales/migrations/Migration20260825120000_sales_document_sequences_backing_sequences.js.map +7 -0
  13. package/dist/modules/sales/services/salesDocumentNumberGenerator.js +126 -24
  14. package/dist/modules/sales/services/salesDocumentNumberGenerator.js.map +2 -2
  15. package/dist/modules/sales/setup.js +16 -12
  16. package/dist/modules/sales/setup.js.map +2 -2
  17. package/package.json +7 -7
  18. package/src/helpers/integration/dbFixtures.ts +10 -0
  19. package/src/modules/customers/index.ts +1 -0
  20. package/src/modules/query_index/lib/coverage.ts +168 -16
  21. package/src/modules/query_index/subscribers/delete_one.ts +4 -0
  22. package/src/modules/query_index/subscribers/upsert_one.ts +4 -0
  23. package/src/modules/sales/data/entities.ts +8 -0
  24. package/src/modules/sales/migrations/Migration20260825120000_sales_document_sequences_backing_sequences.ts +61 -0
  25. package/src/modules/sales/services/salesDocumentNumberGenerator.ts +161 -24
  26. package/src/modules/sales/setup.ts +16 -4
@@ -0,0 +1,49 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20260825120000_sales_document_sequences_backing_sequences extends Migration {
3
+ async up() {
4
+ this.addSql(`
5
+ do $$
6
+ declare
7
+ registry record;
8
+ sequence_name text;
9
+ begin
10
+ for registry in select id, current_value from sales_document_sequences loop
11
+ sequence_name := 'sales_docseq_' || replace(registry.id::text, '-', '');
12
+ execute format(
13
+ 'create sequence if not exists %I as bigint minvalue 1 start with 1 no cycle',
14
+ sequence_name
15
+ );
16
+ if coalesce(registry.current_value, 0) >= 1 then
17
+ execute format('select setval(%L, %s, true)', sequence_name, registry.current_value);
18
+ end if;
19
+ end loop;
20
+ end $$;
21
+ `);
22
+ }
23
+ async down() {
24
+ this.addSql(`
25
+ do $$
26
+ declare
27
+ registry record;
28
+ sequence_name text;
29
+ last_issued bigint;
30
+ begin
31
+ for registry in select id from sales_document_sequences loop
32
+ sequence_name := 'sales_docseq_' || replace(registry.id::text, '-', '');
33
+ execute format('select pg_sequence_last_value(%L::regclass)', sequence_name) into last_issued;
34
+ if last_issued is not null then
35
+ update sales_document_sequences
36
+ set current_value = greatest(coalesce(current_value, 0), last_issued),
37
+ updated_at = now()
38
+ where id = registry.id;
39
+ end if;
40
+ execute format('drop sequence if exists %I', sequence_name);
41
+ end loop;
42
+ end $$;
43
+ `);
44
+ }
45
+ }
46
+ export {
47
+ Migration20260825120000_sales_document_sequences_backing_sequences
48
+ };
49
+ //# sourceMappingURL=Migration20260825120000_sales_document_sequences_backing_sequences.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/sales/migrations/Migration20260825120000_sales_document_sequences_backing_sequences.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations'\n\n/**\n * Backs every `sales_document_sequences` registry row with a real Postgres sequence (#5604).\n *\n * The table itself is unchanged, so the ORM snapshot is untouched \u2014 the sequences are\n * catalog objects MikroORM does not model. Each sequence is named after the registry row\n * that owns it (`sales_docseq_<row id without dashes>`), which is how\n * `SalesDocumentNumberGenerator` recomputes the name in SQL without an extra column.\n *\n * `current_value` is carried over as the starting point so numbering continues where it left\n * off, and stops being authoritative from here on \u2014 see the entity's deprecation note.\n */\nexport class Migration20260825120000_sales_document_sequences_backing_sequences extends Migration {\n override async up(): Promise<void> {\n this.addSql(`\n do $$\n declare\n registry record;\n sequence_name text;\n begin\n for registry in select id, current_value from sales_document_sequences loop\n sequence_name := 'sales_docseq_' || replace(registry.id::text, '-', '');\n execute format(\n 'create sequence if not exists %I as bigint minvalue 1 start with 1 no cycle',\n sequence_name\n );\n if coalesce(registry.current_value, 0) >= 1 then\n execute format('select setval(%L, %s, true)', sequence_name, registry.current_value);\n end if;\n end loop;\n end $$;\n `)\n }\n\n override async down(): Promise<void> {\n // Hand authority back to `current_value` before dropping the sequences, otherwise the\n // rolled-back code resumes from a value that stopped advancing when this migration ran\n // and re-issues numbers that are already on documents.\n this.addSql(`\n do $$\n declare\n registry record;\n sequence_name text;\n last_issued bigint;\n begin\n for registry in select id from sales_document_sequences loop\n sequence_name := 'sales_docseq_' || replace(registry.id::text, '-', '');\n execute format('select pg_sequence_last_value(%L::regclass)', sequence_name) into last_issued;\n if last_issued is not null then\n update sales_document_sequences\n set current_value = greatest(coalesce(current_value, 0), last_issued),\n updated_at = now()\n where id = registry.id;\n end if;\n execute format('drop sequence if exists %I', sequence_name);\n end loop;\n end $$;\n `)\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAanB,MAAM,2EAA2E,UAAU;AAAA,EAChG,MAAe,KAAoB;AACjC,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAiBX;AAAA,EACH;AAAA,EAEA,MAAe,OAAsB;AAInC,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAmBX;AAAA,EACH;AACF;",
6
+ "names": []
7
+ }
@@ -1,5 +1,5 @@
1
1
  import { randomBytes, randomUUID } from "crypto";
2
- import { SalesDocumentSequence, SalesSettings } from "../data/entities.js";
2
+ import { SalesSettings } from "../data/entities.js";
3
3
  import {
4
4
  DEFAULT_ORDER_NUMBER_FORMAT,
5
5
  DEFAULT_QUOTE_NUMBER_FORMAT,
@@ -30,6 +30,40 @@ const generateRandomDigits = (size = 4) => {
30
30
  }
31
31
  return digits.join("");
32
32
  };
33
+ const SEQUENCE_NAME_PREFIX = "sales_docseq_";
34
+ const SEQUENCE_NAME_PATTERN = /^sales_docseq_[0-9a-f]{32}$/;
35
+ const SEQUENCE_REGCLASS_SQL = `('${SEQUENCE_NAME_PREFIX}' || replace(id::text, '-', ''))::regclass`;
36
+ const SEQUENCE_SCOPE_PREDICATE = "organization_id = ? and tenant_id = ? and document_kind = ?";
37
+ function documentSequenceName(registryId) {
38
+ const name = `${SEQUENCE_NAME_PREFIX}${String(registryId).replace(/-/g, "").toLowerCase()}`;
39
+ if (!SEQUENCE_NAME_PATTERN.test(name)) {
40
+ throw new Error("[internal] Document-sequence registry id is not a UUID");
41
+ }
42
+ return name;
43
+ }
44
+ function isMissingRelationError(error) {
45
+ if (error?.code === "42P01") return true;
46
+ const message = error instanceof Error ? error.message : "";
47
+ return /does not exist/i.test(message);
48
+ }
49
+ function isDuplicateRelationError(error) {
50
+ const code = error?.code;
51
+ if (code === "42P07" || code === "23505") return true;
52
+ const message = error instanceof Error ? error.message : "";
53
+ return /already exists/i.test(message);
54
+ }
55
+ async function createDocumentSequence(em, registryId) {
56
+ const name = documentSequenceName(registryId);
57
+ try {
58
+ await em.getConnection().execute(
59
+ `create sequence if not exists "${name}" as bigint minvalue ${DEFAULT_SEQUENCE_START} start with ${DEFAULT_SEQUENCE_START} no cycle`,
60
+ [],
61
+ "run"
62
+ );
63
+ } catch (error) {
64
+ if (!isDuplicateRelationError(error)) throw error;
65
+ }
66
+ }
33
67
  class SalesDocumentNumberGenerator {
34
68
  constructor(em) {
35
69
  this.em = em;
@@ -53,16 +87,29 @@ class SalesDocumentNumberGenerator {
53
87
  return { order, quote, return: salesReturn };
54
88
  }
55
89
  async setNextSequence(kind, scope, nextValue) {
56
- const next = Math.min(Math.max(Math.floor(nextValue), DEFAULT_SEQUENCE_START), MAX_SEQUENCE);
57
- const baseValue = next - 1;
90
+ const next = Math.floor(nextValue);
91
+ if (!Number.isFinite(next) || next < DEFAULT_SEQUENCE_START || next > MAX_SEQUENCE) {
92
+ throw new Error("[internal] Sales document sequence target is outside the supported range");
93
+ }
94
+ await this.ensureSequence(kind, scope);
95
+ const isCalled = next > DEFAULT_SEQUENCE_START;
96
+ const armedAt = isCalled ? next - 1 : DEFAULT_SEQUENCE_START;
58
97
  await this.em.getConnection().execute(
59
98
  `
60
- insert into sales_document_sequences (id, organization_id, tenant_id, document_kind, current_value, created_at, updated_at)
61
- values (gen_random_uuid(), ?, ?, ?, ?, now(), now())
62
- on conflict (organization_id, tenant_id, document_kind)
63
- do update set current_value = ?, updated_at = now()
99
+ select setval(${SEQUENCE_REGCLASS_SQL}, ?, ?)
100
+ from sales_document_sequences
101
+ where ${SEQUENCE_SCOPE_PREDICATE}
64
102
  `,
65
- [scope.organizationId, scope.tenantId, kind, baseValue, baseValue]
103
+ [armedAt, isCalled, scope.organizationId, scope.tenantId, kind]
104
+ );
105
+ await this.em.getConnection().execute(
106
+ `
107
+ update sales_document_sequences
108
+ set current_value = ?, updated_at = now()
109
+ where ${SEQUENCE_SCOPE_PREDICATE}
110
+ `,
111
+ [next - 1, scope.organizationId, scope.tenantId, kind],
112
+ "run"
66
113
  );
67
114
  }
68
115
  async generate(params) {
@@ -78,30 +125,83 @@ class SalesDocumentNumberGenerator {
78
125
  return { number, format, sequence };
79
126
  }
80
127
  async peekNextSequence(kind, scope) {
81
- const record = await this.em.findOne(SalesDocumentSequence, {
82
- tenantId: scope.tenantId,
83
- organizationId: scope.organizationId,
84
- documentKind: kind
85
- });
86
- if (record && typeof record.currentValue === "number") {
87
- return Math.min(record.currentValue + 1, MAX_SEQUENCE);
128
+ let rows;
129
+ try {
130
+ rows = await this.em.getConnection().execute(
131
+ `
132
+ select pg_sequence_last_value(${SEQUENCE_REGCLASS_SQL}) as last_value
133
+ from sales_document_sequences
134
+ where ${SEQUENCE_SCOPE_PREDICATE}
135
+ `,
136
+ [scope.organizationId, scope.tenantId, kind]
137
+ );
138
+ } catch (error) {
139
+ if (isMissingRelationError(error)) return DEFAULT_SEQUENCE_START;
140
+ throw error;
88
141
  }
89
- return DEFAULT_SEQUENCE_START;
142
+ const lastValue = rows?.[0]?.last_value;
143
+ if (lastValue == null) return DEFAULT_SEQUENCE_START;
144
+ const value = Number(lastValue);
145
+ if (!Number.isFinite(value)) return DEFAULT_SEQUENCE_START;
146
+ return Math.min(Math.max(value + 1, DEFAULT_SEQUENCE_START), MAX_SEQUENCE);
90
147
  }
148
+ // `nextval` is the whole point of the sequence: it is atomic, never takes a row lock, and
149
+ // produces no dead tuples, so concurrent claims for the same scope no longer serialize on a
150
+ // single `sales_document_sequences` row the way the previous `UPDATE … current_value + 1`
151
+ // did (#5604). The registry row still exists — it names the sequence and records the scope —
152
+ // but the hot path only reads it.
91
153
  async claimSequence(kind, scope) {
154
+ const claimed = await this.tryClaimSequence(kind, scope);
155
+ if (claimed !== null) return claimed;
156
+ await this.ensureSequence(kind, scope);
157
+ const retried = await this.tryClaimSequence(kind, scope);
158
+ if (retried === null) {
159
+ throw new Error("[internal] Sales document sequence is missing immediately after being created");
160
+ }
161
+ return retried;
162
+ }
163
+ async tryClaimSequence(kind, scope) {
164
+ let rows;
165
+ try {
166
+ rows = await this.em.getConnection().execute(
167
+ `
168
+ select nextval(${SEQUENCE_REGCLASS_SQL}) as claimed
169
+ from sales_document_sequences
170
+ where ${SEQUENCE_SCOPE_PREDICATE}
171
+ `,
172
+ [scope.organizationId, scope.tenantId, kind]
173
+ );
174
+ } catch (error) {
175
+ if (isMissingRelationError(error)) return null;
176
+ throw error;
177
+ }
178
+ const claimed = rows?.[0]?.claimed;
179
+ if (claimed == null) return null;
180
+ const value = Number(claimed);
181
+ if (!Number.isFinite(value) || value < DEFAULT_SEQUENCE_START) {
182
+ throw new Error("[internal] Sales document sequence returned a value below its start");
183
+ }
184
+ if (value > MAX_SEQUENCE) {
185
+ throw new Error("[internal] Sales document sequence is exhausted");
186
+ }
187
+ return value;
188
+ }
189
+ async ensureSequence(kind, scope) {
92
190
  const rows = await this.em.getConnection().execute(
93
191
  `
94
192
  insert into sales_document_sequences (id, organization_id, tenant_id, document_kind, current_value, created_at, updated_at)
95
- values (gen_random_uuid(), ?, ?, ?, ?, now(), now())
193
+ values (gen_random_uuid(), ?, ?, ?, 0, now(), now())
96
194
  on conflict (organization_id, tenant_id, document_kind)
97
- do update set current_value = sales_document_sequences.current_value + 1, updated_at = now()
98
- returning current_value
195
+ do update set updated_at = now()
196
+ returning id
99
197
  `,
100
- [scope.organizationId, scope.tenantId, kind, DEFAULT_SEQUENCE_START]
198
+ [scope.organizationId, scope.tenantId, kind]
101
199
  );
102
- const value = Number(rows?.[0]?.current_value ?? DEFAULT_SEQUENCE_START);
103
- if (!Number.isFinite(value) || value < DEFAULT_SEQUENCE_START) return DEFAULT_SEQUENCE_START;
104
- return Math.min(value, MAX_SEQUENCE);
200
+ const registryId = rows?.[0]?.id;
201
+ if (!registryId) {
202
+ throw new Error("[internal] Could not resolve a sales document sequence registry row");
203
+ }
204
+ await createDocumentSequence(this.em, registryId);
105
205
  }
106
206
  formatNumber(template, context) {
107
207
  const source = template?.trim() || (context.kind === "order" ? DEFAULT_ORDER_NUMBER_FORMAT : context.kind === "quote" ? DEFAULT_QUOTE_NUMBER_FORMAT : DEFAULT_RETURN_NUMBER_FORMAT);
@@ -146,6 +246,8 @@ class SalesDocumentNumberGenerator {
146
246
  }
147
247
  }
148
248
  export {
149
- SalesDocumentNumberGenerator
249
+ SalesDocumentNumberGenerator,
250
+ createDocumentSequence,
251
+ documentSequenceName
150
252
  };
151
253
  //# sourceMappingURL=salesDocumentNumberGenerator.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/sales/services/salesDocumentNumberGenerator.ts"],
4
- "sourcesContent": ["import { randomBytes, randomUUID } from 'crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { SalesDocumentSequence, SalesSettings } from '../data/entities'\nimport {\n DEFAULT_ORDER_NUMBER_FORMAT,\n DEFAULT_QUOTE_NUMBER_FORMAT,\n DEFAULT_RETURN_NUMBER_FORMAT,\n DEFAULT_INVOICE_NUMBER_FORMAT,\n DEFAULT_CREDIT_MEMO_NUMBER_FORMAT,\n type SalesDocumentNumberKind,\n} from '../lib/documentNumberTokens'\n\ntype Scope = {\n organizationId: string\n tenantId: string\n}\n\ntype GenerateParams = Scope & {\n kind: SalesDocumentNumberKind\n format?: string | null\n}\n\ntype SettingsSnapshot = {\n orderNumberFormat: string\n quoteNumberFormat: string\n}\n\ntype SequenceSnapshot = {\n order: number\n quote: number\n return: number\n}\n\nconst MAX_SEQUENCE = 1_000_000_000\nconst DEFAULT_SEQUENCE_START = 1\n\nconst createNanoId = (size = 12) => {\n const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'\n const maxValid = 256 - (256 % alphabet.length) // 248 for alphabet.length=62\n let id = ''\n while (id.length < size) {\n const byte = randomBytes(1)[0]\n if (byte >= maxValid) continue // rejection sampling to avoid bias\n id += alphabet[byte % alphabet.length]\n }\n return id\n}\n\nconst generateRandomDigits = (size = 4) => {\n const length = Math.max(1, Math.min(size, 12))\n const digits: string[] = []\n while (digits.length < length) {\n const byte = randomBytes(1)[0]\n if (byte >= 250) continue // rejection sampling: only use 0-249 which divides evenly by 10\n digits.push((byte % 10).toString())\n }\n return digits.join('')\n}\n\nexport class SalesDocumentNumberGenerator {\n constructor(private readonly em: EntityManager) {}\n\n async getSettings(scope: Scope): Promise<SettingsSnapshot> {\n const record = await this.em.findOne(SalesSettings, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n return {\n orderNumberFormat: record?.orderNumberFormat?.trim() || DEFAULT_ORDER_NUMBER_FORMAT,\n quoteNumberFormat: record?.quoteNumberFormat?.trim() || DEFAULT_QUOTE_NUMBER_FORMAT,\n }\n }\n\n async peekSequences(scope: Scope): Promise<SequenceSnapshot> {\n const [order, quote, salesReturn] = await Promise.all([\n this.peekNextSequence('order', scope),\n this.peekNextSequence('quote', scope),\n this.peekNextSequence('return', scope),\n ])\n return { order, quote, return: salesReturn }\n }\n\n async setNextSequence(kind: SalesDocumentNumberKind, scope: Scope, nextValue: number): Promise<void> {\n const next = Math.min(Math.max(Math.floor(nextValue), DEFAULT_SEQUENCE_START), MAX_SEQUENCE)\n const baseValue = next - 1\n await this.em.getConnection().execute(\n `\n insert into sales_document_sequences (id, organization_id, tenant_id, document_kind, current_value, created_at, updated_at)\n values (gen_random_uuid(), ?, ?, ?, ?, now(), now())\n on conflict (organization_id, tenant_id, document_kind)\n do update set current_value = ?, updated_at = now()\n `,\n [scope.organizationId, scope.tenantId, kind, baseValue, baseValue]\n )\n }\n\n async generate(params: GenerateParams): Promise<{ number: string; format: string; sequence: number }> {\n const settings = await this.getSettings(params)\n const format =\n params.format?.trim() ||\n (params.kind === 'order'\n ? settings.orderNumberFormat\n : params.kind === 'quote'\n ? settings.quoteNumberFormat\n : params.kind === 'invoice'\n ? DEFAULT_INVOICE_NUMBER_FORMAT\n : params.kind === 'credit_memo'\n ? DEFAULT_CREDIT_MEMO_NUMBER_FORMAT\n : DEFAULT_RETURN_NUMBER_FORMAT)\n const sequence = await this.claimSequence(params.kind, params)\n const number = this.formatNumber(format, {\n kind: params.kind,\n sequence,\n date: new Date(),\n guid: randomUUID(),\n })\n return { number, format, sequence }\n }\n\n private async peekNextSequence(kind: SalesDocumentNumberKind, scope: Scope): Promise<number> {\n const record = await this.em.findOne(SalesDocumentSequence, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n documentKind: kind,\n })\n if (record && typeof record.currentValue === 'number') {\n return Math.min(record.currentValue + 1, MAX_SEQUENCE)\n }\n return DEFAULT_SEQUENCE_START\n }\n\n private async claimSequence(kind: SalesDocumentNumberKind, scope: Scope): Promise<number> {\n const rows = await this.em.getConnection().execute<{ current_value: string }[]>(\n `\n insert into sales_document_sequences (id, organization_id, tenant_id, document_kind, current_value, created_at, updated_at)\n values (gen_random_uuid(), ?, ?, ?, ?, now(), now())\n on conflict (organization_id, tenant_id, document_kind)\n do update set current_value = sales_document_sequences.current_value + 1, updated_at = now()\n returning current_value\n `,\n [scope.organizationId, scope.tenantId, kind, DEFAULT_SEQUENCE_START]\n )\n const value = Number(rows?.[0]?.current_value ?? DEFAULT_SEQUENCE_START)\n if (!Number.isFinite(value) || value < DEFAULT_SEQUENCE_START) return DEFAULT_SEQUENCE_START\n return Math.min(value, MAX_SEQUENCE)\n }\n\n private formatNumber(\n template: string,\n context: { kind: SalesDocumentNumberKind; sequence: number; date: Date; guid?: string | null }\n ): string {\n const source =\n template?.trim() ||\n (context.kind === 'order'\n ? DEFAULT_ORDER_NUMBER_FORMAT\n : context.kind === 'quote'\n ? DEFAULT_QUOTE_NUMBER_FORMAT\n : DEFAULT_RETURN_NUMBER_FORMAT)\n const now = context.date\n return source.replace(/\\{([a-zA-Z]+)(?::([^}]+))?\\}/g, (match, rawToken, rawArg) => {\n const token = rawToken.toLowerCase()\n const arg = typeof rawArg === 'string' ? rawArg.trim() : ''\n switch (token) {\n case 'yyyy':\n return String(now.getFullYear())\n case 'yy':\n return String(now.getFullYear()).slice(-2)\n case 'mm':\n return String(now.getMonth() + 1).padStart(2, '0')\n case 'dd':\n return String(now.getDate()).padStart(2, '0')\n case 'hh':\n return String(now.getHours()).padStart(2, '0')\n case 'seq': {\n const requested = parseInt(arg || '', 10)\n const width = Number.isFinite(requested) && requested > 0 ? Math.min(requested, 12) : undefined\n return width ? String(context.sequence).padStart(width, '0') : String(context.sequence)\n }\n case 'rand': {\n const requested = parseInt(arg || '', 10)\n const length = Number.isFinite(requested) && requested > 0 ? requested : 4\n return generateRandomDigits(length)\n }\n case 'guid':\n return context.guid || randomUUID()\n case 'nanoid': {\n const requested = parseInt(arg || '', 10)\n const size =\n Number.isFinite(requested) && requested > 0 ? Math.min(requested, 32) : 12\n return createNanoId(size)\n }\n case 'kind':\n return context.kind\n default:\n return match\n }\n })\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { randomBytes, randomUUID } from 'crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { SalesSettings } from '../data/entities'\nimport {\n DEFAULT_ORDER_NUMBER_FORMAT,\n DEFAULT_QUOTE_NUMBER_FORMAT,\n DEFAULT_RETURN_NUMBER_FORMAT,\n DEFAULT_INVOICE_NUMBER_FORMAT,\n DEFAULT_CREDIT_MEMO_NUMBER_FORMAT,\n type SalesDocumentNumberKind,\n} from '../lib/documentNumberTokens'\n\ntype Scope = {\n organizationId: string\n tenantId: string\n}\n\ntype GenerateParams = Scope & {\n kind: SalesDocumentNumberKind\n format?: string | null\n}\n\ntype SettingsSnapshot = {\n orderNumberFormat: string\n quoteNumberFormat: string\n}\n\ntype SequenceSnapshot = {\n order: number\n quote: number\n return: number\n}\n\nconst MAX_SEQUENCE = 1_000_000_000\nconst DEFAULT_SEQUENCE_START = 1\n\nconst createNanoId = (size = 12) => {\n const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'\n const maxValid = 256 - (256 % alphabet.length) // 248 for alphabet.length=62\n let id = ''\n while (id.length < size) {\n const byte = randomBytes(1)[0]\n if (byte >= maxValid) continue // rejection sampling to avoid bias\n id += alphabet[byte % alphabet.length]\n }\n return id\n}\n\nconst generateRandomDigits = (size = 4) => {\n const length = Math.max(1, Math.min(size, 12))\n const digits: string[] = []\n while (digits.length < length) {\n const byte = randomBytes(1)[0]\n if (byte >= 250) continue // rejection sampling: only use 0-249 which divides evenly by 10\n digits.push((byte % 10).toString())\n }\n return digits.join('')\n}\n\n// Each (organization, tenant, kind) scope is backed by its own Postgres sequence, named\n// after the `sales_document_sequences` registry row that owns it (#5604). Deriving the name\n// from the row's primary key keeps the mapping recomputable from the table alone \u2014 no extra\n// column, and `\\ds sales_docseq_*` in psql lines up with the registry.\nconst SEQUENCE_NAME_PREFIX = 'sales_docseq_'\nconst SEQUENCE_NAME_PATTERN = /^sales_docseq_[0-9a-f]{32}$/\n// Recomputes the name in SQL so a claim is a single round trip: look the registry row up by\n// its unique scope index and call `nextval` on the sequence that row names.\nconst SEQUENCE_REGCLASS_SQL = `('${SEQUENCE_NAME_PREFIX}' || replace(id::text, '-', ''))::regclass`\nconst SEQUENCE_SCOPE_PREDICATE = 'organization_id = ? and tenant_id = ? and document_kind = ?'\n\nexport function documentSequenceName(registryId: string): string {\n const name = `${SEQUENCE_NAME_PREFIX}${String(registryId).replace(/-/g, '').toLowerCase()}`\n if (!SEQUENCE_NAME_PATTERN.test(name)) {\n throw new Error('[internal] Document-sequence registry id is not a UUID')\n }\n return name\n}\n\nfunction isMissingRelationError(error: unknown): boolean {\n if ((error as { code?: unknown } | null)?.code === '42P01') return true\n const message = error instanceof Error ? error.message : ''\n return /does not exist/i.test(message)\n}\n\nfunction isDuplicateRelationError(error: unknown): boolean {\n const code = (error as { code?: unknown } | null)?.code\n if (code === '42P07' || code === '23505') return true\n const message = error instanceof Error ? error.message : ''\n return /already exists/i.test(message)\n}\n\n/**\n * Creates the Postgres sequence backing a `sales_document_sequences` registry row, if it is\n * not there yet. Safe to call concurrently: `if not exists` still races on `pg_class`, so a\n * duplicate error means another caller won and is treated as success.\n */\nexport async function createDocumentSequence(em: EntityManager, registryId: string): Promise<void> {\n const name = documentSequenceName(registryId)\n try {\n await em.getConnection().execute(\n `create sequence if not exists \"${name}\" as bigint minvalue ${DEFAULT_SEQUENCE_START} start with ${DEFAULT_SEQUENCE_START} no cycle`,\n [],\n 'run'\n )\n } catch (error) {\n if (!isDuplicateRelationError(error)) throw error\n }\n}\n\nexport class SalesDocumentNumberGenerator {\n constructor(private readonly em: EntityManager) {}\n\n async getSettings(scope: Scope): Promise<SettingsSnapshot> {\n const record = await this.em.findOne(SalesSettings, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n return {\n orderNumberFormat: record?.orderNumberFormat?.trim() || DEFAULT_ORDER_NUMBER_FORMAT,\n quoteNumberFormat: record?.quoteNumberFormat?.trim() || DEFAULT_QUOTE_NUMBER_FORMAT,\n }\n }\n\n async peekSequences(scope: Scope): Promise<SequenceSnapshot> {\n const [order, quote, salesReturn] = await Promise.all([\n this.peekNextSequence('order', scope),\n this.peekNextSequence('quote', scope),\n this.peekNextSequence('return', scope),\n ])\n return { order, quote, return: salesReturn }\n }\n\n async setNextSequence(kind: SalesDocumentNumberKind, scope: Scope, nextValue: number): Promise<void> {\n const next = Math.floor(nextValue)\n // Refuse an out-of-range target rather than clamping to it, for the same reason\n // `tryClaimSequence` refuses to hand out a clamped value: parking the series on the ceiling\n // makes every later claim collide there, so silently accepting the value is the same \"two\n // documents share a number\" hazard one step earlier. `salesSettingsUpsertSchema` already\n // rejects the range at the API boundary, so this only fires for a direct service caller.\n if (!Number.isFinite(next) || next < DEFAULT_SEQUENCE_START || next > MAX_SEQUENCE) {\n throw new Error('[internal] Sales document sequence target is outside the supported range')\n }\n await this.ensureSequence(kind, scope)\n // Park the sequence on `next - 1` as an already-issued value, so the next `nextval`\n // returns exactly `next`. The `setval(next, false)` form would do that too, but it leaves\n // `is_called = false`, and that is precisely the state `pg_sequence_last_value` reports as\n // NULL \u2014 `peekNextSequence` would then read the series back as the start value and the\n // settings form would write that rewind back on its next save.\n // The start value has no predecessor to park on and `minvalue` forbids one; it is also the\n // one value the untouched \"never called\" state already claims and reports correctly.\n const isCalled = next > DEFAULT_SEQUENCE_START\n const armedAt = isCalled ? next - 1 : DEFAULT_SEQUENCE_START\n await this.em.getConnection().execute(\n `\n select setval(${SEQUENCE_REGCLASS_SQL}, ?, ?)\n from sales_document_sequences\n where ${SEQUENCE_SCOPE_PREDICATE}\n `,\n [armedAt, isCalled, scope.organizationId, scope.tenantId, kind]\n )\n await this.em.getConnection().execute(\n `\n update sales_document_sequences\n set current_value = ?, updated_at = now()\n where ${SEQUENCE_SCOPE_PREDICATE}\n `,\n [next - 1, scope.organizationId, scope.tenantId, kind],\n 'run'\n )\n }\n\n async generate(params: GenerateParams): Promise<{ number: string; format: string; sequence: number }> {\n const settings = await this.getSettings(params)\n const format =\n params.format?.trim() ||\n (params.kind === 'order'\n ? settings.orderNumberFormat\n : params.kind === 'quote'\n ? settings.quoteNumberFormat\n : params.kind === 'invoice'\n ? DEFAULT_INVOICE_NUMBER_FORMAT\n : params.kind === 'credit_memo'\n ? DEFAULT_CREDIT_MEMO_NUMBER_FORMAT\n : DEFAULT_RETURN_NUMBER_FORMAT)\n const sequence = await this.claimSequence(params.kind, params)\n const number = this.formatNumber(format, {\n kind: params.kind,\n sequence,\n date: new Date(),\n guid: randomUUID(),\n })\n return { number, format, sequence }\n }\n\n private async peekNextSequence(kind: SalesDocumentNumberKind, scope: Scope): Promise<number> {\n let rows: { last_value: string | number | null }[] | undefined\n try {\n // `pg_sequence_last_value` reports the sequence's state without consuming a value, and\n // returns NULL for a sequence that has never been called \u2014 which is the only state\n // `setNextSequence` leaves behind for the start value, and the value it claims next.\n rows = await this.em.getConnection().execute<{ last_value: string | number | null }[]>(\n `\n select pg_sequence_last_value(${SEQUENCE_REGCLASS_SQL}) as last_value\n from sales_document_sequences\n where ${SEQUENCE_SCOPE_PREDICATE}\n `,\n [scope.organizationId, scope.tenantId, kind]\n )\n } catch (error) {\n if (isMissingRelationError(error)) return DEFAULT_SEQUENCE_START\n throw error\n }\n const lastValue = rows?.[0]?.last_value\n if (lastValue == null) return DEFAULT_SEQUENCE_START\n const value = Number(lastValue)\n if (!Number.isFinite(value)) return DEFAULT_SEQUENCE_START\n return Math.min(Math.max(value + 1, DEFAULT_SEQUENCE_START), MAX_SEQUENCE)\n }\n\n // `nextval` is the whole point of the sequence: it is atomic, never takes a row lock, and\n // produces no dead tuples, so concurrent claims for the same scope no longer serialize on a\n // single `sales_document_sequences` row the way the previous `UPDATE \u2026 current_value + 1`\n // did (#5604). The registry row still exists \u2014 it names the sequence and records the scope \u2014\n // but the hot path only reads it.\n private async claimSequence(kind: SalesDocumentNumberKind, scope: Scope): Promise<number> {\n const claimed = await this.tryClaimSequence(kind, scope)\n if (claimed !== null) return claimed\n // First claim for this scope (or a tenant created before the sequences existed): create\n // the registry row and its sequence, then claim once more.\n await this.ensureSequence(kind, scope)\n const retried = await this.tryClaimSequence(kind, scope)\n if (retried === null) {\n throw new Error('[internal] Sales document sequence is missing immediately after being created')\n }\n return retried\n }\n\n private async tryClaimSequence(kind: SalesDocumentNumberKind, scope: Scope): Promise<number | null> {\n let rows: { claimed: string | number | null }[] | undefined\n try {\n rows = await this.em.getConnection().execute<{ claimed: string | number | null }[]>(\n `\n select nextval(${SEQUENCE_REGCLASS_SQL}) as claimed\n from sales_document_sequences\n where ${SEQUENCE_SCOPE_PREDICATE}\n `,\n [scope.organizationId, scope.tenantId, kind]\n )\n } catch (error) {\n if (isMissingRelationError(error)) return null\n throw error\n }\n const claimed = rows?.[0]?.claimed\n if (claimed == null) return null\n const value = Number(claimed)\n if (!Number.isFinite(value) || value < DEFAULT_SEQUENCE_START) {\n throw new Error('[internal] Sales document sequence returned a value below its start')\n }\n if (value > MAX_SEQUENCE) {\n // Never hand out a clamped value: two documents sharing a number is a worse failure\n // than refusing to issue one, and the caller's transaction can still roll back cleanly.\n throw new Error('[internal] Sales document sequence is exhausted')\n }\n return value\n }\n\n private async ensureSequence(kind: SalesDocumentNumberKind, scope: Scope): Promise<void> {\n const rows = await this.em.getConnection().execute<{ id: string }[]>(\n `\n insert into sales_document_sequences (id, organization_id, tenant_id, document_kind, current_value, created_at, updated_at)\n values (gen_random_uuid(), ?, ?, ?, 0, now(), now())\n on conflict (organization_id, tenant_id, document_kind)\n do update set updated_at = now()\n returning id\n `,\n [scope.organizationId, scope.tenantId, kind]\n )\n const registryId = rows?.[0]?.id\n if (!registryId) {\n throw new Error('[internal] Could not resolve a sales document sequence registry row')\n }\n await createDocumentSequence(this.em, registryId)\n }\n\n private formatNumber(\n template: string,\n context: { kind: SalesDocumentNumberKind; sequence: number; date: Date; guid?: string | null }\n ): string {\n const source =\n template?.trim() ||\n (context.kind === 'order'\n ? DEFAULT_ORDER_NUMBER_FORMAT\n : context.kind === 'quote'\n ? DEFAULT_QUOTE_NUMBER_FORMAT\n : DEFAULT_RETURN_NUMBER_FORMAT)\n const now = context.date\n return source.replace(/\\{([a-zA-Z]+)(?::([^}]+))?\\}/g, (match, rawToken, rawArg) => {\n const token = rawToken.toLowerCase()\n const arg = typeof rawArg === 'string' ? rawArg.trim() : ''\n switch (token) {\n case 'yyyy':\n return String(now.getFullYear())\n case 'yy':\n return String(now.getFullYear()).slice(-2)\n case 'mm':\n return String(now.getMonth() + 1).padStart(2, '0')\n case 'dd':\n return String(now.getDate()).padStart(2, '0')\n case 'hh':\n return String(now.getHours()).padStart(2, '0')\n case 'seq': {\n const requested = parseInt(arg || '', 10)\n const width = Number.isFinite(requested) && requested > 0 ? Math.min(requested, 12) : undefined\n return width ? String(context.sequence).padStart(width, '0') : String(context.sequence)\n }\n case 'rand': {\n const requested = parseInt(arg || '', 10)\n const length = Number.isFinite(requested) && requested > 0 ? requested : 4\n return generateRandomDigits(length)\n }\n case 'guid':\n return context.guid || randomUUID()\n case 'nanoid': {\n const requested = parseInt(arg || '', 10)\n const size =\n Number.isFinite(requested) && requested > 0 ? Math.min(requested, 32) : 12\n return createNanoId(size)\n }\n case 'kind':\n return context.kind\n default:\n return match\n }\n })\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -4,6 +4,7 @@ import { seedSalesStatusDictionaries, seedSalesAdjustmentKinds } from "./lib/dic
4
4
  import { seedSalesChannelsToggle } from "./lib/salesChannelsToggleSeed.js";
5
5
  import { ensureExampleShippingMethods, ensureExamplePaymentMethods } from "./seed/examples-data.js";
6
6
  import { seedSalesExamples } from "./seed/examples.js";
7
+ import { createDocumentSequence } from "./services/salesDocumentNumberGenerator.js";
7
8
  const DEFAULT_TAX_RATES = [
8
9
  { code: "vat-23", name: "23% VAT", rate: "23" },
9
10
  { code: "vat-0", name: "0% VAT", rate: "0" }
@@ -77,26 +78,29 @@ const setup = {
77
78
  })
78
79
  );
79
80
  }
81
+ const sequenceRows = [];
80
82
  for (const kind of ["order", "quote", "return", "invoice", "credit_memo"]) {
81
83
  const seq = await em.findOne(SalesDocumentSequence, {
82
84
  tenantId,
83
85
  organizationId,
84
86
  documentKind: kind
85
87
  });
86
- if (!seq) {
87
- em.persist(
88
- em.create(SalesDocumentSequence, {
89
- tenantId,
90
- organizationId,
91
- documentKind: kind,
92
- currentValue: 0,
93
- createdAt: /* @__PURE__ */ new Date(),
94
- updatedAt: /* @__PURE__ */ new Date()
95
- })
96
- );
97
- }
88
+ sequenceRows.push(
89
+ seq ?? em.create(SalesDocumentSequence, {
90
+ tenantId,
91
+ organizationId,
92
+ documentKind: kind,
93
+ currentValue: 0,
94
+ createdAt: /* @__PURE__ */ new Date(),
95
+ updatedAt: /* @__PURE__ */ new Date()
96
+ })
97
+ );
98
+ if (!seq) em.persist(sequenceRows[sequenceRows.length - 1]);
98
99
  }
99
100
  await em.flush();
101
+ for (const row of sequenceRows) {
102
+ await createDocumentSequence(em, row.id);
103
+ }
100
104
  },
101
105
  async seedDefaults({ em, tenantId, organizationId }) {
102
106
  const scope = { tenantId, organizationId };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/sales/setup.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { SalesSettings, SalesDocumentSequence, SalesTaxRate } from './data/entities'\nimport { DEFAULT_ORDER_NUMBER_FORMAT, DEFAULT_QUOTE_NUMBER_FORMAT } from './lib/documentNumberTokens'\nimport { seedSalesStatusDictionaries, seedSalesAdjustmentKinds } from './lib/dictionaries'\nimport { seedSalesChannelsToggle } from './lib/salesChannelsToggleSeed'\nimport { ensureExampleShippingMethods, ensureExamplePaymentMethods } from './seed/examples-data'\nimport { seedSalesExamples } from './seed/examples'\n\ntype SeedScope = { tenantId: string; organizationId: string }\n\nconst DEFAULT_TAX_RATES = [\n { code: 'vat-23', name: '23% VAT', rate: '23' },\n { code: 'vat-0', name: '0% VAT', rate: '0' },\n] as const\n\nasync function seedSalesTaxRates(em: EntityManager, scope: SeedScope): Promise<void> {\n await em.transactional(async (tem) => {\n const existing = await tem.find(SalesTaxRate, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n })\n const existingCodes = new Set(existing.map((rate) => rate.code))\n const hasDefault = existing.some((rate) => rate.isDefault)\n const now = new Date()\n let isFirst = !hasDefault\n\n for (const seed of DEFAULT_TAX_RATES) {\n if (existingCodes.has(seed.code)) continue\n tem.persist(\n tem.create(SalesTaxRate, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n code: seed.code,\n name: seed.name,\n rate: seed.rate,\n priority: 0,\n isCompound: false,\n isDefault: isFirst,\n createdAt: now,\n updatedAt: now,\n })\n )\n isFirst = false\n }\n })\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: ['sales.*', 'sales.documents.number.edit'],\n employee: [\n 'sales.channels.view',\n 'sales.channels.manage',\n 'sales.settings.view',\n 'sales.settings.manage',\n 'sales.orders.view',\n 'sales.orders.manage',\n 'sales.orders.approve',\n 'sales.widgets.new-orders',\n 'sales.widgets.new-quotes',\n 'sales.quotes.view',\n 'sales.quotes.manage',\n 'sales.shipments.manage',\n 'sales.payments.manage',\n 'sales.returns.view',\n 'sales.returns.create',\n 'sales.returns.manage',\n 'sales.invoices.manage',\n 'sales.credit_memos.manage',\n ],\n },\n\n async onTenantCreated({ em, tenantId, organizationId }) {\n const exists = await em.findOne(SalesSettings, { tenantId, organizationId })\n if (!exists) {\n em.persist(\n em.create(SalesSettings, {\n tenantId,\n organizationId,\n orderNumberFormat: DEFAULT_ORDER_NUMBER_FORMAT,\n quoteNumberFormat: DEFAULT_QUOTE_NUMBER_FORMAT,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n )\n }\n\n for (const kind of ['order', 'quote', 'return', 'invoice', 'credit_memo'] as const) {\n const seq = await em.findOne(SalesDocumentSequence, {\n tenantId,\n organizationId,\n documentKind: kind,\n })\n if (!seq) {\n em.persist(\n em.create(SalesDocumentSequence, {\n tenantId,\n organizationId,\n documentKind: kind,\n currentValue: 0,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n )\n }\n }\n\n await em.flush()\n },\n\n async seedDefaults({ em, tenantId, organizationId }) {\n const scope = { tenantId, organizationId }\n await seedSalesTaxRates(em, scope)\n await seedSalesStatusDictionaries(em, scope)\n await seedSalesAdjustmentKinds(em, scope)\n await ensureExampleShippingMethods(em, scope)\n await ensureExamplePaymentMethods(em, scope)\n await seedSalesChannelsToggle(em)\n },\n\n async seedExamples({ em, container, tenantId, organizationId }) {\n const scope = { tenantId, organizationId }\n await seedSalesExamples(em, container, scope)\n },\n}\n\nexport default setup\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { SalesSettings, SalesDocumentSequence, SalesTaxRate } from './data/entities'\nimport { DEFAULT_ORDER_NUMBER_FORMAT, DEFAULT_QUOTE_NUMBER_FORMAT } from './lib/documentNumberTokens'\nimport { seedSalesStatusDictionaries, seedSalesAdjustmentKinds } from './lib/dictionaries'\nimport { seedSalesChannelsToggle } from './lib/salesChannelsToggleSeed'\nimport { ensureExampleShippingMethods, ensureExamplePaymentMethods } from './seed/examples-data'\nimport { seedSalesExamples } from './seed/examples'\nimport { createDocumentSequence } from './services/salesDocumentNumberGenerator'\n\ntype SeedScope = { tenantId: string; organizationId: string }\n\nconst DEFAULT_TAX_RATES = [\n { code: 'vat-23', name: '23% VAT', rate: '23' },\n { code: 'vat-0', name: '0% VAT', rate: '0' },\n] as const\n\nasync function seedSalesTaxRates(em: EntityManager, scope: SeedScope): Promise<void> {\n await em.transactional(async (tem) => {\n const existing = await tem.find(SalesTaxRate, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n })\n const existingCodes = new Set(existing.map((rate) => rate.code))\n const hasDefault = existing.some((rate) => rate.isDefault)\n const now = new Date()\n let isFirst = !hasDefault\n\n for (const seed of DEFAULT_TAX_RATES) {\n if (existingCodes.has(seed.code)) continue\n tem.persist(\n tem.create(SalesTaxRate, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n code: seed.code,\n name: seed.name,\n rate: seed.rate,\n priority: 0,\n isCompound: false,\n isDefault: isFirst,\n createdAt: now,\n updatedAt: now,\n })\n )\n isFirst = false\n }\n })\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: ['sales.*', 'sales.documents.number.edit'],\n employee: [\n 'sales.channels.view',\n 'sales.channels.manage',\n 'sales.settings.view',\n 'sales.settings.manage',\n 'sales.orders.view',\n 'sales.orders.manage',\n 'sales.orders.approve',\n 'sales.widgets.new-orders',\n 'sales.widgets.new-quotes',\n 'sales.quotes.view',\n 'sales.quotes.manage',\n 'sales.shipments.manage',\n 'sales.payments.manage',\n 'sales.returns.view',\n 'sales.returns.create',\n 'sales.returns.manage',\n 'sales.invoices.manage',\n 'sales.credit_memos.manage',\n ],\n },\n\n async onTenantCreated({ em, tenantId, organizationId }) {\n const exists = await em.findOne(SalesSettings, { tenantId, organizationId })\n if (!exists) {\n em.persist(\n em.create(SalesSettings, {\n tenantId,\n organizationId,\n orderNumberFormat: DEFAULT_ORDER_NUMBER_FORMAT,\n quoteNumberFormat: DEFAULT_QUOTE_NUMBER_FORMAT,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n )\n }\n\n const sequenceRows: SalesDocumentSequence[] = []\n for (const kind of ['order', 'quote', 'return', 'invoice', 'credit_memo'] as const) {\n const seq = await em.findOne(SalesDocumentSequence, {\n tenantId,\n organizationId,\n documentKind: kind,\n })\n sequenceRows.push(\n seq ??\n em.create(SalesDocumentSequence, {\n tenantId,\n organizationId,\n documentKind: kind,\n currentValue: 0,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n )\n if (!seq) em.persist(sequenceRows[sequenceRows.length - 1]!)\n }\n\n await em.flush()\n\n // Each registry row is backed by its own Postgres sequence (#5604); create them now so the\n // first document of a fresh tenant does not have to fall back to lazy creation. The ids come\n // from the rows just flushed rather than from a re-read: `createDocumentSequence` issues DDL\n // over `em.getConnection()`, which runs outside the EntityManager's transaction context, so\n // a read on that connection would not see rows this hook wrote inside a transaction and the\n // loop would silently create nothing.\n for (const row of sequenceRows) {\n await createDocumentSequence(em, row.id)\n }\n },\n\n async seedDefaults({ em, tenantId, organizationId }) {\n const scope = { tenantId, organizationId }\n await seedSalesTaxRates(em, scope)\n await seedSalesStatusDictionaries(em, scope)\n await seedSalesAdjustmentKinds(em, scope)\n await ensureExampleShippingMethods(em, scope)\n await ensureExamplePaymentMethods(em, scope)\n await seedSalesChannelsToggle(em)\n },\n\n async seedExamples({ em, container, tenantId, organizationId }) {\n const scope = { tenantId, organizationId }\n await seedSalesExamples(em, container, scope)\n },\n}\n\nexport default setup\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.8.1-develop.7239.1.d90c352076",
3
+ "version": "0.8.1-develop.7241.1.fad7b2ce2a",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -253,16 +253,16 @@
253
253
  "zod": "^4.4.3"
254
254
  },
255
255
  "peerDependencies": {
256
- "@open-mercato/ai-assistant": "0.8.1-develop.7239.1.d90c352076",
257
- "@open-mercato/shared": "0.8.1-develop.7239.1.d90c352076",
258
- "@open-mercato/ui": "0.8.1-develop.7239.1.d90c352076",
256
+ "@open-mercato/ai-assistant": "0.8.1-develop.7241.1.fad7b2ce2a",
257
+ "@open-mercato/shared": "0.8.1-develop.7241.1.fad7b2ce2a",
258
+ "@open-mercato/ui": "0.8.1-develop.7241.1.fad7b2ce2a",
259
259
  "react": "^19.0.0",
260
260
  "react-dom": "^19.0.0"
261
261
  },
262
262
  "devDependencies": {
263
- "@open-mercato/ai-assistant": "0.8.1-develop.7239.1.d90c352076",
264
- "@open-mercato/shared": "0.8.1-develop.7239.1.d90c352076",
265
- "@open-mercato/ui": "0.8.1-develop.7239.1.d90c352076",
263
+ "@open-mercato/ai-assistant": "0.8.1-develop.7241.1.fad7b2ce2a",
264
+ "@open-mercato/shared": "0.8.1-develop.7241.1.fad7b2ce2a",
265
+ "@open-mercato/ui": "0.8.1-develop.7241.1.fad7b2ce2a",
266
266
  "@testing-library/dom": "^10.4.1",
267
267
  "@testing-library/jest-dom": "^7.0.1",
268
268
  "@testing-library/react": "^16.3.3",
@@ -40,6 +40,16 @@ function resolveDatabaseUrl(): string {
40
40
  return url;
41
41
  }
42
42
 
43
+ /**
44
+ * The same connection string `withClient` uses, for specs that need their own pool rather
45
+ * than a single `pg` client — a spec driving server-side library code that expects a Kysely
46
+ * instance, for example. Reuses the `.env` fallback so those specs resolve the database the
47
+ * same way every other fixture here does.
48
+ */
49
+ export function resolveIntegrationDatabaseUrl(): string {
50
+ return resolveDatabaseUrl();
51
+ }
52
+
43
53
  /**
44
54
  * The slice of `pg`'s client these fixtures use, declared structurally.
45
55
  *
@@ -7,6 +7,7 @@ export const metadata: ModuleInfo = {
7
7
  description: 'Core CRM capabilities for people, companies, deals, and activities.',
8
8
  author: 'Open Mercato Team',
9
9
  license: 'MIT',
10
+ requires: ['progress'],
10
11
  ejectable: true,
11
12
  }
12
13