@open-mercato/core 0.6.7-develop.6788.1.c2a1520a30 → 0.6.7-develop.6795.1.8a3f27921c

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 (190) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities.ids.generated.js +1 -0
  3. package/dist/generated/entities.ids.generated.js.map +2 -2
  4. package/dist/modules/customers/cli.js +17 -7
  5. package/dist/modules/customers/cli.js.map +2 -2
  6. package/dist/modules/customers/search.js +50 -29
  7. package/dist/modules/customers/search.js.map +2 -2
  8. package/dist/modules/data_sync/api/mappings/[id]/route.js +22 -9
  9. package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
  10. package/dist/modules/data_sync/api/mappings/route.js +13 -4
  11. package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
  12. package/dist/modules/data_sync/api/options.js +7 -2
  13. package/dist/modules/data_sync/api/options.js.map +2 -2
  14. package/dist/modules/data_sync/api/run.js +7 -2
  15. package/dist/modules/data_sync/api/run.js.map +2 -2
  16. package/dist/modules/data_sync/api/runs/[id]/cancel.js +8 -3
  17. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  18. package/dist/modules/data_sync/api/runs/[id]/retry.js +7 -2
  19. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  20. package/dist/modules/data_sync/api/runs/[id]/route.js +8 -3
  21. package/dist/modules/data_sync/api/runs/[id]/route.js.map +2 -2
  22. package/dist/modules/data_sync/api/runs.js +7 -2
  23. package/dist/modules/data_sync/api/runs.js.map +2 -2
  24. package/dist/modules/data_sync/api/schedules/[id]/route.js +19 -6
  25. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
  26. package/dist/modules/data_sync/api/schedules/route.js +13 -4
  27. package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
  28. package/dist/modules/data_sync/api/validate.js +9 -4
  29. package/dist/modules/data_sync/api/validate.js.map +2 -2
  30. package/dist/modules/design_system/acl.js +9 -0
  31. package/dist/modules/design_system/acl.js.map +7 -0
  32. package/dist/modules/design_system/backend/design-system/page.js +11 -0
  33. package/dist/modules/design_system/backend/design-system/page.js.map +7 -0
  34. package/dist/modules/design_system/backend/design-system/page.meta.js +15 -0
  35. package/dist/modules/design_system/backend/design-system/page.meta.js.map +7 -0
  36. package/dist/modules/design_system/gallery/components/CodeSnippet.js +52 -0
  37. package/dist/modules/design_system/gallery/components/CodeSnippet.js.map +7 -0
  38. package/dist/modules/design_system/gallery/components/EntryCard.js +63 -0
  39. package/dist/modules/design_system/gallery/components/EntryCard.js.map +7 -0
  40. package/dist/modules/design_system/gallery/components/GalleryShell.js +163 -0
  41. package/dist/modules/design_system/gallery/components/GalleryShell.js.map +7 -0
  42. package/dist/modules/design_system/gallery/components/VariantPreview.js +9 -0
  43. package/dist/modules/design_system/gallery/components/VariantPreview.js.map +7 -0
  44. package/dist/modules/design_system/gallery/entries/banners.js +186 -0
  45. package/dist/modules/design_system/gallery/entries/banners.js.map +7 -0
  46. package/dist/modules/design_system/gallery/entries/buttons.js +361 -0
  47. package/dist/modules/design_system/gallery/entries/buttons.js.map +7 -0
  48. package/dist/modules/design_system/gallery/entries/charts.js +457 -0
  49. package/dist/modules/design_system/gallery/entries/charts.js.map +7 -0
  50. package/dist/modules/design_system/gallery/entries/dates.js +306 -0
  51. package/dist/modules/design_system/gallery/entries/dates.js.map +7 -0
  52. package/dist/modules/design_system/gallery/entries/detail.js +358 -0
  53. package/dist/modules/design_system/gallery/entries/detail.js.map +7 -0
  54. package/dist/modules/design_system/gallery/entries/display.js +785 -0
  55. package/dist/modules/design_system/gallery/entries/display.js.map +7 -0
  56. package/dist/modules/design_system/gallery/entries/feedback.js +658 -0
  57. package/dist/modules/design_system/gallery/entries/feedback.js.map +7 -0
  58. package/dist/modules/design_system/gallery/entries/filters.js +505 -0
  59. package/dist/modules/design_system/gallery/entries/filters.js.map +7 -0
  60. package/dist/modules/design_system/gallery/entries/foundations.js +453 -0
  61. package/dist/modules/design_system/gallery/entries/foundations.js.map +7 -0
  62. package/dist/modules/design_system/gallery/entries/icons.js +140 -0
  63. package/dist/modules/design_system/gallery/entries/icons.js.map +7 -0
  64. package/dist/modules/design_system/gallery/entries/inputs.js +1217 -0
  65. package/dist/modules/design_system/gallery/entries/inputs.js.map +7 -0
  66. package/dist/modules/design_system/gallery/entries/messages.js +334 -0
  67. package/dist/modules/design_system/gallery/entries/messages.js.map +7 -0
  68. package/dist/modules/design_system/gallery/entries/navigation.js +475 -0
  69. package/dist/modules/design_system/gallery/entries/navigation.js.map +7 -0
  70. package/dist/modules/design_system/gallery/entries/notifications.js +257 -0
  71. package/dist/modules/design_system/gallery/entries/notifications.js.map +7 -0
  72. package/dist/modules/design_system/gallery/entries/overlays.js +576 -0
  73. package/dist/modules/design_system/gallery/entries/overlays.js.map +7 -0
  74. package/dist/modules/design_system/gallery/entries/scaffolding.js +378 -0
  75. package/dist/modules/design_system/gallery/entries/scaffolding.js.map +7 -0
  76. package/dist/modules/design_system/gallery/entries/schedule.js +297 -0
  77. package/dist/modules/design_system/gallery/entries/schedule.js.map +7 -0
  78. package/dist/modules/design_system/gallery/registry.js +139 -0
  79. package/dist/modules/design_system/gallery/registry.js.map +7 -0
  80. package/dist/modules/design_system/gallery/types.js +1 -0
  81. package/dist/modules/design_system/gallery/types.js.map +7 -0
  82. package/dist/modules/design_system/index.js +9 -0
  83. package/dist/modules/design_system/index.js.map +7 -0
  84. package/dist/modules/design_system/setup.js +14 -0
  85. package/dist/modules/design_system/setup.js.map +7 -0
  86. package/dist/modules/inbox_ops/search.js +15 -4
  87. package/dist/modules/inbox_ops/search.js.map +2 -2
  88. package/dist/modules/integrations/api/[id]/credentials/route.js +11 -5
  89. package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
  90. package/dist/modules/integrations/api/[id]/health/route.js +6 -3
  91. package/dist/modules/integrations/api/[id]/health/route.js.map +2 -2
  92. package/dist/modules/integrations/api/[id]/route.js +6 -3
  93. package/dist/modules/integrations/api/[id]/route.js.map +2 -2
  94. package/dist/modules/integrations/api/[id]/state/route.js +6 -3
  95. package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
  96. package/dist/modules/integrations/api/[id]/version/route.js +6 -3
  97. package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
  98. package/dist/modules/integrations/api/logs/route.js +6 -3
  99. package/dist/modules/integrations/api/logs/route.js.map +2 -2
  100. package/dist/modules/integrations/api/route.js +6 -3
  101. package/dist/modules/integrations/api/route.js.map +2 -2
  102. package/dist/modules/integrations/api/umes-read.js +2 -2
  103. package/dist/modules/integrations/api/umes-read.js.map +2 -2
  104. package/dist/modules/integrations/lib/organization-scope.js +2 -9
  105. package/dist/modules/integrations/lib/organization-scope.js.map +2 -2
  106. package/dist/modules/messages/search.js +10 -5
  107. package/dist/modules/messages/search.js.map +2 -2
  108. package/dist/modules/query_index/data/entities.js +2 -1
  109. package/dist/modules/query_index/data/entities.js.map +2 -2
  110. package/dist/modules/query_index/lib/engine.js +32 -50
  111. package/dist/modules/query_index/lib/engine.js.map +2 -2
  112. package/dist/modules/query_index/migrations/Migration20260731105052_query_index.js +17 -0
  113. package/dist/modules/query_index/migrations/Migration20260731105052_query_index.js.map +7 -0
  114. package/generated/entities.ids.generated.ts +1 -0
  115. package/package.json +16 -16
  116. package/src/modules/customers/cli.ts +17 -7
  117. package/src/modules/customers/i18n/de.json +17 -0
  118. package/src/modules/customers/i18n/en.json +17 -0
  119. package/src/modules/customers/i18n/es.json +17 -0
  120. package/src/modules/customers/i18n/pl.json +17 -0
  121. package/src/modules/customers/search.ts +59 -27
  122. package/src/modules/data_sync/AGENTS.md +1 -0
  123. package/src/modules/data_sync/api/mappings/[id]/route.ts +22 -9
  124. package/src/modules/data_sync/api/mappings/route.ts +13 -4
  125. package/src/modules/data_sync/api/options.ts +7 -2
  126. package/src/modules/data_sync/api/run.ts +7 -2
  127. package/src/modules/data_sync/api/runs/[id]/cancel.ts +8 -3
  128. package/src/modules/data_sync/api/runs/[id]/retry.ts +7 -2
  129. package/src/modules/data_sync/api/runs/[id]/route.ts +8 -3
  130. package/src/modules/data_sync/api/runs.ts +7 -2
  131. package/src/modules/data_sync/api/schedules/[id]/route.ts +19 -6
  132. package/src/modules/data_sync/api/schedules/route.ts +13 -4
  133. package/src/modules/data_sync/api/validate.ts +9 -4
  134. package/src/modules/design_system/AGENTS.md +34 -0
  135. package/src/modules/design_system/acl.ts +5 -0
  136. package/src/modules/design_system/backend/design-system/page.meta.ts +11 -0
  137. package/src/modules/design_system/backend/design-system/page.tsx +14 -0
  138. package/src/modules/design_system/gallery/components/CodeSnippet.tsx +55 -0
  139. package/src/modules/design_system/gallery/components/EntryCard.tsx +95 -0
  140. package/src/modules/design_system/gallery/components/GalleryShell.tsx +206 -0
  141. package/src/modules/design_system/gallery/components/VariantPreview.tsx +15 -0
  142. package/src/modules/design_system/gallery/entries/banners.tsx +226 -0
  143. package/src/modules/design_system/gallery/entries/buttons.tsx +422 -0
  144. package/src/modules/design_system/gallery/entries/charts.tsx +518 -0
  145. package/src/modules/design_system/gallery/entries/dates.tsx +325 -0
  146. package/src/modules/design_system/gallery/entries/detail.tsx +403 -0
  147. package/src/modules/design_system/gallery/entries/display.tsx +893 -0
  148. package/src/modules/design_system/gallery/entries/feedback.tsx +746 -0
  149. package/src/modules/design_system/gallery/entries/filters.tsx +570 -0
  150. package/src/modules/design_system/gallery/entries/foundations.tsx +529 -0
  151. package/src/modules/design_system/gallery/entries/icons.tsx +168 -0
  152. package/src/modules/design_system/gallery/entries/inputs.tsx +1516 -0
  153. package/src/modules/design_system/gallery/entries/messages.tsx +339 -0
  154. package/src/modules/design_system/gallery/entries/navigation.tsx +573 -0
  155. package/src/modules/design_system/gallery/entries/notifications.tsx +285 -0
  156. package/src/modules/design_system/gallery/entries/overlays.tsx +677 -0
  157. package/src/modules/design_system/gallery/entries/scaffolding.tsx +430 -0
  158. package/src/modules/design_system/gallery/entries/schedule.tsx +307 -0
  159. package/src/modules/design_system/gallery/registry.ts +152 -0
  160. package/src/modules/design_system/gallery/types.ts +32 -0
  161. package/src/modules/design_system/i18n/.hardcoded-allowlist.json +71 -0
  162. package/src/modules/design_system/i18n/de.json +46 -0
  163. package/src/modules/design_system/i18n/en.json +46 -0
  164. package/src/modules/design_system/i18n/es.json +46 -0
  165. package/src/modules/design_system/i18n/pl.json +46 -0
  166. package/src/modules/design_system/index.ts +7 -0
  167. package/src/modules/design_system/setup.ts +12 -0
  168. package/src/modules/inbox_ops/i18n/de.json +3 -0
  169. package/src/modules/inbox_ops/i18n/en.json +3 -0
  170. package/src/modules/inbox_ops/i18n/es.json +3 -0
  171. package/src/modules/inbox_ops/i18n/pl.json +3 -0
  172. package/src/modules/inbox_ops/search.ts +20 -4
  173. package/src/modules/integrations/api/[id]/credentials/route.ts +11 -5
  174. package/src/modules/integrations/api/[id]/health/route.ts +6 -3
  175. package/src/modules/integrations/api/[id]/route.ts +6 -3
  176. package/src/modules/integrations/api/[id]/state/route.ts +6 -3
  177. package/src/modules/integrations/api/[id]/version/route.ts +6 -3
  178. package/src/modules/integrations/api/logs/route.ts +6 -3
  179. package/src/modules/integrations/api/route.ts +6 -3
  180. package/src/modules/integrations/api/umes-read.ts +2 -2
  181. package/src/modules/integrations/lib/organization-scope.ts +5 -25
  182. package/src/modules/messages/i18n/de.json +2 -0
  183. package/src/modules/messages/i18n/en.json +2 -0
  184. package/src/modules/messages/i18n/es.json +2 -0
  185. package/src/modules/messages/i18n/pl.json +2 -0
  186. package/src/modules/messages/search.ts +11 -5
  187. package/src/modules/query_index/data/entities.ts +7 -0
  188. package/src/modules/query_index/lib/engine.ts +40 -63
  189. package/src/modules/query_index/migrations/.snapshot-open-mercato.json +12 -0
  190. package/src/modules/query_index/migrations/Migration20260731105052_query_index.ts +35 -0
