@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/resources_resource/index.js +2 -0
- package/dist/generated/entities/resources_resource/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/salesFixtures.js +31 -2
- package/dist/helpers/integration/salesFixtures.js.map +2 -2
- package/dist/helpers/integration/salesUi.js +5 -0
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/auth/api/profile/route.js +8 -2
- package/dist/modules/auth/api/profile/route.js.map +2 -2
- package/dist/modules/auth/backend/auth/profile/page.js.map +2 -2
- package/dist/modules/auth/backend/profile/change-password/page.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +4 -4
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/customers/analytics.js +1 -0
- package/dist/modules/customers/analytics.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +2 -1
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +1 -1
- package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
- package/dist/modules/customers/data/validators.js +3 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +52 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +42 -26
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +2 -2
- package/dist/modules/data_sync/lib/queue-policy.js +15 -0
- package/dist/modules/data_sync/lib/queue-policy.js.map +7 -0
- package/dist/modules/data_sync/lib/queue.js +16 -1
- package/dist/modules/data_sync/lib/queue.js.map +2 -2
- package/dist/modules/data_sync/lib/start-run.js +2 -1
- package/dist/modules/data_sync/lib/start-run.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-engine.js +35 -2
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-run-service.js +70 -2
- package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-export.js +9 -2
- package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-import.js +9 -2
- package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +34 -12
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
- package/dist/modules/dictionaries/api/openapi.js +15 -2
- package/dist/modules/dictionaries/api/openapi.js.map +2 -2
- package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js +30 -29
- package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js.map +2 -2
- package/dist/modules/dictionaries/data/validators.js +7 -0
- package/dist/modules/dictionaries/data/validators.js.map +2 -2
- package/dist/modules/dictionaries/lib/clientEntries.js +4 -3
- package/dist/modules/dictionaries/lib/clientEntries.js.map +2 -2
- package/dist/modules/dictionaries/lib/fetchAllEntries.js +43 -0
- package/dist/modules/dictionaries/lib/fetchAllEntries.js.map +7 -0
- package/dist/modules/entities/api/definitions.js +51 -3
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/api/entities.js +21 -2
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +3 -24
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/api/sidebar-entities.js +34 -12
- package/dist/modules/entities/api/sidebar-entities.js.map +2 -2
- package/dist/modules/entities/lib/entityAcl.js +46 -0
- package/dist/modules/entities/lib/entityAcl.js.map +2 -2
- package/dist/modules/messages/api/route.js +2 -5
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/messages/lib/participantScope.js +15 -0
- package/dist/modules/messages/lib/participantScope.js.map +7 -0
- package/dist/modules/notifications/api/route.js +3 -1
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/notifications/api/unread-count/route.js +25 -6
- package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
- package/dist/modules/notifications/lib/notificationScope.js +26 -0
- package/dist/modules/notifications/lib/notificationScope.js.map +7 -0
- package/dist/modules/notifications/lib/notificationService.js +7 -3
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/lib/routeHelpers.js +16 -2
- package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
- package/dist/modules/planner/components/unavailabilityReasons.js +3 -3
- package/dist/modules/planner/components/unavailabilityReasons.js.map +2 -2
- package/dist/modules/progress/lib/progressService.js +2 -0
- package/dist/modules/progress/lib/progressService.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +322 -104
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/resources/api/resources.js +2 -0
- package/dist/modules/resources/api/resources.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +3 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/resources/commands/resources.js +9 -0
- package/dist/modules/resources/commands/resources.js.map +2 -2
- package/dist/modules/resources/components/detail/dictionaries.js +3 -3
- package/dist/modules/resources/components/detail/dictionaries.js.map +2 -2
- package/dist/modules/resources/data/entities.js +3 -0
- package/dist/modules/resources/data/entities.js.map +2 -2
- package/dist/modules/resources/data/validators.js +6 -2
- package/dist/modules/resources/data/validators.js.map +2 -2
- package/dist/modules/resources/lib/seeds.js +16 -9
- package/dist/modules/resources/lib/seeds.js.map +2 -2
- package/dist/modules/resources/migrations/Migration20260608231000.js +13 -0
- package/dist/modules/resources/migrations/Migration20260608231000.js.map +7 -0
- package/dist/modules/sales/backend/sales/documents/create/page.js +11 -14
- package/dist/modules/sales/backend/sales/documents/create/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +8 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +18 -9
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +12 -5
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +48 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +167 -0
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +7 -0
- package/dist/modules/sales/data/validators.js +5 -1
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/staff/components/detail/dictionaries.js +3 -3
- package/dist/modules/staff/components/detail/dictionaries.js.map +2 -2
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +11 -4
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +2 -2
- package/generated/entities/resources_resource/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/helpers/integration/salesFixtures.ts +34 -2
- package/src/helpers/integration/salesUi.ts +8 -0
- package/src/modules/auth/api/profile/route.ts +8 -2
- package/src/modules/auth/backend/auth/profile/page.tsx +1 -0
- package/src/modules/auth/backend/profile/change-password/page.tsx +1 -0
- package/src/modules/communication_channels/data/enrichers.ts +6 -26
- package/src/modules/customers/analytics.ts +1 -0
- package/src/modules/customers/components/detail/DealForm.tsx +5 -1
- package/src/modules/customers/components/detail/DealsSection.tsx +1 -1
- package/src/modules/customers/data/validators.ts +5 -1
- package/src/modules/dashboards/i18n/de.json +3 -0
- package/src/modules/dashboards/i18n/en.json +3 -0
- package/src/modules/dashboards/i18n/es.json +3 -0
- package/src/modules/dashboards/i18n/pl.json +3 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +86 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +33 -18
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +2 -2
- package/src/modules/data_sync/lib/adapter.ts +18 -0
- package/src/modules/data_sync/lib/queue-policy.ts +30 -0
- package/src/modules/data_sync/lib/queue.ts +19 -1
- package/src/modules/data_sync/lib/start-run.ts +2 -1
- package/src/modules/data_sync/lib/sync-engine.ts +46 -0
- package/src/modules/data_sync/lib/sync-run-service.ts +82 -1
- package/src/modules/data_sync/workers/sync-export.ts +8 -1
- package/src/modules/data_sync/workers/sync-import.ts +8 -1
- package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +39 -11
- package/src/modules/dictionaries/api/openapi.ts +19 -0
- package/src/modules/dictionaries/components/hooks/useDictionaryEntries.ts +35 -36
- package/src/modules/dictionaries/data/validators.ts +14 -0
- package/src/modules/dictionaries/lib/clientEntries.ts +4 -3
- package/src/modules/dictionaries/lib/fetchAllEntries.ts +72 -0
- package/src/modules/entities/api/definitions.ts +51 -3
- package/src/modules/entities/api/entities.ts +30 -2
- package/src/modules/entities/api/records.ts +3 -27
- package/src/modules/entities/api/sidebar-entities.ts +43 -14
- package/src/modules/entities/lib/entityAcl.ts +58 -0
- package/src/modules/messages/api/route.ts +4 -5
- package/src/modules/messages/lib/participantScope.ts +64 -0
- package/src/modules/notifications/api/route.ts +2 -0
- package/src/modules/notifications/api/unread-count/route.ts +42 -5
- package/src/modules/notifications/lib/notificationScope.ts +40 -0
- package/src/modules/notifications/lib/notificationService.ts +5 -0
- package/src/modules/notifications/lib/routeHelpers.ts +19 -2
- package/src/modules/planner/components/unavailabilityReasons.ts +3 -3
- package/src/modules/progress/AGENTS.md +11 -1
- package/src/modules/progress/lib/progressService.ts +1 -0
- package/src/modules/progress/lib/progressServiceImpl.ts +385 -117
- package/src/modules/resources/api/resources.ts +2 -0
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +9 -3
- package/src/modules/resources/commands/resources.ts +10 -0
- package/src/modules/resources/components/detail/dictionaries.ts +3 -3
- package/src/modules/resources/data/entities.ts +3 -0
- package/src/modules/resources/data/validators.ts +5 -0
- package/src/modules/resources/lib/seeds.ts +16 -9
- package/src/modules/resources/migrations/.snapshot-open-mercato.json +17 -1
- package/src/modules/resources/migrations/Migration20260608231000.ts +13 -0
- package/src/modules/sales/backend/sales/documents/create/page.tsx +2 -4
- package/src/modules/sales/commands/documents.ts +8 -0
- package/src/modules/sales/components/documents/ItemsSection.tsx +19 -10
- package/src/modules/sales/components/documents/LineItemDialog.tsx +13 -5
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +48 -6
- package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +198 -0
- package/src/modules/sales/data/validators.ts +7 -1
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
- package/src/modules/staff/components/detail/dictionaries.ts +3 -3
- 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;
|
|
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 {
|
|
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
|
-
|
|
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__ */
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 {
|
|
5
|
-
"mappings": ";
|
|
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
|
|
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
|
|
5
|
-
"mappings": "AAAA,SAAS,2BAAuD;AAChE,SAAS,kBAAkB,uBAAqD;
|
|
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(
|
|
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
|
|
5
|
-
"mappings": "AAAA,SAAS,yBAAqC;
|
|
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" ?
|
|
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' ?
|
|
5
|
-
"mappings": "AAEA,SAAS,oBAAoB;
|
|
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
|
{
|