@open-mercato/core 0.6.7-develop.6726.1.983ae8a07e → 0.6.7-develop.6749.1.6b54c56dfe

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 (191) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/resources_resource/index.js +2 -0
  3. package/dist/generated/entities/resources_resource/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/helpers/integration/salesFixtures.js +31 -2
  7. package/dist/helpers/integration/salesFixtures.js.map +2 -2
  8. package/dist/helpers/integration/salesUi.js +5 -0
  9. package/dist/helpers/integration/salesUi.js.map +2 -2
  10. package/dist/modules/auth/api/profile/route.js +8 -2
  11. package/dist/modules/auth/api/profile/route.js.map +2 -2
  12. package/dist/modules/auth/backend/auth/profile/page.js.map +2 -2
  13. package/dist/modules/auth/backend/profile/change-password/page.js.map +2 -2
  14. package/dist/modules/communication_channels/data/enrichers.js +4 -4
  15. package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
  16. package/dist/modules/customers/analytics.js +1 -0
  17. package/dist/modules/customers/analytics.js.map +2 -2
  18. package/dist/modules/customers/components/detail/DealForm.js +2 -1
  19. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  20. package/dist/modules/customers/components/detail/DealsSection.js +1 -1
  21. package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
  22. package/dist/modules/customers/data/validators.js +3 -1
  23. package/dist/modules/customers/data/validators.js.map +2 -2
  24. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +52 -2
  25. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +2 -2
  26. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +42 -26
  27. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
  28. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +2 -2
  29. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +2 -2
  30. package/dist/modules/data_sync/lib/queue-policy.js +15 -0
  31. package/dist/modules/data_sync/lib/queue-policy.js.map +7 -0
  32. package/dist/modules/data_sync/lib/queue.js +16 -1
  33. package/dist/modules/data_sync/lib/queue.js.map +2 -2
  34. package/dist/modules/data_sync/lib/start-run.js +2 -1
  35. package/dist/modules/data_sync/lib/start-run.js.map +2 -2
  36. package/dist/modules/data_sync/lib/sync-engine.js +35 -2
  37. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  38. package/dist/modules/data_sync/lib/sync-run-service.js +70 -2
  39. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  40. package/dist/modules/data_sync/workers/sync-export.js +9 -2
  41. package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
  42. package/dist/modules/data_sync/workers/sync-import.js +9 -2
  43. package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
  44. package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +34 -12
  45. package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
  46. package/dist/modules/dictionaries/api/openapi.js +15 -2
  47. package/dist/modules/dictionaries/api/openapi.js.map +2 -2
  48. package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js +30 -29
  49. package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js.map +2 -2
  50. package/dist/modules/dictionaries/data/validators.js +7 -0
  51. package/dist/modules/dictionaries/data/validators.js.map +2 -2
  52. package/dist/modules/dictionaries/lib/clientEntries.js +4 -3
  53. package/dist/modules/dictionaries/lib/clientEntries.js.map +2 -2
  54. package/dist/modules/dictionaries/lib/fetchAllEntries.js +43 -0
  55. package/dist/modules/dictionaries/lib/fetchAllEntries.js.map +7 -0
  56. package/dist/modules/entities/api/definitions.js +51 -3
  57. package/dist/modules/entities/api/definitions.js.map +2 -2
  58. package/dist/modules/entities/api/entities.js +21 -2
  59. package/dist/modules/entities/api/entities.js.map +2 -2
  60. package/dist/modules/entities/api/records.js +3 -24
  61. package/dist/modules/entities/api/records.js.map +2 -2
  62. package/dist/modules/entities/api/sidebar-entities.js +34 -12
  63. package/dist/modules/entities/api/sidebar-entities.js.map +2 -2
  64. package/dist/modules/entities/lib/entityAcl.js +46 -0
  65. package/dist/modules/entities/lib/entityAcl.js.map +2 -2
  66. package/dist/modules/messages/api/route.js +2 -5
  67. package/dist/modules/messages/api/route.js.map +2 -2
  68. package/dist/modules/messages/lib/participantScope.js +15 -0
  69. package/dist/modules/messages/lib/participantScope.js.map +7 -0
  70. package/dist/modules/notifications/api/route.js +3 -1
  71. package/dist/modules/notifications/api/route.js.map +2 -2
  72. package/dist/modules/notifications/api/unread-count/route.js +25 -6
  73. package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
  74. package/dist/modules/notifications/lib/notificationScope.js +26 -0
  75. package/dist/modules/notifications/lib/notificationScope.js.map +7 -0
  76. package/dist/modules/notifications/lib/notificationService.js +7 -3
  77. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  78. package/dist/modules/notifications/lib/routeHelpers.js +16 -2
  79. package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
  80. package/dist/modules/planner/components/unavailabilityReasons.js +3 -3
  81. package/dist/modules/planner/components/unavailabilityReasons.js.map +2 -2
  82. package/dist/modules/progress/lib/progressService.js +2 -0
  83. package/dist/modules/progress/lib/progressService.js.map +2 -2
  84. package/dist/modules/progress/lib/progressServiceImpl.js +322 -104
  85. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  86. package/dist/modules/resources/api/resources.js +2 -0
  87. package/dist/modules/resources/api/resources.js.map +2 -2
  88. package/dist/modules/resources/backend/resources/resources/[id]/page.js +3 -2
  89. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  90. package/dist/modules/resources/commands/resources.js +9 -0
  91. package/dist/modules/resources/commands/resources.js.map +2 -2
  92. package/dist/modules/resources/components/detail/dictionaries.js +3 -3
  93. package/dist/modules/resources/components/detail/dictionaries.js.map +2 -2
  94. package/dist/modules/resources/data/entities.js +3 -0
  95. package/dist/modules/resources/data/entities.js.map +2 -2
  96. package/dist/modules/resources/data/validators.js +6 -2
  97. package/dist/modules/resources/data/validators.js.map +2 -2
  98. package/dist/modules/resources/lib/seeds.js +16 -9
  99. package/dist/modules/resources/lib/seeds.js.map +2 -2
  100. package/dist/modules/resources/migrations/Migration20260608231000.js +13 -0
  101. package/dist/modules/resources/migrations/Migration20260608231000.js.map +7 -0
  102. package/dist/modules/sales/backend/sales/documents/create/page.js +11 -14
  103. package/dist/modules/sales/backend/sales/documents/create/page.js.map +2 -2
  104. package/dist/modules/sales/commands/documents.js +8 -0
  105. package/dist/modules/sales/commands/documents.js.map +2 -2
  106. package/dist/modules/sales/components/documents/ItemsSection.js +18 -9
  107. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  108. package/dist/modules/sales/components/documents/LineItemDialog.js +12 -5
  109. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  110. package/dist/modules/sales/components/documents/SalesDocumentForm.js +48 -2
  111. package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
  112. package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +167 -0
  113. package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +7 -0
  114. package/dist/modules/sales/data/validators.js +5 -1
  115. package/dist/modules/sales/data/validators.js.map +2 -2
  116. package/dist/modules/staff/components/detail/dictionaries.js +3 -3
  117. package/dist/modules/staff/components/detail/dictionaries.js.map +2 -2
  118. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +11 -4
  119. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +2 -2
  120. package/generated/entities/resources_resource/index.ts +1 -0
  121. package/generated/entity-fields-registry.ts +1 -0
  122. package/package.json +7 -7
  123. package/src/helpers/integration/salesFixtures.ts +34 -2
  124. package/src/helpers/integration/salesUi.ts +8 -0
  125. package/src/modules/auth/api/profile/route.ts +8 -2
  126. package/src/modules/auth/backend/auth/profile/page.tsx +1 -0
  127. package/src/modules/auth/backend/profile/change-password/page.tsx +1 -0
  128. package/src/modules/communication_channels/data/enrichers.ts +6 -26
  129. package/src/modules/customers/analytics.ts +1 -0
  130. package/src/modules/customers/components/detail/DealForm.tsx +5 -1
  131. package/src/modules/customers/components/detail/DealsSection.tsx +1 -1
  132. package/src/modules/customers/data/validators.ts +5 -1
  133. package/src/modules/dashboards/i18n/de.json +3 -0
  134. package/src/modules/dashboards/i18n/en.json +3 -0
  135. package/src/modules/dashboards/i18n/es.json +3 -0
  136. package/src/modules/dashboards/i18n/pl.json +3 -0
  137. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +86 -0
  138. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +33 -18
  139. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +2 -2
  140. package/src/modules/data_sync/lib/adapter.ts +18 -0
  141. package/src/modules/data_sync/lib/queue-policy.ts +30 -0
  142. package/src/modules/data_sync/lib/queue.ts +19 -1
  143. package/src/modules/data_sync/lib/start-run.ts +2 -1
  144. package/src/modules/data_sync/lib/sync-engine.ts +46 -0
  145. package/src/modules/data_sync/lib/sync-run-service.ts +82 -1
  146. package/src/modules/data_sync/workers/sync-export.ts +8 -1
  147. package/src/modules/data_sync/workers/sync-import.ts +8 -1
  148. package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +39 -11
  149. package/src/modules/dictionaries/api/openapi.ts +19 -0
  150. package/src/modules/dictionaries/components/hooks/useDictionaryEntries.ts +35 -36
  151. package/src/modules/dictionaries/data/validators.ts +14 -0
  152. package/src/modules/dictionaries/lib/clientEntries.ts +4 -3
  153. package/src/modules/dictionaries/lib/fetchAllEntries.ts +72 -0
  154. package/src/modules/entities/api/definitions.ts +51 -3
  155. package/src/modules/entities/api/entities.ts +30 -2
  156. package/src/modules/entities/api/records.ts +3 -27
  157. package/src/modules/entities/api/sidebar-entities.ts +43 -14
  158. package/src/modules/entities/lib/entityAcl.ts +58 -0
  159. package/src/modules/messages/api/route.ts +4 -5
  160. package/src/modules/messages/lib/participantScope.ts +64 -0
  161. package/src/modules/notifications/api/route.ts +2 -0
  162. package/src/modules/notifications/api/unread-count/route.ts +42 -5
  163. package/src/modules/notifications/lib/notificationScope.ts +40 -0
  164. package/src/modules/notifications/lib/notificationService.ts +5 -0
  165. package/src/modules/notifications/lib/routeHelpers.ts +19 -2
  166. package/src/modules/planner/components/unavailabilityReasons.ts +3 -3
  167. package/src/modules/progress/AGENTS.md +11 -1
  168. package/src/modules/progress/lib/progressService.ts +1 -0
  169. package/src/modules/progress/lib/progressServiceImpl.ts +385 -117
  170. package/src/modules/resources/api/resources.ts +2 -0
  171. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +9 -3
  172. package/src/modules/resources/commands/resources.ts +10 -0
  173. package/src/modules/resources/components/detail/dictionaries.ts +3 -3
  174. package/src/modules/resources/data/entities.ts +3 -0
  175. package/src/modules/resources/data/validators.ts +5 -0
  176. package/src/modules/resources/lib/seeds.ts +16 -9
  177. package/src/modules/resources/migrations/.snapshot-open-mercato.json +17 -1
  178. package/src/modules/resources/migrations/Migration20260608231000.ts +13 -0
  179. package/src/modules/sales/backend/sales/documents/create/page.tsx +2 -4
  180. package/src/modules/sales/commands/documents.ts +8 -0
  181. package/src/modules/sales/components/documents/ItemsSection.tsx +19 -10
  182. package/src/modules/sales/components/documents/LineItemDialog.tsx +13 -5
  183. package/src/modules/sales/components/documents/SalesDocumentForm.tsx +48 -6
  184. package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +198 -0
  185. package/src/modules/sales/data/validators.ts +7 -1
  186. package/src/modules/sales/i18n/de.json +2 -0
  187. package/src/modules/sales/i18n/en.json +2 -0
  188. package/src/modules/sales/i18n/es.json +2 -0
  189. package/src/modules/sales/i18n/pl.json +2 -0
  190. package/src/modules/staff/components/detail/dictionaries.ts +3 -3
  191. package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +12 -5