@@ -0,0 +1,17 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20260731105052_query_index extends Migration {
3
+ isTransactional() {
4
+ return false;
5
+ }
6
+ up() {
7
+ this.addSql(`drop index concurrently if exists "search_tokens_presence_idx";`);
8
+ this.addSql(`create index concurrently "search_tokens_presence_idx" on "search_tokens" ("entity_type", "tenant_id", "organization_id");`);
9
+ }
10
+ down() {
11
+ this.addSql(`drop index concurrently if exists "search_tokens_presence_idx";`);
12
+ }
13
+ }
14
+ export {
15
+ Migration20260731105052_query_index
16
+ };
17
+ //# sourceMappingURL=Migration20260731105052_query_index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/query_index/migrations/Migration20260731105052_query_index.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\n// Availability-probe index (.ai/specs/2026-07-31-search-token-probe-index.md).\n// The query engines gate like/ilike routing on \"does (entity_type, tenant, org)\n// have any search tokens?\". Neither existing index serves that probe as an\n// access path \u2014 lookup_idx buries tenant_id behind field/token_hash \u2014 so on a\n// large search_tokens the planner degraded the probe to a seq scan and the MISS\n// answer had to exhaust every entry for the entity type (p95 12.5 s in\n// production, #4723). With this (entity_type, tenant_id, organization_id)\n// prefix and the index-usable `= / IS NULL` predicates the availability\n// resolver emits, both the hit AND the miss become a B-tree prefix seek.\n//\n// search_tokens is high-churn, so the index is built CONCURRENTLY to avoid\n// blocking writes during the build. CREATE INDEX CONCURRENTLY cannot run\n// inside a transaction, hence isTransactional() => false; the migration runner\n// applies migrations one-by-one, so this opt-out is safe (same pattern as\n// Migration20260611103000_query_index). Drop first so retrying a failed\n// concurrent build removes PostgreSQL's invalid index stub instead of letting\n// IF NOT EXISTS silently accept it.\nexport class Migration20260731105052_query_index extends Migration {\n\n override isTransactional(): boolean {\n return false;\n }\n\n override up(): void | Promise<void> {\n this.addSql(`drop index concurrently if exists \"search_tokens_presence_idx\";`);\n this.addSql(`create index concurrently \"search_tokens_presence_idx\" on \"search_tokens\" (\"entity_type\", \"tenant_id\", \"organization_id\");`);\n }\n\n override down(): void | Promise<void> {\n this.addSql(`drop index concurrently if exists \"search_tokens_presence_idx\";`);\n }\n\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAmBnB,MAAM,4CAA4C,UAAU;AAAA,EAExD,kBAA2B;AAClC,WAAO;AAAA,EACT;AAAA,EAES,KAA2B;AAClC,SAAK,OAAO,iEAAiE;AAC7E,SAAK,OAAO,4HAA4H;AAAA,EAC1I;AAAA,EAES,OAA6B;AACpC,SAAK,OAAO,iEAAiE;AAAA,EAC/E;AAEF;",
6
+ "names": []
7
+ }
@@ -16,6 +16,7 @@ export const M = {
16
16
  "api_keys": "api_keys",
17
17
  "dictionaries": "dictionaries",
18
18
  "api_docs": "api_docs",
19
+ "design_system": "design_system",
19
20
  "business_rules": "business_rules",
20
21
  "feature_toggles": "feature_toggles",
21
22
  "workflows": "workflows",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.7-develop.6788.1.c2a1520a30",
3
+ "version": "0.6.7-develop.6795.1.8a3f27921c",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -228,9 +228,9 @@
228
228
  }
229
229
  },
230
230
  "dependencies": {
231
- "@mikro-orm/core": "^7.1.5",
232
- "@mikro-orm/decorators": "^7.1.5",
233
- "@mikro-orm/postgresql": "^7.1.5",
231
+ "@mikro-orm/core": "^7.1.8",
232
+ "@mikro-orm/decorators": "^7.1.8",
233
+ "@mikro-orm/postgresql": "^7.1.8",
234
234
  "@types/html-to-text": "^9.0.4",
235
235
  "@types/leaflet": "^1.9.12",
236
236
  "@types/leaflet.markercluster": "^1.5.5",
@@ -245,25 +245,25 @@
245
245
  "leaflet.markercluster": "^1.5.3",
246
246
  "mammoth": "^1.9.0",
247
247
  "pdfjs-dist": "^6.1.200",
248
- "resend": "^6.17.2",
249
- "sanitize-html": "^2.17.5",
248
+ "resend": "^6.18.1",
249
+ "sanitize-html": "2.17.5",
250
250
  "semver": "^7.8.5",
251
251
  "sharp": "^0.35.3",
252
- "svix": "^1.96.1",
252
+ "svix": "^1.99.1",
253
253
  "ts-pattern": "^5.0.0",
254
254
  "zod": "^4.4.3"
255
255
  },