@@ -1,16 +1,66 @@
1
1
  import { isValidDateRangePreset } from "@open-mercato/ui/backend/date-range";
2
+ const CLOSED_DEAL_STATUSES = ["win", "loose", "won", "lost", "closed"];
3
+ const OPEN_DEAL_CLOSURE_OUTCOME_FILTER = {
4
+ field: "closureOutcome",
5
+ operator: "is_null"
6
+ };
7
+ const PIPELINE_STATUS_SCOPES = ["open", "all"];
2
8
  const DEFAULT_SETTINGS = {
3
- dateRange: "this_month"
9
+ dateRange: "this_month",
10
+ statusScope: "open"
4
11
  };
12
+ function isValidStatusScope(value) {
13
+ return typeof value === "string" && PIPELINE_STATUS_SCOPES.includes(value);
14
+ }
5
15
  function hydrateSettings(raw) {
6
16
  if (!raw || typeof raw !== "object") return { ...DEFAULT_SETTINGS };
7
17
  const obj = raw;
8
18
  return {
9
- dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange
19
+ dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,
20
+ statusScope: isValidStatusScope(obj.statusScope) ? obj.statusScope : DEFAULT_SETTINGS.statusScope
21
+ };
22
+ }
23
+ function dehydrateSettings(settings) {
24
+ return {
25
+ dateRange: settings.dateRange,
26
+ statusScope: settings.statusScope
27
+ };
28
+ }
29
+ function buildPipelineDataRequest(settings) {
30
+ const request = {
31
+ entityType: "customers:deals",
32
+ metric: {
33
+ field: "valueAmount",
34
+ aggregate: "sum"
35
+ },
36
+ groupBy: {
37
+ field: "pipelineStage",
38
+ resolveLabels: true
39
+ },
40
+ dateRange: {
41
+ field: "createdAt",
42
+ preset: settings.dateRange
43
+ }
10
44
  };
45
+ if (settings.statusScope === "open") {
46
+ request.filters = [
47
+ ...CLOSED_DEAL_STATUSES.map((status) => ({
48
+ field: "status",
49
+ operator: "neq",
50
+ value: status
51
+ })),
52
+ { ...OPEN_DEAL_CLOSURE_OUTCOME_FILTER }
53
+ ];
54
+ }
55
+ return request;
11
56
  }
12
57
  export {
58
+ CLOSED_DEAL_STATUSES,
13
59
  DEFAULT_SETTINGS,
60
+ OPEN_DEAL_CLOSURE_OUTCOME_FILTER,
61
+ PIPELINE_STATUS_SCOPES,
62
+ buildPipelineDataRequest,
63
+ dehydrateSettings,
14
64
  hydrateSettings
15
65
  };
16
66
  //# sourceMappingURL=config.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts"],