256
256
  "peerDependencies": {
257
- "@open-mercato/ai-assistant": "0.6.7-develop.6788.1.c2a1520a30",
258
- "@open-mercato/shared": "0.6.7-develop.6788.1.c2a1520a30",
259
- "@open-mercato/ui": "0.6.7-develop.6788.1.c2a1520a30",
257
+ "@open-mercato/ai-assistant": "0.6.7-develop.6795.1.8a3f27921c",
258
+ "@open-mercato/shared": "0.6.7-develop.6795.1.8a3f27921c",
259
+ "@open-mercato/ui": "0.6.7-develop.6795.1.8a3f27921c",
260
260
  "react": "^19.0.0",
261
261
  "react-dom": "^19.0.0"
262
262
  },
263
263
  "devDependencies": {
264
- "@open-mercato/ai-assistant": "0.6.7-develop.6788.1.c2a1520a30",
265
- "@open-mercato/shared": "0.6.7-develop.6788.1.c2a1520a30",
266
- "@open-mercato/ui": "0.6.7-develop.6788.1.c2a1520a30",
264
+ "@open-mercato/ai-assistant": "0.6.7-develop.6795.1.8a3f27921c",
265
+ "@open-mercato/shared": "0.6.7-develop.6795.1.8a3f27921c",
266
+ "@open-mercato/ui": "0.6.7-develop.6795.1.8a3f27921c",
267
267
  "@testing-library/dom": "^10.4.1",
268
268
  "@testing-library/jest-dom": "^6.9.1",
269
269
  "@testing-library/react": "^16.3.1",
@@ -274,9 +274,9 @@
274
274
  "chance": "^1.1.13",
275
275
  "jest": "^30.4.2",
276
276
  "jest-environment-jsdom": "^30.4.1",
277
- "react": "19.2.7",
278
- "react-dom": "19.2.7",
279
- "ts-jest": "^29.4.11",
277
+ "react": "19.2.8",
278
+ "react-dom": "19.2.8",
279
+ "ts-jest": "^29.4.12",
280
280
  "typescript": "7.0.2"
281
281
  },