4
- "sourcesContent": ["import { type DateRangePreset, isValidDateRangePreset } from '@open-mercato/ui/backend/date-range'\n\nexport type PipelineSummarySettings = {\n dateRange: DateRangePreset\n}\n\nexport const DEFAULT_SETTINGS: PipelineSummarySettings = {\n dateRange: 'this_month',\n}\n\nexport function hydrateSettings(raw: unknown): PipelineSummarySettings {\n if (!raw || typeof raw !== 'object') return { ...DEFAULT_SETTINGS }\n const obj = raw as Record<string, unknown>\n return {\n dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,\n }\n}\n"],
5
- "mappings": "AAAA,SAA+B,8BAA8B;AAMtD,MAAM,mBAA4C;AAAA,EACvD,WAAW;AACb;AAEO,SAAS,gBAAgB,KAAuC;AACrE,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,EAAE,GAAG,iBAAiB;AAClE,QAAM,MAAM;AACZ,SAAO;AAAA,IACL,WAAW,uBAAuB,IAAI,SAAS,IAAI,IAAI,YAAY,iBAAiB;AAAA,EACtF;AACF;",
4
+ "sourcesContent": ["import { type DateRangePreset, isValidDateRangePreset } from '@open-mercato/ui/backend/date-range'\nimport type { WidgetDataRequest } from '../../../services/widgetDataService'\n\n// Deal statuses that mean the deal is closed and must stop counting as current pipeline.\n//\n// This is a denylist, not an allowlist: `customer_deals.status` is a lenient `text` column\n// fed by the per-tenant `deal_status` dictionary, so a status unknown to code counts as OPEN\n// and keeps contributing to the chart. That keeps tenant-specific stages visible and mirrors\n// `customers/lib/interactionStatus.ts`, which treats an unknown interaction status as open.\n//\n// Every vocabulary that reaches the column has to be listed here:\n// - `win` / `loose` are written by the deal closure UI and the kanban board.\n// - `won` / `lost` are written verbatim by the `customers.update_deal_stage` AI tool, whose\n// free-form `toStage` is passed straight through to `status`.\n// - `closed` is a seeded `deal_status` dictionary value, persisted by the dashboards\n// analytics seed and treated as terminal by the demo-data generator.\nexport const CLOSED_DEAL_STATUSES = ['win', 'loose', 'won', 'lost', 'closed'] as const\n\n// A deal is also closed when `closure_outcome` records the outcome, which the deals-summary\n// KPI already treats as terminal: `api/deals/summary/route.ts` counts a deal as won or lost\n// when EITHER `status` OR `closure_outcome` says so. The two columns can disagree \u2014 the CRUD\n// API accepts `closureOutcome` on its own, and the AI tool writes `status` without ever setting\n// it \u2014 so the chart has to test both signals to agree with those KPI cards (#4668).\n//\n// The `closure_outcome` side is expressed as `IS NULL` rather than as a `neq` denylist for two\n// reasons, and swapping it for `neq` would be a silent regression:\n// - `closure_outcome` is nullable, and every OPEN deal holds NULL there. `neq` renders as\n// `column != ?`, and in SQL `NULL != 'won'` is NULL, not true \u2014 a `neq` filter would drop\n// every open deal and empty the chart. `status` has no such problem: it is `text not null\n// default 'open'`, which is why the status side stays a denylist.\n// - Unlike `status`, this column is not fed by a per-tenant dictionary. Every write path\n// validates it against the same closed `z.enum(['won', 'lost'])` (`data/validators.ts`,\n// `api/deals/[id]/route.ts`, `api/deals/[id]/stats/route.ts`), so a non-null value always\n// means closed and there is no tenant-specific vocabulary to keep visible.\nexport const OPEN_DEAL_CLOSURE_OUTCOME_FILTER = {\n field: 'closureOutcome',\n operator: 'is_null',\n} as const\n\nexport const PIPELINE_STATUS_SCOPES = ['open', 'all'] as const\n\nexport type PipelineStatusScope = typeof PIPELINE_STATUS_SCOPES[number]\n\nexport type PipelineSummarySettings = {\n dateRange: DateRangePreset\n statusScope: PipelineStatusScope\n}\n\nexport const DEFAULT_SETTINGS: PipelineSummarySettings = {\n dateRange: 'this_month',\n statusScope: 'open',\n}\n\nfunction isValidStatusScope(value: unknown): value is PipelineStatusScope {\n return typeof value === 'string' && (PIPELINE_STATUS_SCOPES as readonly string[]).includes(value)\n}\n\nexport function hydrateSettings(raw: unknown): PipelineSummarySettings {\n if (!raw || typeof raw !== 'object') return { ...DEFAULT_SETTINGS }\n const obj = raw as Record<string, unknown>\n return {\n dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,\n statusScope: isValidStatusScope(obj.statusScope) ? obj.statusScope : DEFAULT_SETTINGS.statusScope,\n }\n}\n\nexport function dehydrateSettings(settings: PipelineSummarySettings): Record<string, unknown> {\n return {\n dateRange: settings.dateRange,\n statusScope: settings.statusScope,\n }\n}\n\nexport function buildPipelineDataRequest(settings: PipelineSummarySettings): WidgetDataRequest {\n const request: WidgetDataRequest = {\n entityType: 'customers:deals',\n metric: {\n field: 'valueAmount',\n aggregate: 'sum',\n },\n groupBy: {\n field: 'pipelineStage',\n resolveLabels: true,\n },\n dateRange: {\n field: 'createdAt',\n preset: settings.dateRange,\n },\n }\n\n if (settings.statusScope === 'open') {\n request.filters = [\n ...CLOSED_DEAL_STATUSES.map((status) => ({\n field: 'status',\n operator: 'neq' as const,\n value: status,\n })),\n { ...OPEN_DEAL_CLOSURE_OUTCOME_FILTER },\n ]\n }\n\n return request\n}\n"],
5
+ "mappings": "AAAA,SAA+B,8BAA8B;AAgBtD,MAAM,uBAAuB,CAAC,OAAO,SAAS,OAAO,QAAQ,QAAQ;AAkBrE,MAAM,mCAAmC;AAAA,EAC9C,OAAO;AAAA,EACP,UAAU;AACZ;AAEO,MAAM,yBAAyB,CAAC,QAAQ,KAAK;AAS7C,MAAM,mBAA4C;AAAA,EACvD,WAAW;AAAA,EACX,aAAa;AACf;AAEA,SAAS,mBAAmB,OAA8C;AACxE,SAAO,OAAO,UAAU,YAAa,uBAA6C,SAAS,KAAK;AAClG;AAEO,SAAS,gBAAgB,KAAuC;AACrE,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,EAAE,GAAG,iBAAiB;AAClE,QAAM,MAAM;AACZ,SAAO;AAAA,IACL,WAAW,uBAAuB,IAAI,SAAS,IAAI,IAAI,YAAY,iBAAiB;AAAA,IACpF,aAAa,mBAAmB,IAAI,WAAW,IAAI,IAAI,cAAc,iBAAiB;AAAA,EACxF;AACF;AAEO,SAAS,kBAAkB,UAA4D;AAC5F,SAAO;AAAA,IACL,WAAW,SAAS;AAAA,IACpB,aAAa,SAAS;AAAA,EACxB;AACF;AAEO,SAAS,yBAAyB,UAAsD;AAC7F,QAAM,UAA6B;AAAA,IACjC,YAAY;AAAA,IACZ,QAAQ;AAAA,MACN,OAAO;AAAA,MACP,WAAW;AAAA,IACb;AAAA,IACA,SAAS;AAAA,MACP,OAAO;AAAA,MACP,eAAe;AAAA,IACjB;AAAA,IACA,WAAW;AAAA,MACT,OAAO;AAAA,MACP,QAAQ,SAAS;AAAA,IACnB;AAAA,EACF;AAEA,MAAI,SAAS,gBAAgB,QAAQ;AACnC,YAAQ,UAAU;AAAA,MAChB,GAAG,qBAAqB,IAAI,CAAC,YAAY;AAAA,QACvC,OAAO;AAAA,QACP,UAAU;AAAA,QACV,OAAO;AAAA,MACT,EAAE;AAAA,MACF,EAAE,GAAG,iCAAiC;AAAA,IACxC;AAAA,EACF;AAEA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -8,27 +8,17 @@ import {
8
8
  DateRangeSelect,