282
282
  "publishConfig": {
@@ -12,6 +12,7 @@ import { E as CoreEntities } from '#generated/entities.ids.generated'
12
12
  import { createProgressBar } from '@open-mercato/shared/lib/cli/progress'
13
13
  import { buildIndexDocument, type IndexCustomFieldValue } from '@open-mercato/core/modules/query_index/lib/document'
14
14
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
15
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
15
16
  import { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'
16
17
  import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
17
18
  import type { EntityId } from '@open-mercato/shared/modules/entities'
@@ -1396,13 +1397,22 @@ async function seedCustomerExamples(
1396
1397
  )
1397
1398
  )
1398
1399
  if (exampleDealTitles.length > 0) {
1399
- const already = await em.count(CustomerDeal, {
1400
- tenantId,
1401
- organizationId,
1402
- title: { $in: exampleDealTitles as any },
1403
- })
1404
- if (already > 0) {
1405
- return false
1400
+ const exampleTitles = new Set(exampleDealTitles)
1401
+ const batchSize = 100
1402
+ let offset = 0
1403
+ while (true) {
1404
+ const existingDeals = await findWithDecryption(
1405
+ em,
1406
+ CustomerDeal,
1407
+ { tenantId, organizationId },
1408
+ { fields: ['title'], limit: batchSize, offset, orderBy: { id: 'asc' } },
1409
+ { tenantId, organizationId },
1410
+ )
1411
+ if (existingDeals.some((deal) => typeof deal.title === 'string' && exampleTitles.has(deal.title))) {
1412
+ return false
1413
+ }
1414
+ if (existingDeals.length < batchSize) break
1415
+ offset += existingDeals.length
1406
1416
  }
1407
1417
  }
1408
1418
 
@@ -2651,6 +2651,23 @@
2651
2651
  "customers.schedule.visibility": "Visibility",
2652
2652
  "customers.schedule.visibility.public": "Public",
2653
2653
  "customers.schedule.visibility.team": "Team only",
2654
+ "customers.search.badge.activity": "Aktivität",
2655
+ "customers.search.badge.company": "Unternehmen",
2656
+ "customers.search.badge.deal": "Deal",
2657
+ "customers.search.badge.person": "Person",
2658
+ "customers.search.fallback.activity": "Aktivität: {{type}}",
2659
+ "customers.search.fallback.activityUpdate": "Aktualisierung",
2660
+ "customers.search.fallback.company": "Unternehmen",
2661
+ "customers.search.fallback.customerNote": "Kundennotiz",
2662
+ "customers.search.fallback.customerTask": "Kundenaufgabe",
2663
+ "customers.search.fallback.deal": "Deal",
2664
+ "customers.search.fallback.person": "Person",
2665
+ "customers.search.link.edit": "Bearbeiten",
2666
+ "customers.search.link.openCompany": "Unternehmen öffnen",
2667
+ "customers.search.link.openDeal": "Deal öffnen",
2668
+ "customers.search.link.openPerson": "Person öffnen",
2669
+ "customers.search.link.openTodo": "Aufgabe öffnen",
2670
+ "customers.search.link.viewCustomer": "Kunde anzeigen",
2654
2671
  "customers.tags.manage.addCategory": "Neue Kategorie",
2655
2672
  "customers.tags.manage.addCategoryPlaceholder": "Kategoriename...",
2656
2673
  "customers.tags.manage.addCategoryRequired": "Geben Sie zuerst einen Kategorienamen ein.",
@@ -2651,6 +2651,23 @@
2651
2651
  "customers.schedule.visibility": "Visibility",
2652
2652
  "customers.schedule.visibility.public": "Public",
2653
2653
  "customers.schedule.visibility.team": "Team only",
2654
+ "customers.search.badge.activity": "Activity",
2655
+ "customers.search.badge.company": "Company",
2656
+ "customers.search.badge.deal": "Deal",
2657
+ "customers.search.badge.person": "Person",
2658
+ "customers.search.fallback.activity": "Activity: {{type}}",
2659
+ "customers.search.fallback.activityUpdate": "update",
2660
+ "customers.search.fallback.company": "Company",
2661
+ "customers.search.fallback.customerNote": "Customer note",
2662
+ "customers.search.fallback.customerTask": "Customer task",
2663
+ "customers.search.fallback.deal": "Deal",
2664
+ "customers.search.fallback.person": "Person",
2665
+ "customers.search.link.edit": "Edit",
2666
+ "customers.search.link.openCompany": "Open company",
2667
+ "customers.search.link.openDeal": "Open deal",
2668
+ "customers.search.link.openPerson": "Open person",
2669
+ "customers.search.link.openTodo": "Open todo",
2670
+ "customers.search.link.viewCustomer": "View customer",
2654
2671
  "customers.tags.manage.addCategory": "New category",
2655
2672
  "customers.tags.manage.addCategoryPlaceholder": "Category name...",
2656
2673
  "customers.tags.manage.addCategoryRequired": "Enter a category name first.",
@@ -2651,6 +2651,23 @@
2651
2651
  "customers.schedule.visibility": "Visibility",
2652
2652
  "customers.schedule.visibility.public": "Public",
2653
2653
  "customers.schedule.visibility.team": "Team only",
2654
+ "customers.search.badge.activity": "Actividad",
2655
+ "customers.search.badge.company": "Empresa",
2656
+ "customers.search.badge.deal": "Oportunidad",
2657
+ "customers.search.badge.person": "Persona",
2658
+ "customers.search.fallback.activity": "Actividad: {{type}}",
2659
+ "customers.search.fallback.activityUpdate": "actualización",
2660
+ "customers.search.fallback.company": "Empresa",
2661
+ "customers.search.fallback.customerNote": "Nota del cliente",
2662
+ "customers.search.fallback.customerTask": "Tarea del cliente",
2663
+ "customers.search.fallback.deal": "Oportunidad",
2664
+ "customers.search.fallback.person": "Persona",
2665
+ "customers.search.link.edit": "Editar",
2666
+ "customers.search.link.openCompany": "Abrir empresa",
2667
+ "customers.search.link.openDeal": "Abrir oportunidad",
2668
+ "customers.search.link.openPerson": "Abrir persona",
2669
+ "customers.search.link.openTodo": "Abrir tarea",
2670
+ "customers.search.link.viewCustomer": "Ver cliente",
2654
2671
  "customers.tags.manage.addCategory": "Nueva categoría",
2655
2672
  "customers.tags.manage.addCategoryPlaceholder": "Nombre de la categoría...",
2656
2673
  "customers.tags.manage.addCategoryRequired": "Primero introduce un nombre de categoría.",
@@ -2651,6 +2651,23 @@
2651
2651
  "customers.schedule.visibility": "Widoczność",
2652
2652
  "customers.schedule.visibility.public": "Publiczny",
2653
2653
  "customers.schedule.visibility.team": "Tylko zespół",
2654
+ "customers.search.badge.activity": "Aktywność",
2655
+ "customers.search.badge.company": "Firma",
2656
+ "customers.search.badge.deal": "Szansa sprzedaży",
2657
+ "customers.search.badge.person": "Osoba",
2658
+ "customers.search.fallback.activity": "Aktywność: {{type}}",
2659
+ "customers.search.fallback.activityUpdate": "aktualizacja",
2660
+ "customers.search.fallback.company": "Firma",
2661
+ "customers.search.fallback.customerNote": "Notatka klienta",
2662
+ "customers.search.fallback.customerTask": "Zadanie klienta",
2663
+ "customers.search.fallback.deal": "Szansa sprzedaży",
2664
+ "customers.search.fallback.person": "Osoba",
2665
+ "customers.search.link.edit": "Edytuj",
2666
+ "customers.search.link.openCompany": "Otwórz firmę",
2667
+ "customers.search.link.openDeal": "Otwórz szansę",
2668
+ "customers.search.link.openPerson": "Otwórz osobę",
2669
+ "customers.search.link.openTodo": "Otwórz zadanie",
2670
+ "customers.search.link.viewCustomer": "Zobacz klienta",
2654
2671
  "customers.tags.manage.addCategory": "Nowa kategoria",
2655
2672
  "customers.tags.manage.addCategoryPlaceholder": "Nazwa kategorii...",
2656
2673
  "customers.tags.manage.addCategoryRequired": "Najpierw wpisz nazwę kategorii.",
@@ -6,6 +6,8 @@ import type {
6
6
  SearchResultLink,
7
7
  SearchIndexSource,
8
8
  } from '@open-mercato/shared/modules/search'
9
+ import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
10
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
9
11
  import { CUSTOMER_INTERACTION_TASK_SOURCE, EXAMPLE_TODO_SOURCE } from './lib/interactionCompatibility'
10
12
  import { createLogger } from '@open-mercato/shared/lib/logger'
11
13
 
@@ -480,6 +482,7 @@ function ensureFallbackLines(lines: string[], record: Record<string, unknown>, o
480
482
  // =============================================================================
481
483
 
482
484
  function resolvePersonPresenter(
485
+ translate: TranslateFn,
483
486
  record: Record<string, unknown>,
484
487
  entity: Record<string, unknown> | null,
485
488
  customFields: Record<string, unknown>,
@@ -495,7 +498,7 @@ function resolvePersonPresenter(
495
498
  (nameParts.length ? nameParts : undefined) ??
496
499
  fallbackEntityId ??
497
500
  (record.id as string | undefined) ??
498
- 'Person'
501
+ translate('customers.search.fallback.person', 'Person')
499
502
  const subtitlePieces: string[] = []
500
503
  const jobTitle = record.job_title ?? record.jobTitle ?? customFields.job_title ?? customFields.jobTitle
501
504
  if (jobTitle) subtitlePieces.push(String(jobTitle))
@@ -515,11 +518,14 @@ function resolvePersonPresenter(
515
518
  title: String(title),
516
519
  subtitle: subtitlePieces.length ? subtitlePieces.join(' · ') : undefined,
517
520
  icon: 'user',
518
- badge: pickValue(entity, 'display_name', 'displayName') ? 'Person' : undefined,
521
+ badge: pickValue(entity, 'display_name', 'displayName')
522
+ ? translate('customers.search.badge.person', 'Person')
523
+ : undefined,
519
524
  }
520
525
  }
521
526
 
522
527
  function resolveCompanyPresenter(
528
+ translate: TranslateFn,
523
529
  record: Record<string, unknown>,
524
530
  entity: Record<string, unknown> | null,
525
531
  customFields: Record<string, unknown>,
@@ -537,7 +543,7 @@ function resolveCompanyPresenter(
537
543
  (entity?.id && entity?.display_name ? entity.display_name as string : undefined) ??
538
544
  fallbackEntityId ??
539
545
  (record.id as string | undefined) ??
540
- 'Company'
546
+ translate('customers.search.fallback.company', 'Company')
541
547
  const subtitlePieces: string[] = []
542
548
  const industry = record.industry
543
549
  if (industry) subtitlePieces.push(String(industry))
@@ -567,7 +573,9 @@ function resolveCompanyPresenter(
567
573
  title: String(title),
568
574
  subtitle: subtitlePieces.length ? subtitlePieces.join(' · ') : undefined,
569
575
  icon: 'building',
570
- badge: pickValue(entity, 'display_name', 'displayName') ? 'Company' : undefined,
576
+ badge: pickValue(entity, 'display_name', 'displayName')
577
+ ? translate('customers.search.badge.company', 'Company')
578
+ : undefined,
571
579
  }
572
580
  }
573
581
 
@@ -605,6 +613,7 @@ export const searchConfig: SearchModuleConfig = {
605
613
 
606
614
  buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {
607
615
  assertTenantContext(ctx)
616
+ const { t } = await resolveTranslations()
608
617
  const lines: string[] = []
609
618
  const record = ctx.record
610
619
  appendLine(lines, 'Preferred name', record.preferred_name ?? record.preferredName ?? ctx.customFields.preferred_name)
@@ -643,9 +652,9 @@ export const searchConfig: SearchModuleConfig = {
643
652
  })
644
653
  }
645
654
 
646
- const presenter = resolvePersonPresenter(record, entity, ctx.customFields)
655
+ const presenter = resolvePersonPresenter(t, record, entity, ctx.customFields)
647
656
  logMissingPresenterTitle('person', record, entity, presenter)
648
- const presenterLabel = pickLabel(presenter.title) ?? 'Open person'
657
+ const presenterLabel = pickLabel(presenter.title) ?? t('customers.search.link.openPerson', 'Open person')
649
658
  const links: SearchResultLink[] = []
650
659
  if (entityId) {
651
660
  const href = buildCustomerUrl('person', entityId)
@@ -669,8 +678,9 @@ export const searchConfig: SearchModuleConfig = {
669
678
 
670
679
  formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {
671
680
  assertTenantContext(ctx)
681
+ const { t } = await resolveTranslations()
672
682
  const entity = await getCustomerEntity(ctx, resolveCustomerEntityId(ctx.record))
673
- return resolvePersonPresenter(ctx.record, entity, ctx.customFields)
683
+ return resolvePersonPresenter(t, ctx.record, entity, ctx.customFields)
674
684
  },
675
685
 
676
686
  resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {
@@ -679,11 +689,12 @@ export const searchConfig: SearchModuleConfig = {
679
689
  },
680
690
 
681
691
  resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {
692
+ const { t } = await resolveTranslations()
682
693
  const entityId = resolveCustomerEntityId(ctx.record)
683
694
  if (!entityId) return null
684
695
  const href = buildCustomerUrl('person', entityId)
685
696
  if (!href) return null
686
- return [{ href: `${href}/edit`, label: 'Edit', kind: 'secondary' }]
697
+ return [{ href: `${href}/edit`, label: t('customers.search.link.edit', 'Edit'), kind: 'secondary' }]
687
698
  },
688
699
 
689
700
  fieldPolicy: {
@@ -714,6 +725,7 @@ export const searchConfig: SearchModuleConfig = {
714
725
 
715
726
  buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {
716
727
  assertTenantContext(ctx)
728
+ const { t } = await resolveTranslations()
717
729
  const lines: string[] = []
718
730
  const record = ctx.record
719
731
  appendLine(lines, 'Legal name', record.legal_name ?? record.legalName ?? ctx.customFields.legal_name)
@@ -734,9 +746,9 @@ export const searchConfig: SearchModuleConfig = {
734
746
  ensureFallbackLines(lines, record)
735
747
  if (!lines.length) return null
736
748
 
737
- const presenter = resolveCompanyPresenter(record, entity, ctx.customFields)
749
+ const presenter = resolveCompanyPresenter(t, record, entity, ctx.customFields)
738
750
  logMissingPresenterTitle('company', record, entity, presenter)
739
- const primaryLabel = pickLabel(presenter.title) ?? 'Open company'
751
+ const primaryLabel = pickLabel(presenter.title) ?? t('customers.search.link.openCompany', 'Open company')
740
752
  const links: SearchResultLink[] = []
741
753
  if (entityId) {
742
754
  const href = buildCustomerUrl('company', entityId)
@@ -760,8 +772,9 @@ export const searchConfig: SearchModuleConfig = {
760
772
 
761
773
  formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {
762
774
  assertTenantContext(ctx)
775
+ const { t } = await resolveTranslations()
763
776
  const entity = await getCustomerEntity(ctx, resolveCustomerEntityId(ctx.record))
764
- return resolveCompanyPresenter(ctx.record, entity, ctx.customFields)
777
+ return resolveCompanyPresenter(t, ctx.record, entity, ctx.customFields)
765
778
  },
766
779
 
767
780
  resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {
@@ -770,11 +783,12 @@ export const searchConfig: SearchModuleConfig = {
770
783
  },
771
784
 
772
785
  resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {
786
+ const { t } = await resolveTranslations()
773
787
  const entityId = resolveCustomerEntityId(ctx.record)
774
788
  if (!entityId) return null
775
789
  const href = buildCustomerUrl('company', entityId)
776
790
  if (!href) return null
777
- return [{ href: `${href}/edit`, label: 'Edit', kind: 'secondary' }]
791
+ return [{ href: `${href}/edit`, label: t('customers.search.link.edit', 'Edit'), kind: 'secondary' }]
778
792
  },
779
793
 
780
794
  fieldPolicy: {
@@ -832,8 +846,9 @@ export const searchConfig: SearchModuleConfig = {
832
846
 
833
847
  formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {
834
848
  assertTenantContext(ctx)
849
+ const { t } = await resolveTranslations()
835
850
  const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)
836
- const title = (parent?.display_name as string | undefined) ?? 'Customer note'
851
+ const title = (parent?.display_name as string | undefined) ?? t('customers.search.fallback.customerNote', 'Customer note')
837
852
  return {
838
853
  title,
839
854
  subtitle: snippet(ctx.record.body),
@@ -850,15 +865,16 @@ export const searchConfig: SearchModuleConfig = {
850
865
 
851
866
  resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {
852
867
  assertTenantContext(ctx)
868
+ const { t } = await resolveTranslations()
853
869
  const links: SearchResultLink[] = []
854
870
  const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)
855
871
  const parentUrl = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)
856
872
  if (parentUrl) {
857
- links.push({ href: parentUrl, label: (parent?.display_name as string | undefined) ?? 'View customer', kind: 'primary' })
873
+ links.push({ href: parentUrl, label: (parent?.display_name as string | undefined) ?? t('customers.search.link.viewCustomer', 'View customer'), kind: 'primary' })
858
874
  }
859
875
  if (ctx.record.deal_id) {
860
876
  const dealUrl = `/backend/customers/deals/${encodeURIComponent(ctx.record.deal_id as string)}`
861
- links.push({ href: dealUrl, label: 'Open deal', kind: 'secondary' })
877
+ links.push({ href: dealUrl, label: t('customers.search.link.openDeal', 'Open deal'), kind: 'secondary' })
862
878
  }
863
879
  return links.length ? links : null
864
880
  },
@@ -880,6 +896,7 @@ export const searchConfig: SearchModuleConfig = {
880
896
  priority: 8,
881
897
 
882
898
  buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {
899
+ const { t } = await resolveTranslations()
883
900
  const lines: string[] = []
884
901
  const record = ctx.record
885
902
  appendLine(lines, 'Title', record.title)
@@ -898,10 +915,10 @@ export const searchConfig: SearchModuleConfig = {
898
915
  return {
899
916
  text: lines,
900
917
  presenter: {
901
- title: String(record.title ?? 'Deal'),
918
+ title: String(record.title ?? t('customers.search.fallback.deal', 'Deal')),
902
919
  subtitle: subtitleParts.join(' · ') || undefined,
903
920
  icon: 'briefcase',
904
- badge: 'Deal',
921
+ badge: t('customers.search.badge.deal', 'Deal'),
905
922
  },
906
923
  checksumSource: {
907
924
  title: record.title,
@@ -913,8 +930,9 @@ export const searchConfig: SearchModuleConfig = {
913
930
  },
914
931
 
915
932
  formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {
933
+ const { t } = await resolveTranslations()
916
934
  const { record } = ctx
917
- const title = pickString(record.title as string, 'Deal')
935
+ const title = pickString(record.title as string, t('customers.search.fallback.deal', 'Deal'))
918
936
  const subtitleParts: string[] = []
919
937
  if (record.pipeline_stage) subtitleParts.push(String(record.pipeline_stage))
920
938
  if (record.status) subtitleParts.push(String(record.status))
@@ -925,10 +943,10 @@ export const searchConfig: SearchModuleConfig = {
925
943
  }
926
944
 
927
945
  return {
928
- title: title ?? 'Deal',
946
+ title: title ?? t('customers.search.fallback.deal', 'Deal'),
929
947
  subtitle: subtitleParts.length ? subtitleParts.join(' · ') : undefined,
930
948
  icon: 'briefcase',
931
- badge: 'Deal',
949
+ badge: t('customers.search.badge.deal', 'Deal'),
932
950
  }
933
951
  },
934
952
 
@@ -939,12 +957,13 @@ export const searchConfig: SearchModuleConfig = {
939
957
  },
940
958
 
941
959
  resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {
960
+ const { t } = await resolveTranslations()
942
961
  const id = ctx.record.id
943
962
  if (!id) return null
944
963
  return [
945
964
  {
946
965
  href: `/backend/customers/deals/${encodeURIComponent(String(id))}/edit`,
947
- label: 'Edit',
966
+ label: t('customers.search.link.edit', 'Edit'),
948
967
  kind: 'secondary',
949
968
  },
950
969
  ]
@@ -968,6 +987,7 @@ export const searchConfig: SearchModuleConfig = {
968
987
 
969
988
  buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {
970
989
  assertTenantContext(ctx)
990
+ const { t } = await resolveTranslations()
971
991
  const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)
972
992
  const lines: string[] = []
973
993
  if (parent?.display_name) lines.push(`Customer: ${parent.display_name}`)
@@ -976,7 +996,11 @@ export const searchConfig: SearchModuleConfig = {
976
996
  if (ctx.record.body) lines.push(`Body: ${ctx.record.body}`)
977
997
 
978
998
  const presenter: SearchResultPresenter = {
979
- title: ctx.record.subject ? String(ctx.record.subject) : `Activity: ${ctx.record.activity_type ?? 'update'}`,
999
+ title: ctx.record.subject
1000
+ ? String(ctx.record.subject)
1001
+ : t('customers.search.fallback.activity', 'Activity: {{type}}', {
1002
+ type: String(ctx.record.activity_type ?? t('customers.search.fallback.activityUpdate', 'update')),
1003
+ }),
980
1004
  subtitle: (parent?.display_name as string | undefined) ?? snippet(ctx.record.body),
981
1005
  icon: 'bolt',
982
1006
  }
@@ -995,12 +1019,17 @@ export const searchConfig: SearchModuleConfig = {
995
1019
 
996
1020
  formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {
997
1021
  assertTenantContext(ctx)
1022
+ const { t } = await resolveTranslations()
998
1023
  const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)
999
1024
  return {
1000
- title: ctx.record.subject ? String(ctx.record.subject) : `Activity: ${ctx.record.activity_type ?? 'update'}`,
1025
+ title: ctx.record.subject
1026
+ ? String(ctx.record.subject)
1027
+ : t('customers.search.fallback.activity', 'Activity: {{type}}', {
1028
+ type: String(ctx.record.activity_type ?? t('customers.search.fallback.activityUpdate', 'update')),
1029
+ }),
1001
1030
  subtitle: (parent?.display_name as string | undefined) ?? snippet(ctx.record.body),
1002
1031
  icon: 'bolt',
1003
- badge: 'Activity',
1032
+ badge: t('customers.search.badge.activity', 'Activity'),
1004
1033
  }
1005
1034
  },
1006
1035
 
@@ -1012,11 +1041,12 @@ export const searchConfig: SearchModuleConfig = {
1012
1041
  },
1013
1042
 
1014
1043
  resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {
1044
+ const { t } = await resolveTranslations()
1015
1045
  const links: SearchResultLink[] = []
1016
1046
  if (ctx.record.deal_id) {
1017
1047
  links.push({
1018
1048
  href: `/backend/customers/deals/${encodeURIComponent(ctx.record.deal_id as string)}`,
1019
- label: 'Open deal',
1049
+ label: t('customers.search.link.openDeal', 'Open deal'),
1020
1050
  kind: 'secondary',
1021
1051
  })
1022
1052
  }
@@ -1064,10 +1094,11 @@ export const searchConfig: SearchModuleConfig = {
1064
1094
 
1065
1095
  formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {
1066
1096
  assertTenantContext(ctx)
1097
+ const { t } = await resolveTranslations()
1067
1098
  const todo = await getLinkedTodo(ctx) as Record<string, unknown> | null
1068
1099
  const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)
1069
1100
  return {
1070
- title: (todo?.title as string | undefined) ?? 'Customer task',
1101
+ title: (todo?.title as string | undefined) ?? t('customers.search.fallback.customerTask', 'Customer task'),
1071
1102
  subtitle: parent?.display_name as string | undefined,
1072
1103
  icon: 'check-square',
1073
1104
  }
@@ -1081,11 +1112,12 @@ export const searchConfig: SearchModuleConfig = {
1081
1112
  },
1082
1113
 
1083
1114
  resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {
1115
+ const { t } = await resolveTranslations()
1084
1116
  const todoId = ctx.record.todo_id ?? ctx.record.todoId
1085
1117
  if (!todoId) return null
1086
1118
  return [{
1087
1119
  href: `/backend/todos/${encodeURIComponent(todoId as string)}/edit`,
1088
- label: 'Open todo',
1120
+ label: t('customers.search.link.openTodo', 'Open todo'),
1089
1121
  kind: 'secondary',
1090
1122
  }]
1091
1123
  },
@@ -9,6 +9,7 @@ The `data_sync` module provides a streaming data synchronization hub for import/
9
9
  ## Always
10
10
 
11
11
  - **Always scope by organizationId + tenantId** — every entity query
12
+ - **Resolve the organization in API routes with `resolveActiveOrganizationId(auth)`** from `@open-mercato/shared/lib/auth/organizationScope`, never with raw `auth.orgId` — a super-admin viewing "all organizations" has `auth.orgId === null`. The fallback is tenant-aware: it never pairs the actor organization with a foreign selected tenant. When it returns `null` for an authenticated caller, answer with `organizationScopeRequiredResponse()` (400) — never 401, which sends `apiFetch` into a session-refresh loop
12
13
  - **Use the queue system** — never run syncs inline in API handlers
13
14
  - **New sync providers MUST support provider-owned env preconfiguration** when fresh installs need credentials or default mappings/locales/channels from deployment env
14
15
  - **Persist cursor after each batch** — enables resume on failure