9
9
  InlineDateRangeSelect
10
10
  } from "@open-mercato/ui/backend/date-range";
11
- import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@open-mercato/ui/primitives/select";
12
+ import {
13
+ DEFAULT_SETTINGS,
14
+ buildPipelineDataRequest,
15
+ hydrateSettings
16
+ } from "./config.js";
12
17
  import { formatCurrencyCompact } from "../../../lib/formatters.js";
13
18
  import { createLogger } from "@open-mercato/shared/lib/logger";
14
19
  const logger = createLogger("dashboards").child({ component: "pipeline-summary" });
15
20
  async function fetchPipelineData(settings, fetchWidgetData) {
16
- const body = {
17
- entityType: "customers:deals",
18
- metric: {
19
- field: "valueAmount",
20
- aggregate: "sum"
21
- },
22
- groupBy: {
23
- field: "pipelineStage",
24
- resolveLabels: true
25
- },
26
- dateRange: {
27
- field: "createdAt",
28
- preset: settings.dateRange
29
- }
30
- };
31
- return fetchWidgetData(body);
21
+ return fetchWidgetData(buildPipelineDataRequest(settings));
32
22
  }
33
23
  function formatStageLabel(stage, t) {
34
24
  if (stage == null || stage === "") return t("dashboards.analytics.labels.unknown", "Unknown");
@@ -75,15 +65,41 @@ const PipelineSummaryWidget = ({
75
65
  });
76
66
  }, [refresh, refreshToken]);
77
67
  if (mode === "settings") {
78
- return /* @__PURE__ */ jsx("div", { className: "space-y-4 text-sm", children: /* @__PURE__ */ jsx(
79
- DateRangeSelect,
80
- {
81
- id: "pipeline-summary-date-range",
82
- label: t("dashboards.analytics.settings.dateRange", "Date Range"),
83
- value: hydrated.dateRange,
84
- onChange: (dateRange) => onSettingsChange({ ...hydrated, dateRange })
85
- }
86
- ) });
68
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4 text-sm", children: [
69
+ /* @__PURE__ */ jsx(
70
+ DateRangeSelect,
71
+ {
72
+ id: "pipeline-summary-date-range",
73
+ label: t("dashboards.analytics.settings.dateRange", "Date Range"),
74
+ value: hydrated.dateRange,
75
+ onChange: (dateRange) => onSettingsChange({ ...hydrated, dateRange })
76
+ }
77
+ ),
78
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
79
+ /* @__PURE__ */ jsx(
80
+ "label",
81
+ {
82
+ htmlFor: "pipeline-summary-status-scope",
83
+ className: "text-xs font-semibold uppercase text-muted-foreground",
84
+ children: t("dashboards.analytics.settings.dealStatusScope", "Deals included")
85
+ }
86
+ ),
87
+ /* @__PURE__ */ jsxs(
88
+ Select,
89
+ {
90
+ value: hydrated.statusScope,
91
+ onValueChange: (value) => onSettingsChange({ ...hydrated, statusScope: value }),
92
+ children: [
93
+ /* @__PURE__ */ jsx(SelectTrigger, { id: "pipeline-summary-status-scope", size: "sm", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
94
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
95
+ /* @__PURE__ */ jsx(SelectItem, { value: "open", children: t("dashboards.analytics.settings.dealStatusScopeOpen", "Open deals only") }),
96
+ /* @__PURE__ */ jsx(SelectItem, { value: "all", children: t("dashboards.analytics.settings.dealStatusScopeAll", "All deals, including won and lost") })
97
+ ] })
98
+ ]
99
+ }
100
+ )
101
+ ] })
102
+ ] });
87
103
  }
88
104
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
89
105
  /* @__PURE__ */ jsx("div", { className: "flex justify-end mb-2", children: /* @__PURE__ */ jsx(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'\nimport {\n DateRangeSelect,\n InlineDateRangeSelect,\n type DateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport { DEFAULT_SETTINGS, hydrateSettings, type PipelineSummarySettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { formatCurrencyCompact } from '../../../lib/formatters'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'pipeline-summary' })\n\nasync function fetchPipelineData(settings: PipelineSummarySettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {\n const body = {\n entityType: 'customers:deals',\n metric: {\n field: 'valueAmount',\n aggregate: 'sum',\n },\n groupBy: {\n field: 'pipelineStage',\n resolveLabels: true,\n },\n dateRange: {\n field: 'createdAt',\n preset: settings.dateRange,\n },\n }\n\n return fetchWidgetData<WidgetDataResponse>(body)\n}\n\nfunction formatStageLabel(stage: unknown, t: (key: string, fallback: string) => string): string {\n if (stage == null || stage === '') return t('dashboards.analytics.labels.unknown', 'Unknown')\n const stageStr = String(stage)\n if (stageStr === '0' || stageStr === 'null' || stageStr === 'undefined') {\n return t('dashboards.analytics.labels.unknown', 'Unknown')\n }\n return stageStr\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (l) => l.toUpperCase())\n}\n\nconst PipelineSummaryWidget: React.FC<DashboardWidgetComponentProps<PipelineSummarySettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n const [data, setData] = React.useState<BarChartDataItem[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n const fetchWidgetData = useWidgetData()\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const result = await fetchPipelineData(hydrated, fetchWidgetData)\n const chartData = result.data\n .filter((item) => item.groupKey != null && item.groupKey !== '' && String(item.groupKey) !== '0')\n .map((item) => ({\n stage: formatStageLabel(item.groupLabel ?? item.groupKey, t),\n Value: item.value ?? 0,\n }))\n setData(chartData)\n } catch (err) {\n logger.error('Failed to load pipeline data', { err })\n setError(t('dashboards.analytics.widgets.pipelineSummary.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, fetchWidgetData, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"pipeline-summary-date-range\"\n label={t('dashboards.analytics.settings.dateRange', 'Date Range')}\n value={hydrated.dateRange}\n onChange={(dateRange: DateRangePreset) => onSettingsChange({ ...hydrated, dateRange })}\n />\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex justify-end mb-2\">\n <InlineDateRangeSelect\n value={hydrated.dateRange}\n onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange })}\n />\n </div>\n <div className=\"flex-1 min-h-0\">\n <BarChart\n data={data}\n index=\"stage\"\n categories={['Value']}\n categoryLabels={{ Value: t('dashboards.analytics.labels.value', 'Value') }}\n loading={loading}\n error={error}\n valueFormatter={formatCurrencyCompact}\n colors={['violet']}\n showLegend={false}\n emptyMessage={t('dashboards.analytics.widgets.pipelineSummary.empty', 'No deal data for this period')}\n />\n </div>\n </div>\n )\n}\n\nexport default PipelineSummaryWidget\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'\nimport {\n DateRangeSelect,\n InlineDateRangeSelect,\n type DateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'\nimport {\n DEFAULT_SETTINGS,\n buildPipelineDataRequest,\n hydrateSettings,\n type PipelineStatusScope,\n type PipelineSummarySettings,\n} from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { formatCurrencyCompact } from '../../../lib/formatters'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'pipeline-summary' })\n\nasync function fetchPipelineData(settings: PipelineSummarySettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {\n return fetchWidgetData<WidgetDataResponse>(buildPipelineDataRequest(settings))\n}\n\nfunction formatStageLabel(stage: unknown, t: (key: string, fallback: string) => string): string {\n if (stage == null || stage === '') return t('dashboards.analytics.labels.unknown', 'Unknown')\n const stageStr = String(stage)\n if (stageStr === '0' || stageStr === 'null' || stageStr === 'undefined') {\n return t('dashboards.analytics.labels.unknown', 'Unknown')\n }\n return stageStr\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (l) => l.toUpperCase())\n}\n\nconst PipelineSummaryWidget: React.FC<DashboardWidgetComponentProps<PipelineSummarySettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n const [data, setData] = React.useState<BarChartDataItem[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n const fetchWidgetData = useWidgetData()\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const result = await fetchPipelineData(hydrated, fetchWidgetData)\n const chartData = result.data\n .filter((item) => item.groupKey != null && item.groupKey !== '' && String(item.groupKey) !== '0')\n .map((item) => ({\n stage: formatStageLabel(item.groupLabel ?? item.groupKey, t),\n Value: item.value ?? 0,\n }))\n setData(chartData)\n } catch (err) {\n logger.error('Failed to load pipeline data', { err })\n setError(t('dashboards.analytics.widgets.pipelineSummary.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, fetchWidgetData, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"pipeline-summary-date-range\"\n label={t('dashboards.analytics.settings.dateRange', 'Date Range')}\n value={hydrated.dateRange}\n onChange={(dateRange: DateRangePreset) => onSettingsChange({ ...hydrated, dateRange })}\n />\n <div className=\"space-y-1.5\">\n <label\n htmlFor=\"pipeline-summary-status-scope\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('dashboards.analytics.settings.dealStatusScope', 'Deals included')}\n </label>\n <Select\n value={hydrated.statusScope}\n onValueChange={(value) => onSettingsChange({ ...hydrated, statusScope: value as PipelineStatusScope })}\n >\n <SelectTrigger id=\"pipeline-summary-status-scope\" size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"open\">\n {t('dashboards.analytics.settings.dealStatusScopeOpen', 'Open deals only')}\n </SelectItem>\n <SelectItem value=\"all\">\n {t('dashboards.analytics.settings.dealStatusScopeAll', 'All deals, including won and lost')}\n </SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex justify-end mb-2\">\n <InlineDateRangeSelect\n value={hydrated.dateRange}\n onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange })}\n />\n </div>\n <div className=\"flex-1 min-h-0\">\n <BarChart\n data={data}\n index=\"stage\"\n categories={['Value']}\n categoryLabels={{ Value: t('dashboards.analytics.labels.value', 'Value') }}\n loading={loading}\n error={error}\n valueFormatter={formatCurrencyCompact}\n colors={['violet']}\n showLegend={false}\n emptyMessage={t('dashboards.analytics.widgets.pipelineSummary.empty', 'No deal data for this period')}\n />\n </div>\n </div>\n )\n}\n\nexport default PipelineSummaryWidget\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,5 @@
1
1
  import { lazyDashboardWidget } from "@open-mercato/shared/modules/dashboard/widgets";
2
- import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
2
+ import { DEFAULT_SETTINGS, dehydrateSettings, hydrateSettings } from "./config.js";
3
3
  const PipelineSummaryWidget = lazyDashboardWidget(() => import("./widget.client.js"));
4
4
  const widget = {
5
5
  metadata: {
@@ -17,7 +17,7 @@ const widget = {
17
17
  },
18
18
  Widget: PipelineSummaryWidget,
19
19
  hydrateSettings,
20
- dehydrateSettings: (s) => ({ dateRange: s.dateRange })
20
+ dehydrateSettings
21
21
  };
22
22
  var widget_default = widget;
23
23
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts"],
4
- "sourcesContent": ["import { lazyDashboardWidget, type DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { DEFAULT_SETTINGS, hydrateSettings, type PipelineSummarySettings } from './config'\nconst PipelineSummaryWidget = lazyDashboardWidget(() => import('./widget.client'))\n\nconst widget: DashboardWidgetModule<PipelineSummarySettings> = {\n metadata: {\n id: 'dashboards.analytics.pipelineSummary',\n title: 'Pipeline Summary',\n description: 'Deal value by pipeline stage',\n features: ['analytics.view', 'customers.deals.view'],\n defaultSize: 'md',\n defaultEnabled: false,\n defaultSettings: DEFAULT_SETTINGS,\n tags: ['analytics', 'customers', 'deals', 'chart'],\n category: 'analytics',\n icon: 'git-branch',\n supportsRefresh: true,\n },\n Widget: PipelineSummaryWidget,\n hydrateSettings,\n dehydrateSettings: (s) => ({ dateRange: s.dateRange }),\n}\n\nexport default widget\n"],
5
- "mappings": "AAAA,SAAS,2BAAuD;AAChE,SAAS,kBAAkB,uBAAqD;AAChF,MAAM,wBAAwB,oBAAoB,MAAM,OAAO,iBAAiB,CAAC;AAEjF,MAAM,SAAyD;AAAA,EAC7D,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU,CAAC,kBAAkB,sBAAsB;AAAA,IACnD,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,MAAM,CAAC,aAAa,aAAa,SAAS,OAAO;AAAA,IACjD,UAAU;AAAA,IACV,MAAM;AAAA,IACN,iBAAiB;AAAA,EACnB;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,mBAAmB,CAAC,OAAO,EAAE,WAAW,EAAE,UAAU;AACtD;AAEA,IAAO,iBAAQ;",
4
+ "sourcesContent": ["import { lazyDashboardWidget, type DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { DEFAULT_SETTINGS, dehydrateSettings, hydrateSettings, type PipelineSummarySettings } from './config'\nconst PipelineSummaryWidget = lazyDashboardWidget(() => import('./widget.client'))\n\nconst widget: DashboardWidgetModule<PipelineSummarySettings> = {\n metadata: {\n id: 'dashboards.analytics.pipelineSummary',\n title: 'Pipeline Summary',\n description: 'Deal value by pipeline stage',\n features: ['analytics.view', 'customers.deals.view'],\n defaultSize: 'md',\n defaultEnabled: false,\n defaultSettings: DEFAULT_SETTINGS,\n tags: ['analytics', 'customers', 'deals', 'chart'],\n category: 'analytics',\n icon: 'git-branch',\n supportsRefresh: true,\n },\n Widget: PipelineSummaryWidget,\n hydrateSettings,\n dehydrateSettings,\n}\n\nexport default widget\n"],
5
+ "mappings": "AAAA,SAAS,2BAAuD;AAChE,SAAS,kBAAkB,mBAAmB,uBAAqD;AACnG,MAAM,wBAAwB,oBAAoB,MAAM,OAAO,iBAAiB,CAAC;AAEjF,MAAM,SAAyD;AAAA,EAC7D,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU,CAAC,kBAAkB,sBAAsB;AAAA,IACnD,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,MAAM,CAAC,aAAa,aAAa,SAAS,OAAO;AAAA,IACjD,UAAU;AAAA,IACV,MAAM;AAAA,IACN,iBAAiB;AAAA,EACnB;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AACF;AAEA,IAAO,iBAAQ;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,15 @@
1
+ const DATA_SYNC_IMPORT_QUEUE = "data-sync-import";
2
+ const DATA_SYNC_EXPORT_QUEUE = "data-sync-export";
3
+ const DATA_SYNC_RESUMABLE_QUEUES = [DATA_SYNC_IMPORT_QUEUE, DATA_SYNC_EXPORT_QUEUE];
4
+ const DATA_SYNC_QUEUE_ATTEMPTS = 3;
5
+ const DATA_SYNC_LOCK_DURATION_MS = 12e4;
6
+ const DATA_SYNC_MAX_STALLED_COUNT = 10;
7
+ export {
8
+ DATA_SYNC_EXPORT_QUEUE,
9
+ DATA_SYNC_IMPORT_QUEUE,
10
+ DATA_SYNC_LOCK_DURATION_MS,
11
+ DATA_SYNC_MAX_STALLED_COUNT,
12
+ DATA_SYNC_QUEUE_ATTEMPTS,
13
+ DATA_SYNC_RESUMABLE_QUEUES
14
+ };
15
+ //# sourceMappingURL=queue-policy.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/data_sync/lib/queue-policy.ts"],
4
+ "sourcesContent": ["/**\n * Queue names whose jobs resume an existing SyncRun instead of starting a fresh\n * one. The workers import these to declare their `metadata.queue`, so a rename\n * cannot silently drop the retry policy.\n */\nexport const DATA_SYNC_IMPORT_QUEUE = 'data-sync-import'\nexport const DATA_SYNC_EXPORT_QUEUE = 'data-sync-export'\n\nexport const DATA_SYNC_RESUMABLE_QUEUES: readonly string[] = [DATA_SYNC_IMPORT_QUEUE, DATA_SYNC_EXPORT_QUEUE]\n\nexport const DATA_SYNC_QUEUE_ATTEMPTS = 3\n\n/**\n * A backfill batch can hold the processor well past BullMQ's 30s default lock,\n * so these jobs stall on slow upstreams even when the worker is perfectly\n * healthy. `DATA_SYNC_LOCK_DURATION_MS` is the primary defence \u2014 it stops most\n * of those false stalls at the source rather than reacting to them.\n *\n * `DATA_SYNC_MAX_STALLED_COUNT` covers what is left. BullMQ fails a job once it\n * stalls more than this many times, and at the default of 1 a single stall\n * during a long backfill discards the run permanently. 10 buys enough\n * redeliveries to survive a rolling restart. Raising it is only safe because a\n * duplicate delivery can no longer corrupt the run: the ownership\n * compare-and-swap in `commitBatchProgress` lets exactly one worker advance a\n * run, and every other delivery aborts on its first commit. A job that poisons its\n * worker still dies \u2014 after 10 stalls, with the run left `running` and its\n * cursor unmoved, which is the state the admin runs list surfaces.\n */\nexport const DATA_SYNC_LOCK_DURATION_MS = 120_000\nexport const DATA_SYNC_MAX_STALLED_COUNT = 10\n"],
5
+ "mappings": "AAKO,MAAM,yBAAyB;AAC/B,MAAM,yBAAyB;AAE/B,MAAM,6BAAgD,CAAC,wBAAwB,sBAAsB;AAErG,MAAM,2BAA2B;AAkBjC,MAAM,6BAA6B;AACnC,MAAM,8BAA8B;",
6
+ "names": []
7
+ }
@@ -1,10 +1,25 @@
1
1
  import { createModuleQueue } from "@open-mercato/queue";
2
+ import {
3
+ DATA_SYNC_LOCK_DURATION_MS,
4
+ DATA_SYNC_MAX_STALLED_COUNT,
5
+ DATA_SYNC_QUEUE_ATTEMPTS,
6
+ DATA_SYNC_RESUMABLE_QUEUES
7
+ } from "./queue-policy.js";
2
8
  const queues = /* @__PURE__ */ new Map();
9
+ const resumableQueueNames = new Set(DATA_SYNC_RESUMABLE_QUEUES);
3
10
  function getSyncQueue(queueName) {
4
11
  const existing = queues.get(queueName);
5
12
  if (existing) return existing;
6
13
  const concurrency = Math.max(1, Number.parseInt(process.env.DATA_SYNC_QUEUE_CONCURRENCY ?? "5", 10) || 5);
7
- const created = createModuleQueue(queueName, { concurrency });
14
+ const created = createModuleQueue(
15
+ queueName,
16
+ resumableQueueNames.has(queueName) ? {
17
+ concurrency,
18
+ attempts: DATA_SYNC_QUEUE_ATTEMPTS,
19
+ lockDuration: DATA_SYNC_LOCK_DURATION_MS,
20
+ maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT
21
+ } : { concurrency }
22
+ );
8
23
  queues.set(queueName, created);
9
24
  return created;
10
25
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/data_sync/lib/queue.ts"],
4
- "sourcesContent": ["import { createModuleQueue, type Queue } from '@open-mercato/queue'\n\nconst queues = new Map<string, Queue<Record<string, unknown>>>()\n\nexport function getSyncQueue(queueName: string): Queue<Record<string, unknown>> {\n const existing = queues.get(queueName)\n if (existing) return existing\n\n const concurrency = Math.max(1, Number.parseInt(process.env.DATA_SYNC_QUEUE_CONCURRENCY ?? '5', 10) || 5)\n const created = createModuleQueue<Record<string, unknown>>(queueName, { concurrency })\n\n queues.set(queueName, created)\n return created\n}\n"],
5
- "mappings": "AAAA,SAAS,yBAAqC;AAE9C,MAAM,SAAS,oBAAI,IAA4C;AAExD,SAAS,aAAa,WAAmD;AAC9E,QAAM,WAAW,OAAO,IAAI,SAAS;AACrC,MAAI,SAAU,QAAO;AAErB,QAAM,cAAc,KAAK,IAAI,GAAG,OAAO,SAAS,QAAQ,IAAI,+BAA+B,KAAK,EAAE,KAAK,CAAC;AACxG,QAAM,UAAU,kBAA2C,WAAW,EAAE,YAAY,CAAC;AAErF,SAAO,IAAI,WAAW,OAAO;AAC7B,SAAO;AACT;",
4
+ "sourcesContent": ["import { createModuleQueue, type Queue } from '@open-mercato/queue'\nimport {\n DATA_SYNC_LOCK_DURATION_MS,\n DATA_SYNC_MAX_STALLED_COUNT,\n DATA_SYNC_QUEUE_ATTEMPTS,\n DATA_SYNC_RESUMABLE_QUEUES,\n} from './queue-policy'\n\nconst queues = new Map<string, Queue<Record<string, unknown>>>()\n\nconst resumableQueueNames = new Set<string>(DATA_SYNC_RESUMABLE_QUEUES)\n\nexport function getSyncQueue(queueName: string): Queue<Record<string, unknown>> {\n const existing = queues.get(queueName)\n if (existing) return existing\n\n const concurrency = Math.max(1, Number.parseInt(process.env.DATA_SYNC_QUEUE_CONCURRENCY ?? '5', 10) || 5)\n const created = createModuleQueue<Record<string, unknown>>(\n queueName,\n resumableQueueNames.has(queueName)\n ? {\n concurrency,\n attempts: DATA_SYNC_QUEUE_ATTEMPTS,\n lockDuration: DATA_SYNC_LOCK_DURATION_MS,\n maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,\n }\n : { concurrency },\n )\n\n queues.set(queueName, created)\n return created\n}\n"],
5
+ "mappings": "AAAA,SAAS,yBAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,SAAS,oBAAI,IAA4C;AAE/D,MAAM,sBAAsB,IAAI,IAAY,0BAA0B;AAE/D,SAAS,aAAa,WAAmD;AAC9E,QAAM,WAAW,OAAO,IAAI,SAAS;AACrC,MAAI,SAAU,QAAO;AAErB,QAAM,cAAc,KAAK,IAAI,GAAG,OAAO,SAAS,QAAQ,IAAI,+BAA+B,KAAK,EAAE,KAAK,CAAC;AACxG,QAAM,UAAU;AAAA,IACd;AAAA,IACA,oBAAoB,IAAI,SAAS,IAC7B;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV,cAAc;AAAA,MACd,iBAAiB;AAAA,IACnB,IACE,EAAE,YAAY;AAAA,EACpB;AAEA,SAAO,IAAI,WAAW,OAAO;AAC7B,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,5 @@
1
1
  import { getSyncQueue } from "./queue.js";
2
+ import { DATA_SYNC_EXPORT_QUEUE, DATA_SYNC_IMPORT_QUEUE } from "./queue-policy.js";
2
3
  async function startDataSyncRun(params) {
3
4
  const { syncRunService, progressService, scope, input } = params;
4
5
  const createProgressJob = input.createProgressJob !== false;
@@ -35,7 +36,7 @@ async function startDataSyncRun(params) {
35
36
  tenantId: scope.tenantId
36
37
  }
37
38
  );
38
- const queueName = input.direction === "import" ? "data-sync-import" : "data-sync-export";
39
+ const queueName = input.direction === "import" ? DATA_SYNC_IMPORT_QUEUE : DATA_SYNC_EXPORT_QUEUE;
39
40
  const queue = getSyncQueue(queueName);
40
41
  await queue.enqueue({
41
42
  runId: run.id,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/data_sync/lib/start-run.ts"],
4
- "sourcesContent": ["import type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncRunService } from './sync-run-service'\nimport { getSyncQueue } from './queue'\n\nexport type DataSyncStartScope = {\n organizationId: string\n tenantId: string\n userId?: string | null\n}\n\nexport type StartDataSyncRunInput = {\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n cursor?: string | null\n triggeredBy?: string | null\n batchSize?: number\n createProgressJob?: boolean\n progressJob?: {\n jobType?: string\n name?: string\n description?: string\n cancellable?: boolean\n meta?: Record<string, unknown>\n }\n}\n\nexport async function startDataSyncRun(params: {\n syncRunService: SyncRunService\n progressService: ProgressService\n scope: DataSyncStartScope\n input: StartDataSyncRunInput\n}) {\n const { syncRunService, progressService, scope, input } = params\n const createProgressJob = input.createProgressJob !== false\n\n const progressJob = createProgressJob\n ? await progressService.createJob(\n {\n jobType: input.progressJob?.jobType ?? `data_sync:${input.direction}`,\n name: input.progressJob?.name ?? `Data sync ${input.integrationId} \u2014 ${input.entityType}`,\n description: input.progressJob?.description ?? `${input.entityType} ${input.direction}`,\n cancellable: input.progressJob?.cancellable ?? true,\n meta: {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n ...(input.progressJob?.meta ?? {}),\n },\n },\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n : null\n\n const run = await syncRunService.createRun(\n {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n cursor: input.cursor ?? null,\n triggeredBy: input.triggeredBy ?? scope.userId ?? null,\n progressJobId: progressJob?.id ?? null,\n },\n {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n )\n\n const queueName = input.direction === 'import' ? 'data-sync-import' : 'data-sync-export'\n const queue = getSyncQueue(queueName)\n await queue.enqueue({\n runId: run.id,\n batchSize: input.batchSize ?? 100,\n scope: {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n userId: scope.userId ?? null,\n },\n })\n\n return {\n run,\n progressJob,\n }\n}\n"],
5
- "mappings": "AAEA,SAAS,oBAAoB;AAyB7B,eAAsB,iBAAiB,QAKpC;AACD,QAAM,EAAE,gBAAgB,iBAAiB,OAAO,MAAM,IAAI;AAC1D,QAAM,oBAAoB,MAAM,sBAAsB;AAEtD,QAAM,cAAc,oBAChB,MAAM,gBAAgB;AAAA,IACtB;AAAA,MACE,SAAS,MAAM,aAAa,WAAW,aAAa,MAAM,SAAS;AAAA,MACnE,MAAM,MAAM,aAAa,QAAQ,aAAa,MAAM,aAAa,WAAM,MAAM,UAAU;AAAA,MACvF,aAAa,MAAM,aAAa,eAAe,GAAG,MAAM,UAAU,IAAI,MAAM,SAAS;AAAA,MACrF,aAAa,MAAM,aAAa,eAAe;AAAA,MAC/C,MAAM;AAAA,QACJ,eAAe,MAAM;AAAA,QACrB,YAAY,MAAM;AAAA,QAClB,WAAW,MAAM;AAAA,QACjB,GAAI,MAAM,aAAa,QAAQ,CAAC;AAAA,MAClC;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,QAAQ,MAAM;AAAA,IAChB;AAAA,EACF,IACE;AAEJ,QAAM,MAAM,MAAM,eAAe;AAAA,IAC/B;AAAA,MACE,eAAe,MAAM;AAAA,MACrB,YAAY,MAAM;AAAA,MAClB,WAAW,MAAM;AAAA,MACjB,QAAQ,MAAM,UAAU;AAAA,MACxB,aAAa,MAAM,eAAe,MAAM,UAAU;AAAA,MAClD,eAAe,aAAa,MAAM;AAAA,IACpC;AAAA,IACA;AAAA,MACE,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,cAAc,WAAW,qBAAqB;AACtE,QAAM,QAAQ,aAAa,SAAS;AACpC,QAAM,MAAM,QAAQ;AAAA,IAClB,OAAO,IAAI;AAAA,IACX,WAAW,MAAM,aAAa;AAAA,IAC9B,OAAO;AAAA,MACL,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,MAChB,QAAQ,MAAM,UAAU;AAAA,IAC1B;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncRunService } from './sync-run-service'\nimport { getSyncQueue } from './queue'\nimport { DATA_SYNC_EXPORT_QUEUE, DATA_SYNC_IMPORT_QUEUE } from './queue-policy'\n\nexport type DataSyncStartScope = {\n organizationId: string\n tenantId: string\n userId?: string | null\n}\n\nexport type StartDataSyncRunInput = {\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n cursor?: string | null\n triggeredBy?: string | null\n batchSize?: number\n createProgressJob?: boolean\n progressJob?: {\n jobType?: string\n name?: string\n description?: string\n cancellable?: boolean\n meta?: Record<string, unknown>\n }\n}\n\nexport async function startDataSyncRun(params: {\n syncRunService: SyncRunService\n progressService: ProgressService\n scope: DataSyncStartScope\n input: StartDataSyncRunInput\n}) {\n const { syncRunService, progressService, scope, input } = params\n const createProgressJob = input.createProgressJob !== false\n\n const progressJob = createProgressJob\n ? await progressService.createJob(\n {\n jobType: input.progressJob?.jobType ?? `data_sync:${input.direction}`,\n name: input.progressJob?.name ?? `Data sync ${input.integrationId} \u2014 ${input.entityType}`,\n description: input.progressJob?.description ?? `${input.entityType} ${input.direction}`,\n cancellable: input.progressJob?.cancellable ?? true,\n meta: {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n ...(input.progressJob?.meta ?? {}),\n },\n },\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n : null\n\n const run = await syncRunService.createRun(\n {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n cursor: input.cursor ?? null,\n triggeredBy: input.triggeredBy ?? scope.userId ?? null,\n progressJobId: progressJob?.id ?? null,\n },\n {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n )\n\n const queueName = input.direction === 'import' ? DATA_SYNC_IMPORT_QUEUE : DATA_SYNC_EXPORT_QUEUE\n const queue = getSyncQueue(queueName)\n await queue.enqueue({\n runId: run.id,\n batchSize: input.batchSize ?? 100,\n scope: {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n userId: scope.userId ?? null,\n },\n })\n\n return {\n run,\n progressJob,\n }\n}\n"],
5
+ "mappings": "AAEA,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB,8BAA8B;AAyB/D,eAAsB,iBAAiB,QAKpC;AACD,QAAM,EAAE,gBAAgB,iBAAiB,OAAO,MAAM,IAAI;AAC1D,QAAM,oBAAoB,MAAM,sBAAsB;AAEtD,QAAM,cAAc,oBAChB,MAAM,gBAAgB;AAAA,IACtB;AAAA,MACE,SAAS,MAAM,aAAa,WAAW,aAAa,MAAM,SAAS;AAAA,MACnE,MAAM,MAAM,aAAa,QAAQ,aAAa,MAAM,aAAa,WAAM,MAAM,UAAU;AAAA,MACvF,aAAa,MAAM,aAAa,eAAe,GAAG,MAAM,UAAU,IAAI,MAAM,SAAS;AAAA,MACrF,aAAa,MAAM,aAAa,eAAe;AAAA,MAC/C,MAAM;AAAA,QACJ,eAAe,MAAM;AAAA,QACrB,YAAY,MAAM;AAAA,QAClB,WAAW,MAAM;AAAA,QACjB,GAAI,MAAM,aAAa,QAAQ,CAAC;AAAA,MAClC;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,QAAQ,MAAM;AAAA,IAChB;AAAA,EACF,IACE;AAEJ,QAAM,MAAM,MAAM,eAAe;AAAA,IAC/B;AAAA,MACE,eAAe,MAAM;AAAA,MACrB,YAAY,MAAM;AAAA,MAClB,WAAW,MAAM;AAAA,MACjB,QAAQ,MAAM,UAAU;AAAA,MACxB,aAAa,MAAM,eAAe,MAAM,UAAU;AAAA,MAClD,eAAe,aAAa,MAAM;AAAA,IACpC;AAAA,IACA;AAAA,MACE,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,cAAc,WAAW,yBAAyB;AAC1E,QAAM,QAAQ,aAAa,SAAS;AACpC,QAAM,MAAM,QAAQ;AAAA,IAClB,OAAO,IAAI;AAAA,IACX,WAAW,MAAM,aAAa;AAAA,IAC9B,OAAO;AAAA,MACL,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,MAChB,QAAQ,MAAM,UAAU;AAAA,IAC1B;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,7 @@ import { getIntegration } from "@open-mercato/shared/modules/integrations/types"
2
2
  import { refreshCoverageSnapshot } from "../../query_index/lib/coverage.js";
3
3
  import { emitDataSyncEvent } from "../events.js";
4
4
  import { getDataSyncAdapter } from "./adapter-registry.js";
5
+ import { SyncRunOwnershipConflictError } from "./sync-run-service.js";
5
6
  import { createLogger } from "@open-mercato/shared/lib/logger";
6
7
  const logger = createLogger("data_sync").child({ component: "sync-engine" });
7
8
  function resolveProviderKey(integrationId) {
@@ -143,6 +144,14 @@ function createSyncEngine(deps) {
143
144
  if (alreadyFinalizedWithSameStatus) {
144
145
  return;
145
146
  }
147
+ if (run.status !== status) {
148
+ logger.warn("Skipping finalization of a sync run another worker already finalized", {
149
+ runId,
150
+ requestedStatus: status,
151
+ actualStatus: run.status
152
+ });
153
+ return;
154
+ }
146
155
  if (run.progressJobId) {
147
156
  if (status === "completed") {
148
157
  await progressService.completeJob(
@@ -307,6 +316,7 @@ function createSyncEngine(deps) {
307
316
  if (!credentials) {
308
317
  throw new Error(`Integration ${run.integrationId} is missing credentials`);
309
318
  }
319
+ const resumed = run.status === "running";
310
320
  const activeRun = await syncRunService.markStatus(run.id, "running", scope);
311
321
  if (!activeRun || activeRun.status !== "running") {
312
322
  return;
@@ -316,6 +326,7 @@ function createSyncEngine(deps) {
316
326
  integrationId: run.integrationId,
317
327
  entityType: run.entityType,
318
328
  direction: run.direction,
329
+ resumed,
319
330
  tenantId: scope.tenantId,
320
331
  organizationId: scope.organizationId
321
332
  });
@@ -349,6 +360,7 @@ function createSyncEngine(deps) {
349
360
  const mapping = await resolveMapping(adapter, run.entityType, scope);
350
361
  let processedCount = 0;
351
362
  let totalCount = null;
363
+ let committedBatches = activeRun.batchesCompleted ?? 0;
352
364
  try {
353
365
  for await (const batch of adapter.streamImport({
354
366
  entityType: run.entityType,
@@ -374,8 +386,10 @@ function createSyncEngine(deps) {
374
386
  batchesCompleted: 1
375
387
  },
376
388
  batch.cursor,
377
- scope
389
+ scope,
390
+ committedBatches
378
391
  );
392
+ committedBatches += 1;
379
393
  await updateProgress(run.progressJobId, processedCount, totalCount, scope);
380
394
  await refreshCoverageSnapshots(batch.refreshCoverageEntityTypes, scope);
381
395
  await logImportItemFailures(run.id, run.integrationId, batch.items, scope);
@@ -397,6 +411,13 @@ function createSyncEngine(deps) {
397
411
  });
398
412
  }
399
413
  } catch (error) {
414
+ if (error instanceof SyncRunOwnershipConflictError) {
415
+ logger.warn("Yielding import run to a concurrent worker that already advanced it", {
416
+ runId: run.id,
417
+ expectedBatchesCompleted: error.expectedBatchesCompleted
418
+ });
419
+ return;
420
+ }
400
421
  const message = error instanceof Error ? error.message : "Sync import failed";
401
422
  await integrationLogService.write(
402
423
  {
@@ -438,6 +459,7 @@ function createSyncEngine(deps) {
438
459
  if (!credentials) {
439
460
  throw new Error(`Integration ${run.integrationId} is missing credentials`);
440
461
  }
462
+ const resumed = run.status === "running";
441
463
  const activeRun = await syncRunService.markStatus(run.id, "running", scope);
442
464
  if (!activeRun || activeRun.status !== "running") {
443
465
  return;
@@ -447,6 +469,7 @@ function createSyncEngine(deps) {
447
469
  integrationId: run.integrationId,
448
470
  entityType: run.entityType,
449
471
  direction: run.direction,
472
+ resumed,
450
473
  tenantId: scope.tenantId,
451
474
  organizationId: scope.organizationId
452
475
  });
@@ -479,6 +502,7 @@ function createSyncEngine(deps) {
479
502
  }
480
503
  const mapping = await resolveMapping(adapter, run.entityType, scope);
481
504
  let processedCount = 0;
505
+ let committedBatches = activeRun.batchesCompleted ?? 0;
482
506
  try {
483
507
  for await (const batch of adapter.streamExport({
484
508
  entityType: run.entityType,
@@ -505,8 +529,10 @@ function createSyncEngine(deps) {
505
529
  batchesCompleted: 1
506
530
  },
507
531
  batch.cursor,
508
- scope
532
+ scope,
533
+ committedBatches
509
534
  );
535
+ committedBatches += 1;
510
536
  await updateProgress(run.progressJobId, processedCount, null, scope);
511
537
  await logExportItemFailures(run.id, run.integrationId, batch.results, scope);
512
538
  await writeOperationalLog({
@@ -526,6 +552,13 @@ function createSyncEngine(deps) {
526
552
  });
527
553
  }
528
554
  } catch (error) {
555
+ if (error instanceof SyncRunOwnershipConflictError) {
556
+ logger.warn("Yielding export run to a concurrent worker that already advanced it", {
557
+ runId: run.id,
558
+ expectedBatchesCompleted: error.expectedBatchesCompleted
559
+ });
560
+ return;
561
+ }
529
562
  const message = error instanceof Error ? error.message : "Sync export failed";
530
563
  await integrationLogService.write(
531
564
  {