@open-mercato/core 0.6.6-develop.6257.1.6d0af84d26 → 0.6.6-develop.6290.1.4bb5a8ba3f

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 (220) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/carrier_shipment_idempotency_key/index.js +19 -0
  3. package/dist/generated/entities/carrier_shipment_idempotency_key/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +2 -1
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +10 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/catalog/lib/productResolution.js +45 -0
  9. package/dist/modules/catalog/lib/productResolution.js.map +7 -0
  10. package/dist/modules/communication_channels/workers/channel-import-history.js +1 -1
  11. package/dist/modules/communication_channels/workers/channel-import-history.js.map +2 -2
  12. package/dist/modules/currencies/api/fetch-configs/route.js +34 -0
  13. package/dist/modules/currencies/api/fetch-configs/route.js.map +2 -2
  14. package/dist/modules/currencies/components/CurrencyFetchingConfig.js +17 -15
  15. package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
  16. package/dist/modules/customers/api/companies/[id]/route.js +176 -161
  17. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  18. package/dist/modules/customers/api/people/[id]/route.js +75 -59
  19. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  20. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +17 -10
  21. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
  22. package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js +6 -4
  23. package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js.map +2 -2
  24. package/dist/modules/customers/components/detail/AiActionChips.js +5 -2
  25. package/dist/modules/customers/components/detail/AiActionChips.js.map +2 -2
  26. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +19 -21
  27. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
  28. package/dist/modules/data_sync/api/runs/[id]/cancel.js +1 -1
  29. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  30. package/dist/modules/data_sync/backend/data-sync/page.js +9 -45
  31. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  32. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +11 -17
  33. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  34. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +12 -10
  35. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
  36. package/dist/modules/data_sync/lib/id-mapping.js +46 -1
  37. package/dist/modules/data_sync/lib/id-mapping.js.map +2 -2
  38. package/dist/modules/data_sync/lib/sync-engine.js +2 -2
  39. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  40. package/dist/modules/data_sync/lib/syncRunStatus.js +30 -0
  41. package/dist/modules/data_sync/lib/syncRunStatus.js.map +7 -0
  42. package/dist/modules/data_sync/workers/sync-scheduled.js +13 -14
  43. package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
  44. package/dist/modules/directory/components/TenantSelect.js +29 -16
  45. package/dist/modules/directory/components/TenantSelect.js.map +2 -2
  46. package/dist/modules/entities/api/records.js +6 -0
  47. package/dist/modules/entities/api/records.js.map +2 -2
  48. package/dist/modules/feature_toggles/api/global/route.js +17 -15
  49. package/dist/modules/feature_toggles/api/global/route.js.map +2 -2
  50. package/dist/modules/feature_toggles/api/openapi.js +2 -2
  51. package/dist/modules/feature_toggles/api/openapi.js.map +2 -2
  52. package/dist/modules/feature_toggles/commands/global.js +14 -0
  53. package/dist/modules/feature_toggles/commands/global.js.map +2 -2
  54. package/dist/modules/inbox_ops/backend/inbox-ops/page.js +6 -0
  55. package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
  56. package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js +3 -1
  57. package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js.map +2 -2
  58. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +12 -5
  59. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
  60. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js +10 -1
  61. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js.map +2 -2
  62. package/dist/modules/payment_gateways/api/cancel/route.js +5 -0
  63. package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
  64. package/dist/modules/payment_gateways/api/capture/route.js +5 -0
  65. package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
  66. package/dist/modules/payment_gateways/api/refund/route.js +5 -0
  67. package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
  68. package/dist/modules/payment_gateways/lib/gateway-service.js +52 -25
  69. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  70. package/dist/modules/payment_gateways/lib/status-machine.js +13 -0
  71. package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
  72. package/dist/modules/perspectives/api/[tableId]/route.js +8 -2
  73. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  74. package/dist/modules/perspectives/data/entities.js +33 -3
  75. package/dist/modules/perspectives/data/entities.js.map +2 -2
  76. package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js +63 -0
  77. package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js.map +7 -0
  78. package/dist/modules/perspectives/services/perspectiveService.js +17 -0
  79. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  80. package/dist/modules/planner/commands/availability-weekly.js +33 -1
  81. package/dist/modules/planner/commands/availability-weekly.js.map +2 -2
  82. package/dist/modules/planner/components/AvailabilityRulesEditor.js +14 -2
  83. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  84. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/hiddenWidgetsStorage.js +35 -0
  85. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/hiddenWidgetsStorage.js.map +7 -0
  86. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +18 -19
  87. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
  88. package/dist/modules/progress/api/active/route.js +1 -1
  89. package/dist/modules/progress/api/active/route.js.map +2 -2
  90. package/dist/modules/progress/lib/progressService.js.map +1 -1
  91. package/dist/modules/progress/lib/progressServiceImpl.js +33 -8
  92. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  93. package/dist/modules/query_index/api/status.js +19 -24
  94. package/dist/modules/query_index/api/status.js.map +3 -3
  95. package/dist/modules/query_index/lib/coverage.js +35 -0
  96. package/dist/modules/query_index/lib/coverage.js.map +2 -2
  97. package/dist/modules/query_index/lib/engine.js +11 -6
  98. package/dist/modules/query_index/lib/engine.js.map +2 -2
  99. package/dist/modules/resources/api/resource-types.js +5 -37
  100. package/dist/modules/resources/api/resource-types.js.map +2 -2
  101. package/dist/modules/resources/api/resourceTypeCounts.js +35 -0
  102. package/dist/modules/resources/api/resourceTypeCounts.js.map +7 -0
  103. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +1 -1
  104. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +1 -1
  105. package/dist/modules/resources/backend/resources/resource-types/page.js +2 -1
  106. package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
  107. package/dist/modules/sales/components/documents/ReturnDialog.js +4 -1
  108. package/dist/modules/sales/components/documents/ReturnDialog.js.map +2 -2
  109. package/dist/modules/sales/components/documents/ShipmentDialog.js +17 -2
  110. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  111. package/dist/modules/sales/di.js +4 -0
  112. package/dist/modules/sales/di.js.map +2 -2
  113. package/dist/modules/sales/services/salesOrderService.js +26 -0
  114. package/dist/modules/sales/services/salesOrderService.js.map +7 -0
  115. package/dist/modules/shipping_carriers/api/shipments/route.js +8 -0
  116. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  117. package/dist/modules/shipping_carriers/data/entities.js +38 -0
  118. package/dist/modules/shipping_carriers/data/entities.js.map +2 -2
  119. package/dist/modules/shipping_carriers/data/validators.js +2 -1
  120. package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
  121. package/dist/modules/shipping_carriers/lib/shipment-idempotency.js +76 -0
  122. package/dist/modules/shipping_carriers/lib/shipment-idempotency.js.map +7 -0
  123. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js +2 -1
  124. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js.map +2 -2
  125. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +20 -2
  126. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
  127. package/dist/modules/shipping_carriers/lib/shipping-service.js +82 -43
  128. package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
  129. package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js +14 -0
  130. package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js.map +7 -0
  131. package/dist/modules/staff/lib/timesheets-ui/ListView.js +15 -6
  132. package/dist/modules/staff/lib/timesheets-ui/ListView.js.map +2 -2
  133. package/dist/modules/translations/components/TranslationManager.js +9 -18
  134. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  135. package/generated/entities/carrier_shipment_idempotency_key/index.ts +8 -0
  136. package/generated/entities.ids.generated.ts +2 -1
  137. package/generated/entity-fields-registry.ts +10 -0
  138. package/package.json +9 -8
  139. package/src/modules/catalog/lib/productResolution.ts +75 -0
  140. package/src/modules/communication_channels/workers/channel-import-history.ts +1 -1
  141. package/src/modules/currencies/api/fetch-configs/route.ts +36 -0
  142. package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +20 -17
  143. package/src/modules/currencies/i18n/de.json +2 -1
  144. package/src/modules/currencies/i18n/en.json +2 -1
  145. package/src/modules/currencies/i18n/es.json +2 -1
  146. package/src/modules/currencies/i18n/pl.json +2 -1
  147. package/src/modules/customers/api/companies/[id]/route.ts +214 -189
  148. package/src/modules/customers/api/people/[id]/route.ts +106 -80
  149. package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +19 -12
  150. package/src/modules/customers/components/detail/ActivitiesAddNewMenu.tsx +8 -6
  151. package/src/modules/customers/components/detail/AiActionChips.tsx +6 -3
  152. package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +16 -18
  153. package/src/modules/data_sync/api/runs/[id]/cancel.ts +1 -1
  154. package/src/modules/data_sync/backend/data-sync/page.tsx +11 -58
  155. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +13 -20
  156. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +10 -8
  157. package/src/modules/data_sync/lib/id-mapping.ts +58 -1
  158. package/src/modules/data_sync/lib/sync-engine.ts +2 -2
  159. package/src/modules/data_sync/lib/syncRunStatus.ts +39 -0
  160. package/src/modules/data_sync/workers/sync-scheduled.ts +13 -14
  161. package/src/modules/directory/components/TenantSelect.tsx +33 -16
  162. package/src/modules/entities/api/records.ts +14 -0
  163. package/src/modules/feature_toggles/api/global/route.ts +16 -14
  164. package/src/modules/feature_toggles/api/openapi.ts +2 -2
  165. package/src/modules/feature_toggles/commands/global.ts +15 -0
  166. package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +7 -0
  167. package/src/modules/messages/components/message-detail/hooks/useMessageDetails.ts +2 -0
  168. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +14 -4
  169. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.ts +15 -1
  170. package/src/modules/payment_gateways/api/cancel/route.ts +5 -0
  171. package/src/modules/payment_gateways/api/capture/route.ts +5 -0
  172. package/src/modules/payment_gateways/api/refund/route.ts +5 -0
  173. package/src/modules/payment_gateways/lib/gateway-service.ts +58 -25
  174. package/src/modules/payment_gateways/lib/status-machine.ts +16 -0
  175. package/src/modules/perspectives/api/[tableId]/route.ts +8 -2
  176. package/src/modules/perspectives/data/entities.ts +43 -3
  177. package/src/modules/perspectives/migrations/.snapshot-open-mercato.json +69 -25
  178. package/src/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.ts +65 -0
  179. package/src/modules/perspectives/services/perspectiveService.ts +17 -0
  180. package/src/modules/planner/commands/availability-weekly.ts +48 -1
  181. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +31 -10
  182. package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/hiddenWidgetsStorage.ts +36 -0
  183. package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +22 -23
  184. package/src/modules/progress/api/active/route.ts +1 -1
  185. package/src/modules/progress/lib/progressService.ts +2 -2
  186. package/src/modules/progress/lib/progressServiceImpl.ts +33 -8
  187. package/src/modules/query_index/api/status.ts +28 -32
  188. package/src/modules/query_index/lib/coverage.ts +63 -0
  189. package/src/modules/query_index/lib/engine.ts +37 -24
  190. package/src/modules/resources/api/resource-types.ts +4 -44
  191. package/src/modules/resources/api/resourceTypeCounts.ts +74 -0
  192. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +1 -1
  193. package/src/modules/resources/backend/resources/resource-types/page.tsx +1 -0
  194. package/src/modules/sales/components/documents/ReturnDialog.tsx +4 -1
  195. package/src/modules/sales/components/documents/ShipmentDialog.tsx +20 -2
  196. package/src/modules/sales/di.ts +6 -0
  197. package/src/modules/sales/services/salesOrderService.ts +38 -0
  198. package/src/modules/shipping_carriers/api/shipments/route.ts +8 -0
  199. package/src/modules/shipping_carriers/data/entities.ts +33 -0
  200. package/src/modules/shipping_carriers/data/validators.ts +1 -0
  201. package/src/modules/shipping_carriers/i18n/de.json +1 -0
  202. package/src/modules/shipping_carriers/i18n/en.json +1 -0
  203. package/src/modules/shipping_carriers/i18n/es.json +1 -0
  204. package/src/modules/shipping_carriers/i18n/pl.json +1 -0
  205. package/src/modules/shipping_carriers/lib/shipment-idempotency.ts +119 -0
  206. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.ts +3 -1
  207. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +24 -2
  208. package/src/modules/shipping_carriers/lib/shipping-service.ts +87 -43
  209. package/src/modules/shipping_carriers/migrations/.snapshot-open-mercato.json +197 -0
  210. package/src/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.ts +14 -0
  211. package/src/modules/staff/i18n/de.json +1 -0
  212. package/src/modules/staff/i18n/en.json +1 -0
  213. package/src/modules/staff/i18n/es.json +1 -0
  214. package/src/modules/staff/i18n/pl.json +1 -0
  215. package/src/modules/staff/lib/timesheets-ui/ListView.tsx +15 -7
  216. package/src/modules/translations/components/TranslationManager.tsx +17 -24
  217. package/src/modules/translations/i18n/de.json +1 -0
  218. package/src/modules/translations/i18n/en.json +1 -0
  219. package/src/modules/translations/i18n/es.json +1 -0
  220. package/src/modules/translations/i18n/pl.json +1 -0
@@ -0,0 +1,45 @@
1
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
2
+ import { CatalogProduct } from "../data/entities.js";
3
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4
+ function normalizeLookupValue(value) {
5
+ return typeof value === "string" ? value.trim() : "";
6
+ }
7
+ async function resolveProductBySkuOrExternalId(em, lookup, scope) {
8
+ const externalId = normalizeLookupValue(lookup.externalId);
9
+ const sku = normalizeLookupValue(lookup.sku);
10
+ if (!externalId && !sku) return null;
11
+ if (externalId && UUID_REGEX.test(externalId)) {
12
+ const byExternalId = await findOneWithDecryption(
13
+ em,
14
+ CatalogProduct,
15
+ {
16
+ id: externalId,
17
+ organizationId: scope.organizationId,
18
+ tenantId: scope.tenantId,
19
+ deletedAt: null
20
+ },
21
+ void 0,
22
+ scope
23
+ );
24
+ if (byExternalId) return byExternalId;
25
+ }
26
+ if (sku) {
27
+ return findOneWithDecryption(
28
+ em,
29
+ CatalogProduct,
30
+ {
31
+ sku,
32
+ organizationId: scope.organizationId,
33
+ tenantId: scope.tenantId,
34
+ deletedAt: null
35
+ },
36
+ void 0,
37
+ scope
38
+ );
39
+ }
40
+ return null;
41
+ }
42
+ export {
43
+ resolveProductBySkuOrExternalId
44
+ };
45
+ //# sourceMappingURL=productResolution.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/catalog/lib/productResolution.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CatalogProduct } from '../data/entities'\n\nconst UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n\nexport type ResolveCatalogProductLookup = {\n sku?: string | null\n externalId?: string | null\n}\n\nexport type ResolveCatalogProductScope = {\n organizationId: string\n tenantId: string\n}\n\nfunction normalizeLookupValue(value: string | null | undefined): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\n/**\n * Public seam for cross-module callers (e.g. the `sync_magento` order mapper) to look up a\n * {@link CatalogProduct} without reaching into the catalog ORM directly. Uses\n * `findOneWithDecryption` so encrypted product fields are respected, and always scopes the query by\n * `organizationId` + `tenantId` plus `deletedAt: null`.\n *\n * Catalog does not store a foreign external id on the product \u2014 those mappings live per-integration\n * in `SyncExternalIdMapping` (data_sync) and resolve to the local OM product id. `externalId` here is\n * therefore matched against the OM product `id` (the value an external\u2192local mapping yields) and only\n * when it is a valid UUID, so a stray non-UUID value falls through to the SKU lookup instead of\n * raising a Postgres uuid-cast error. When both identifiers are supplied the `externalId` match takes\n * precedence, mirroring the mapping-first/SKU-fallback order used by the existing sync importers.\n */\nexport async function resolveProductBySkuOrExternalId(\n em: EntityManager,\n lookup: ResolveCatalogProductLookup,\n scope: ResolveCatalogProductScope,\n): Promise<CatalogProduct | null> {\n const externalId = normalizeLookupValue(lookup.externalId)\n const sku = normalizeLookupValue(lookup.sku)\n if (!externalId && !sku) return null\n\n if (externalId && UUID_REGEX.test(externalId)) {\n const byExternalId = await findOneWithDecryption(\n em,\n CatalogProduct,\n {\n id: externalId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (byExternalId) return byExternalId\n }\n\n if (sku) {\n return findOneWithDecryption(\n em,\n CatalogProduct,\n {\n sku,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n }\n\n return null\n}\n"],
5
+ "mappings": "AACA,SAAS,6BAA6B;AACtC,SAAS,sBAAsB;AAE/B,MAAM,aAAa;AAYnB,SAAS,qBAAqB,OAA0C;AACtE,SAAO,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;AACpD;AAeA,eAAsB,gCACpB,IACA,QACA,OACgC;AAChC,QAAM,aAAa,qBAAqB,OAAO,UAAU;AACzD,QAAM,MAAM,qBAAqB,OAAO,GAAG;AAC3C,MAAI,CAAC,cAAc,CAAC,IAAK,QAAO;AAEhC,MAAI,cAAc,WAAW,KAAK,UAAU,GAAG;AAC7C,UAAM,eAAe,MAAM;AAAA,MACzB;AAAA,MACA;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,QAChB,WAAW;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,QAAI,aAAc,QAAO;AAAA,EAC3B;AAEA,MAAI,KAAK;AACP,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,QACE;AAAA,QACA,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,QAChB,WAAW;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -101,7 +101,7 @@ async function handle(job, ctx) {
101
101
  let firstPage = true;
102
102
  const MAX_PAGES = 100;
103
103
  for (let pageIndex = 0; pageIndex < MAX_PAGES; pageIndex += 1) {
104
- if (await progressService.isCancellationRequested(progressJobId, scope.tenantId)) {
104
+ if (await progressService.isCancellationRequested(progressJobId, scope.tenantId, scope.organizationId)) {
105
105
  await progressService.markCancelled(progressJobId, progressContext);
106
106
  return;
107
107
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/communication_channels/workers/channel-import-history.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CommunicationChannel } from '../data/entities'\nimport {\n COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,\n type IngestInboundMessageInput,\n} from '../commands/ingest-inbound-message'\nimport { COMMUNICATION_CHANNELS_QUEUES } from '../lib/queue'\nimport { refreshCredentialsIfNeeded } from '../lib/credential-refresh'\nimport { classifyOutboundError } from '../lib/error-classification'\nimport type { ChannelAdapterRegistry } from '../lib/registry'\nimport type {\n ChannelImportHistoryJobPayload,\n} from '../commands/queue-import-history'\nimport type {\n ProgressService,\n ProgressServiceContext,\n} from '../../progress/lib/progressService'\n\n/**\n * Spec B \u00A7 Phase B6 \u2014 operator-triggered backlog import worker.\n *\n * Distinct from `poll-channel` (which runs every scheduler tick and ingests\n * *new* mail since the channel cursor). This worker reaches backward in time\n * by calling `adapter.importHistory` with explicit `sinceDays` / `contactEmails`\n * filters, paginating until the adapter signals `hasMore: false` or the\n * `maxMessages` cap is reached.\n *\n * Runs with `concurrency: 1` to avoid hammering the provider with multiple\n * historical sweeps in parallel; per-channel concurrency is additionally\n * enforced at enqueue time by `queueImportHistory`.\n */\nexport const metadata: WorkerMeta = {\n queue: COMMUNICATION_CHANNELS_QUEUES.importHistory,\n id: 'communication_channels:channel-import-history',\n concurrency: 1,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\ntype CredentialsServiceLike = {\n resolve: (\n integrationId: string,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n save?: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<void>\n}\n\nexport default async function handle(\n job: QueuedJob<ChannelImportHistoryJobPayload>,\n ctx: HandlerContext,\n): Promise<void> {\n const payload = job.payload\n const { progressJobId, channelId, sinceDays, contactEmails, maxMessages, scope } = payload\n\n const progressService = ctx.resolve<ProgressService>('progressService')\n const progressContext: ProgressServiceContext = {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n }\n\n const em = (ctx.resolve('em') as EntityManager).fork()\n const adapterRegistry = ctx.resolve<ChannelAdapterRegistry>('channelAdapterRegistry')\n\n try {\n await progressService.startJob(progressJobId, progressContext)\n\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: channelId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!channel) {\n await progressService.failJob(\n progressJobId,\n { errorMessage: 'Channel not found' },\n progressContext,\n )\n return\n }\n if (!channel.isActive || channel.status !== 'connected') {\n await progressService.failJob(\n progressJobId,\n { errorMessage: `Channel is not connected (status=${channel.status})` },\n progressContext,\n )\n return\n }\n\n const adapter = adapterRegistry?.get(channel.providerKey)\n if (!adapter || typeof adapter.importHistory !== 'function') {\n await progressService.failJob(\n progressJobId,\n {\n errorMessage: `Provider \"${channel.providerKey}\" does not support history import`,\n },\n progressContext,\n )\n return\n }\n\n // Resolve + refresh credentials. Same flow as poll-channel.\n let credentialsService: CredentialsServiceLike | null = null\n try {\n credentialsService = ctx.resolve<CredentialsServiceLike>('integrationCredentialsService')\n } catch {\n credentialsService = null\n }\n const credentialsScope = {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: channel.userId ?? null,\n }\n let credentials: Record<string, unknown> = {}\n if (channel.credentialsRef && credentialsService) {\n try {\n credentials =\n (await credentialsService.resolve(`channel_${channel.providerKey}`, credentialsScope)) ?? {}\n } catch {\n credentials = {}\n }\n }\n const refreshed = await refreshCredentialsIfNeeded(\n { adapter, channelId: channel.id, credentials, scope: credentialsScope },\n { credentialsService },\n )\n credentials = refreshed.credentials\n\n const commandBus = ctx.resolve<CommandBus>('commandBus')\n const containerProxy = { resolve: ctx.resolve.bind(ctx) }\n const commandCtx = {\n container: containerProxy as never,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId,\n organizationIds: [scope.organizationId],\n }\n\n let cursor: string | undefined\n let processedCount = 0\n let totalCount = maxMessages\n let firstPage = true\n\n // Bounded loop guards against a misbehaving adapter (infinite hasMore).\n // 100 pages * HARD_CAP (default 200) = 20k messages \u2014 far above the 5k cap\n // enforced by the schema, so this only trips on adapter bugs.\n const MAX_PAGES = 100\n for (let pageIndex = 0; pageIndex < MAX_PAGES; pageIndex += 1) {\n if (await progressService.isCancellationRequested(progressJobId, scope.tenantId)) {\n await progressService.markCancelled(progressJobId, progressContext)\n return\n }\n\n const page = await adapter.importHistory!({\n credentials,\n scope: { tenantId: scope.tenantId, organizationId: scope.organizationId },\n sinceDays,\n contactEmails,\n maxMessages,\n cursor,\n })\n\n if (firstPage && typeof page.totalCandidates === 'number') {\n totalCount = Math.min(maxMessages, page.totalCandidates)\n await progressService.updateProgress(\n progressJobId,\n { totalCount, processedCount: 0 },\n progressContext,\n )\n firstPage = false\n }\n\n for (const message of page.messages) {\n if (processedCount >= maxMessages) break\n try {\n const input: IngestInboundMessageInput = {\n channelId: channel.id,\n providerKey: channel.providerKey,\n channelType: channel.channelType,\n scope: { tenantId: scope.tenantId, organizationId: scope.organizationId },\n message,\n }\n await commandBus.execute(COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID, {\n input,\n ctx: commandCtx as never,\n })\n } catch (err) {\n // Import-history is best-effort per-message. Same classification\n // logic as poll-channel: a transient failure aborts the whole job\n // (the operator can retry), permanent failures are logged and\n // skipped (the bad message would loop forever otherwise).\n const classification = classifyOutboundError(err)\n if (classification.transient) {\n throw err\n }\n console.warn(\n `[communication_channels:channel-import-history] permanent ingest failure on channel ${channel.id}; skipping message ${message.externalMessageId}. Reason: ${classification.message}`,\n )\n }\n processedCount += 1\n }\n\n await progressService.updateProgress(\n progressJobId,\n { processedCount, totalCount },\n progressContext,\n )\n\n if (processedCount >= maxMessages) break\n if (!page.hasMore || !page.nextCursor) break\n cursor = page.nextCursor\n }\n\n await progressService.completeJob(\n progressJobId,\n { resultSummary: { importedCount: processedCount, channelId } },\n progressContext,\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Import history job failed'\n const stack = err instanceof Error ? err.stack : undefined\n try {\n await progressService.failJob(\n progressJobId,\n { errorMessage: message.slice(0, 2000), errorStack: stack?.slice(0, 10000) },\n progressContext,\n )\n } catch (failErr) {\n console.error(\n `[communication_channels:channel-import-history] failed to mark progress job ${progressJobId} as failed: ${\n failErr instanceof Error ? failErr.message : String(failErr)\n }`,\n )\n }\n throw err\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CommunicationChannel } from '../data/entities'\nimport {\n COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,\n type IngestInboundMessageInput,\n} from '../commands/ingest-inbound-message'\nimport { COMMUNICATION_CHANNELS_QUEUES } from '../lib/queue'\nimport { refreshCredentialsIfNeeded } from '../lib/credential-refresh'\nimport { classifyOutboundError } from '../lib/error-classification'\nimport type { ChannelAdapterRegistry } from '../lib/registry'\nimport type {\n ChannelImportHistoryJobPayload,\n} from '../commands/queue-import-history'\nimport type {\n ProgressService,\n ProgressServiceContext,\n} from '../../progress/lib/progressService'\n\n/**\n * Spec B \u00A7 Phase B6 \u2014 operator-triggered backlog import worker.\n *\n * Distinct from `poll-channel` (which runs every scheduler tick and ingests\n * *new* mail since the channel cursor). This worker reaches backward in time\n * by calling `adapter.importHistory` with explicit `sinceDays` / `contactEmails`\n * filters, paginating until the adapter signals `hasMore: false` or the\n * `maxMessages` cap is reached.\n *\n * Runs with `concurrency: 1` to avoid hammering the provider with multiple\n * historical sweeps in parallel; per-channel concurrency is additionally\n * enforced at enqueue time by `queueImportHistory`.\n */\nexport const metadata: WorkerMeta = {\n queue: COMMUNICATION_CHANNELS_QUEUES.importHistory,\n id: 'communication_channels:channel-import-history',\n concurrency: 1,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\ntype CredentialsServiceLike = {\n resolve: (\n integrationId: string,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n save?: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<void>\n}\n\nexport default async function handle(\n job: QueuedJob<ChannelImportHistoryJobPayload>,\n ctx: HandlerContext,\n): Promise<void> {\n const payload = job.payload\n const { progressJobId, channelId, sinceDays, contactEmails, maxMessages, scope } = payload\n\n const progressService = ctx.resolve<ProgressService>('progressService')\n const progressContext: ProgressServiceContext = {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n }\n\n const em = (ctx.resolve('em') as EntityManager).fork()\n const adapterRegistry = ctx.resolve<ChannelAdapterRegistry>('channelAdapterRegistry')\n\n try {\n await progressService.startJob(progressJobId, progressContext)\n\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: channelId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!channel) {\n await progressService.failJob(\n progressJobId,\n { errorMessage: 'Channel not found' },\n progressContext,\n )\n return\n }\n if (!channel.isActive || channel.status !== 'connected') {\n await progressService.failJob(\n progressJobId,\n { errorMessage: `Channel is not connected (status=${channel.status})` },\n progressContext,\n )\n return\n }\n\n const adapter = adapterRegistry?.get(channel.providerKey)\n if (!adapter || typeof adapter.importHistory !== 'function') {\n await progressService.failJob(\n progressJobId,\n {\n errorMessage: `Provider \"${channel.providerKey}\" does not support history import`,\n },\n progressContext,\n )\n return\n }\n\n // Resolve + refresh credentials. Same flow as poll-channel.\n let credentialsService: CredentialsServiceLike | null = null\n try {\n credentialsService = ctx.resolve<CredentialsServiceLike>('integrationCredentialsService')\n } catch {\n credentialsService = null\n }\n const credentialsScope = {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: channel.userId ?? null,\n }\n let credentials: Record<string, unknown> = {}\n if (channel.credentialsRef && credentialsService) {\n try {\n credentials =\n (await credentialsService.resolve(`channel_${channel.providerKey}`, credentialsScope)) ?? {}\n } catch {\n credentials = {}\n }\n }\n const refreshed = await refreshCredentialsIfNeeded(\n { adapter, channelId: channel.id, credentials, scope: credentialsScope },\n { credentialsService },\n )\n credentials = refreshed.credentials\n\n const commandBus = ctx.resolve<CommandBus>('commandBus')\n const containerProxy = { resolve: ctx.resolve.bind(ctx) }\n const commandCtx = {\n container: containerProxy as never,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId,\n organizationIds: [scope.organizationId],\n }\n\n let cursor: string | undefined\n let processedCount = 0\n let totalCount = maxMessages\n let firstPage = true\n\n // Bounded loop guards against a misbehaving adapter (infinite hasMore).\n // 100 pages * HARD_CAP (default 200) = 20k messages \u2014 far above the 5k cap\n // enforced by the schema, so this only trips on adapter bugs.\n const MAX_PAGES = 100\n for (let pageIndex = 0; pageIndex < MAX_PAGES; pageIndex += 1) {\n if (await progressService.isCancellationRequested(progressJobId, scope.tenantId, scope.organizationId)) {\n await progressService.markCancelled(progressJobId, progressContext)\n return\n }\n\n const page = await adapter.importHistory!({\n credentials,\n scope: { tenantId: scope.tenantId, organizationId: scope.organizationId },\n sinceDays,\n contactEmails,\n maxMessages,\n cursor,\n })\n\n if (firstPage && typeof page.totalCandidates === 'number') {\n totalCount = Math.min(maxMessages, page.totalCandidates)\n await progressService.updateProgress(\n progressJobId,\n { totalCount, processedCount: 0 },\n progressContext,\n )\n firstPage = false\n }\n\n for (const message of page.messages) {\n if (processedCount >= maxMessages) break\n try {\n const input: IngestInboundMessageInput = {\n channelId: channel.id,\n providerKey: channel.providerKey,\n channelType: channel.channelType,\n scope: { tenantId: scope.tenantId, organizationId: scope.organizationId },\n message,\n }\n await commandBus.execute(COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID, {\n input,\n ctx: commandCtx as never,\n })\n } catch (err) {\n // Import-history is best-effort per-message. Same classification\n // logic as poll-channel: a transient failure aborts the whole job\n // (the operator can retry), permanent failures are logged and\n // skipped (the bad message would loop forever otherwise).\n const classification = classifyOutboundError(err)\n if (classification.transient) {\n throw err\n }\n console.warn(\n `[communication_channels:channel-import-history] permanent ingest failure on channel ${channel.id}; skipping message ${message.externalMessageId}. Reason: ${classification.message}`,\n )\n }\n processedCount += 1\n }\n\n await progressService.updateProgress(\n progressJobId,\n { processedCount, totalCount },\n progressContext,\n )\n\n if (processedCount >= maxMessages) break\n if (!page.hasMore || !page.nextCursor) break\n cursor = page.nextCursor\n }\n\n await progressService.completeJob(\n progressJobId,\n { resultSummary: { importedCount: processedCount, channelId } },\n progressContext,\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Import history job failed'\n const stack = err instanceof Error ? err.stack : undefined\n try {\n await progressService.failJob(\n progressJobId,\n { errorMessage: message.slice(0, 2000), errorStack: stack?.slice(0, 10000) },\n progressContext,\n )\n } catch (failErr) {\n console.error(\n `[communication_channels:channel-import-history] failed to mark progress job ${progressJobId} as failed: ${\n failErr instanceof Error ? failErr.message : String(failErr)\n }`,\n )\n }\n throw err\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,8 @@ import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
4
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
5
+ import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
6
+ import { enforceCommandOptimisticLock } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
5
7
  import { CurrencyFetchConfig } from "../../data/entities.js";
6
8
  import {
7
9
  createFetchConfig,
@@ -85,6 +87,19 @@ async function PUT(req) {
85
87
  if (!id) {
86
88
  return NextResponse.json({ error: "ID required" }, { status: 400 });
87
89
  }
90
+ const existing = await em.findOne(CurrencyFetchConfig, {
91
+ id,
92
+ tenantId: auth.tenantId,
93
+ organizationId: auth.orgId
94
+ });
95
+ if (existing) {
96
+ enforceCommandOptimisticLock({
97
+ resourceKind: "currencies.currency_fetch_config",
98
+ resourceId: existing.id,
99
+ current: existing.updatedAt ?? null,
100
+ request: req
101
+ });
102
+ }
88
103
  const config = await updateFetchConfig(em, id, data, {
89
104
  tenantId: auth.tenantId,
90
105
  organizationId: auth.orgId,
@@ -92,6 +107,9 @@ async function PUT(req) {
92
107
  });
93
108
  return NextResponse.json({ config });
94
109
  } catch (err) {
110
+ if (isCrudHttpError(err)) {
111
+ return NextResponse.json(err.body, { status: err.status });
112
+ }
95
113
  return NextResponse.json({ error: err.message }, { status: 400 });
96
114
  } finally {
97
115
  await container.dispose?.();
@@ -110,6 +128,19 @@ async function DELETE(req) {
110
128
  if (!id) {
111
129
  return NextResponse.json({ error: "ID required" }, { status: 400 });
112
130
  }
131
+ const existing = await em.findOne(CurrencyFetchConfig, {
132
+ id,
133
+ tenantId: auth.tenantId,
134
+ organizationId: auth.orgId
135
+ });
136
+ if (existing) {
137
+ enforceCommandOptimisticLock({
138
+ resourceKind: "currencies.currency_fetch_config",
139
+ resourceId: existing.id,
140
+ current: existing.updatedAt ?? null,
141
+ request: req
142
+ });
143
+ }
113
144
  await deleteFetchConfig(em, id, {
114
145
  tenantId: auth.tenantId,
115
146
  organizationId: auth.orgId,
@@ -117,6 +148,9 @@ async function DELETE(req) {
117
148
  });
118
149
  return NextResponse.json({ success: true });
119
150
  } catch (err) {
151
+ if (isCrudHttpError(err)) {
152
+ return NextResponse.json(err.body, { status: err.status });
153
+ }
120
154
  return NextResponse.json({ error: err.message }, { status: 400 });
121
155
  } finally {
122
156
  await container.dispose?.();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/currencies/api/fetch-configs/route.ts"],
4
- "sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { CurrencyFetchConfig } from '../../data/entities'\nimport {\n createFetchConfig,\n updateFetchConfig,\n deleteFetchConfig,\n} from '../../commands/fetch-configs'\nimport { currencyFetchConfigCreateSchema, currencyFetchConfigUpdateSchema } from '../../data/validators'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['currencies.fetch.view'] },\n POST: { requireAuth: true, requireFeatures: ['currencies.fetch.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['currencies.fetch.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['currencies.fetch.manage'] },\n}\n\nexport async function GET(req: NextRequest) {\n const container = await createRequestContainer()\n\n try {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const em = container.resolve<EntityManager>('em')\n\n const findFilter: Record<string, unknown> = {\n tenantId: auth.tenantId,\n }\n if (auth.orgId) {\n findFilter.organizationId = auth.orgId\n }\n\n const configs = await em.find(\n CurrencyFetchConfig,\n findFilter,\n {\n orderBy: { provider: 'ASC' },\n }\n )\n\n return NextResponse.json({ configs })\n } finally {\n await (container as any).dispose?.()\n }\n}\n\nexport async function POST(req: NextRequest) {\n const container = await createRequestContainer()\n\n try {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const em = container.resolve<EntityManager>('em')\n\n let body\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n\n const config = await createFetchConfig(em, body, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub,\n })\n\n return NextResponse.json({ config }, { status: 201 })\n } catch (err: any) {\n return NextResponse.json({ error: err.message }, { status: 400 })\n } finally {\n await (container as any).dispose?.()\n }\n}\n\nexport async function PUT(req: NextRequest) {\n const container = await createRequestContainer()\n\n try {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const em = container.resolve<EntityManager>('em')\n\n let body\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n\n const { id, ...data } = body\n\n if (!id) {\n return NextResponse.json({ error: 'ID required' }, { status: 400 })\n }\n\n const config = await updateFetchConfig(em, id, data, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub,\n })\n\n return NextResponse.json({ config })\n } catch (err: any) {\n return NextResponse.json({ error: err.message }, { status: 400 })\n } finally {\n await (container as any).dispose?.()\n }\n}\n\nexport async function DELETE(req: NextRequest) {\n const container = await createRequestContainer()\n\n try {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const em = container.resolve<EntityManager>('em')\n const { searchParams } = new URL(req.url)\n const id = searchParams.get('id')\n\n if (!id) {\n return NextResponse.json({ error: 'ID required' }, { status: 400 })\n }\n\n await deleteFetchConfig(em, id, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub,\n })\n\n return NextResponse.json({ success: true })\n } catch (err: any) {\n return NextResponse.json({ error: err.message }, { status: 400 })\n } finally {\n await (container as any).dispose?.()\n }\n}\n\nconst fetchConfigItemSchema = z.object({\n id: z.string().uuid(),\n organizationId: z.string().uuid(),\n tenantId: z.string().uuid(),\n provider: z.string(),\n isEnabled: z.boolean(),\n syncTime: z.string().nullable(),\n lastSyncAt: z.string().nullable().optional(),\n lastSyncStatus: z.string().nullable().optional(),\n lastSyncMessage: z.string().nullable().optional(),\n lastSyncCount: z.number().nullable().optional(),\n config: z.record(z.string(), z.unknown()).nullable().optional(),\n createdAt: z.string(),\n updatedAt: z.string(),\n})\n\nconst errorSchema = z.object({ error: z.string() })\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Manage currency fetch configurations',\n description: 'Endpoints for managing currency rate fetch configurations from external providers.',\n methods: {\n GET: {\n operationId: 'listCurrencyFetchConfigs',\n summary: 'List currency fetch configurations',\n description: 'Returns all currency fetch configurations scoped to the authenticated organization.',\n responses: [\n {\n status: 200,\n description: 'A list of currency fetch configurations',\n schema: z.object({\n configs: z.array(fetchConfigItemSchema),\n }),\n },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n POST: {\n operationId: 'createCurrencyFetchConfig',\n summary: 'Create currency fetch configuration',\n description: 'Creates a new currency fetch configuration.',\n requestBody: {\n schema: currencyFetchConfigCreateSchema,\n contentType: 'application/json',\n },\n responses: [\n {\n status: 201,\n description: 'Currency fetch configuration created successfully',\n schema: z.object({\n config: fetchConfigItemSchema,\n }),\n },\n ],\n errors: [\n { status: 400, description: 'Bad request', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n PUT: {\n operationId: 'updateCurrencyFetchConfig',\n summary: 'Update currency fetch configuration',\n description: 'Updates an existing currency fetch configuration by id.',\n requestBody: {\n schema: currencyFetchConfigUpdateSchema.extend({\n id: z.string().uuid(),\n }),\n contentType: 'application/json',\n },\n responses: [\n {\n status: 200,\n description: 'Currency fetch configuration updated successfully',\n schema: z.object({\n config: fetchConfigItemSchema,\n }),\n },\n ],\n errors: [\n { status: 400, description: 'Bad request', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n DELETE: {\n operationId: 'deleteCurrencyFetchConfig',\n summary: 'Delete currency fetch configuration',\n description: 'Deletes a currency fetch configuration by id.',\n query: z.object({\n id: z.string().uuid().describe('Currency fetch configuration identifier to delete'),\n }),\n responses: [\n {\n status: 200,\n description: 'Currency fetch configuration deleted successfully',\n schema: z.object({ success: z.literal(true) }),\n },\n ],\n errors: [\n { status: 400, description: 'Bad request', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { CurrencyFetchConfig } from '../../data/entities'\nimport {\n createFetchConfig,\n updateFetchConfig,\n deleteFetchConfig,\n} from '../../commands/fetch-configs'\nimport { currencyFetchConfigCreateSchema, currencyFetchConfigUpdateSchema } from '../../data/validators'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['currencies.fetch.view'] },\n POST: { requireAuth: true, requireFeatures: ['currencies.fetch.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['currencies.fetch.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['currencies.fetch.manage'] },\n}\n\nexport async function GET(req: NextRequest) {\n const container = await createRequestContainer()\n\n try {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const em = container.resolve<EntityManager>('em')\n\n const findFilter: Record<string, unknown> = {\n tenantId: auth.tenantId,\n }\n if (auth.orgId) {\n findFilter.organizationId = auth.orgId\n }\n\n const configs = await em.find(\n CurrencyFetchConfig,\n findFilter,\n {\n orderBy: { provider: 'ASC' },\n }\n )\n\n return NextResponse.json({ configs })\n } finally {\n await (container as any).dispose?.()\n }\n}\n\nexport async function POST(req: NextRequest) {\n const container = await createRequestContainer()\n\n try {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const em = container.resolve<EntityManager>('em')\n\n let body\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n\n const config = await createFetchConfig(em, body, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub,\n })\n\n return NextResponse.json({ config }, { status: 201 })\n } catch (err: any) {\n return NextResponse.json({ error: err.message }, { status: 400 })\n } finally {\n await (container as any).dispose?.()\n }\n}\n\nexport async function PUT(req: NextRequest) {\n const container = await createRequestContainer()\n\n try {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const em = container.resolve<EntityManager>('em')\n\n let body\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n\n const { id, ...data } = body\n\n if (!id) {\n return NextResponse.json({ error: 'ID required' }, { status: 400 })\n }\n\n const existing = await em.findOne(CurrencyFetchConfig, {\n id,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (existing) {\n enforceCommandOptimisticLock({\n resourceKind: 'currencies.currency_fetch_config',\n resourceId: existing.id,\n current: existing.updatedAt ?? null,\n request: req,\n })\n }\n\n const config = await updateFetchConfig(em, id, data, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub,\n })\n\n return NextResponse.json({ config })\n } catch (err: any) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n return NextResponse.json({ error: err.message }, { status: 400 })\n } finally {\n await (container as any).dispose?.()\n }\n}\n\nexport async function DELETE(req: NextRequest) {\n const container = await createRequestContainer()\n\n try {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const em = container.resolve<EntityManager>('em')\n const { searchParams } = new URL(req.url)\n const id = searchParams.get('id')\n\n if (!id) {\n return NextResponse.json({ error: 'ID required' }, { status: 400 })\n }\n\n const existing = await em.findOne(CurrencyFetchConfig, {\n id,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (existing) {\n enforceCommandOptimisticLock({\n resourceKind: 'currencies.currency_fetch_config',\n resourceId: existing.id,\n current: existing.updatedAt ?? null,\n request: req,\n })\n }\n\n await deleteFetchConfig(em, id, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub,\n })\n\n return NextResponse.json({ success: true })\n } catch (err: any) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n return NextResponse.json({ error: err.message }, { status: 400 })\n } finally {\n await (container as any).dispose?.()\n }\n}\n\nconst fetchConfigItemSchema = z.object({\n id: z.string().uuid(),\n organizationId: z.string().uuid(),\n tenantId: z.string().uuid(),\n provider: z.string(),\n isEnabled: z.boolean(),\n syncTime: z.string().nullable(),\n lastSyncAt: z.string().nullable().optional(),\n lastSyncStatus: z.string().nullable().optional(),\n lastSyncMessage: z.string().nullable().optional(),\n lastSyncCount: z.number().nullable().optional(),\n config: z.record(z.string(), z.unknown()).nullable().optional(),\n createdAt: z.string(),\n updatedAt: z.string(),\n})\n\nconst errorSchema = z.object({ error: z.string() })\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Manage currency fetch configurations',\n description: 'Endpoints for managing currency rate fetch configurations from external providers.',\n methods: {\n GET: {\n operationId: 'listCurrencyFetchConfigs',\n summary: 'List currency fetch configurations',\n description: 'Returns all currency fetch configurations scoped to the authenticated organization.',\n responses: [\n {\n status: 200,\n description: 'A list of currency fetch configurations',\n schema: z.object({\n configs: z.array(fetchConfigItemSchema),\n }),\n },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n POST: {\n operationId: 'createCurrencyFetchConfig',\n summary: 'Create currency fetch configuration',\n description: 'Creates a new currency fetch configuration.',\n requestBody: {\n schema: currencyFetchConfigCreateSchema,\n contentType: 'application/json',\n },\n responses: [\n {\n status: 201,\n description: 'Currency fetch configuration created successfully',\n schema: z.object({\n config: fetchConfigItemSchema,\n }),\n },\n ],\n errors: [\n { status: 400, description: 'Bad request', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n PUT: {\n operationId: 'updateCurrencyFetchConfig',\n summary: 'Update currency fetch configuration',\n description: 'Updates an existing currency fetch configuration by id.',\n requestBody: {\n schema: currencyFetchConfigUpdateSchema.extend({\n id: z.string().uuid(),\n }),\n contentType: 'application/json',\n },\n responses: [\n {\n status: 200,\n description: 'Currency fetch configuration updated successfully',\n schema: z.object({\n config: fetchConfigItemSchema,\n }),\n },\n ],\n errors: [\n { status: 400, description: 'Bad request', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n DELETE: {\n operationId: 'deleteCurrencyFetchConfig',\n summary: 'Delete currency fetch configuration',\n description: 'Deletes a currency fetch configuration by id.',\n query: z.object({\n id: z.string().uuid().describe('Currency fetch configuration identifier to delete'),\n }),\n responses: [\n {\n status: 200,\n description: 'Currency fetch configuration deleted successfully',\n schema: z.object({ success: z.literal(true) }),\n },\n ],\n errors: [\n { status: 400, description: 'Bad request', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -8,7 +8,15 @@ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuarde
8
8
  import { Button } from "@open-mercato/ui/primitives/button";
9
9
  import { Spinner } from "@open-mercato/ui/primitives/spinner";
10
10
  import { Switch } from "@open-mercato/ui/primitives/switch";
11
+ import { StatusBadge } from "@open-mercato/ui/primitives/status-badge";
12
+ import { Alert } from "@open-mercato/ui/primitives/alert";
13
+ import { TimeInput } from "@open-mercato/ui/backend/inputs/TimeInput";
11
14
  import { useT } from "@open-mercato/shared/lib/i18n/context";
15
+ const STATUS_BADGE_VARIANT = {
16
+ success: "success",
17
+ error: "error",
18
+ partial: "warning"
19
+ };
12
20
  function CurrencyFetchingConfig() {
13
21
  const t = useT();
14
22
  const [configs, setConfigs] = useState([]);
@@ -195,7 +203,7 @@ function CurrencyFetchingConfig() {
195
203
  if (count === 0) {
196
204
  flash(t("currencies.fetch.sync_no_rates"), "warning");
197
205
  } else {
198
- flash(`${t("currencies.fetch.sync_success")}: ${count} rates fetched`, "success");
206
+ flash(t("currencies.fetch.sync_success", { count }), "success");
199
207
  }
200
208
  await loadConfigs();
201
209
  }
@@ -209,17 +217,12 @@ function CurrencyFetchingConfig() {
209
217
  loadConfigs();
210
218
  }, [loadConfigs]);
211
219
  function formatLastSync(date) {
212
- if (!date) return "Never";
220
+ if (!date) return t("currencies.fetch.last_sync_never");
213
221
  return new Date(date).toLocaleString();
214
222
  }
215
223
  function getStatusBadge(status) {
216
224
  if (!status) return null;
217
- const styles = {
218
- success: "bg-green-100 text-green-800 px-2 py-1 rounded text-xs",
219
- error: "bg-red-100 text-red-800 px-2 py-1 rounded text-xs",
220
- partial: "bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-xs"
221
- };
222
- return /* @__PURE__ */ jsx("span", { className: styles[status] || styles.error, children: status });
225
+ return /* @__PURE__ */ jsx(StatusBadge, { variant: STATUS_BADGE_VARIANT[status] ?? "neutral", children: status });
223
226
  }
224
227
  function getProviderName(provider) {
225
228
  if (provider === "NBP") return t("currencies.fetch.provider_nbp");
@@ -279,12 +282,10 @@ function CurrencyFetchingConfig() {
279
282
  ":"
280
283
  ] }),
281
284
  /* @__PURE__ */ jsx(
282
- "input",
285
+ TimeInput,
283
286
  {
284
- type: "time",
285
287
  value: config.syncTime || "09:00",
286
- onChange: (e) => updateSyncTime(config.id, e.target.value, config.updatedAt),
287
- className: "rounded border bg-background px-2 py-1.5 text-sm"
288
+ onChange: (time) => updateSyncTime(config.id, time, config.updatedAt)
288
289
  }
289
290
  )
290
291
  ] }),
@@ -299,6 +300,7 @@ function CurrencyFetchingConfig() {
299
300
  /* @__PURE__ */ jsx(
300
301
  Button,
301
302
  {
303
+ type: "button",
302
304
  onClick: () => fetchNow(config.provider),
303
305
  disabled: fetching === config.provider,
304
306
  size: "default",
@@ -313,10 +315,9 @@ function CurrencyFetchingConfig() {
313
315
  config.lastSyncCount !== null && /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground mt-2", children: [
314
316
  t("currencies.fetch.last_sync_count"),
315
317
  ": ",
316
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: config.lastSyncCount }),
317
- " rates"
318
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: config.lastSyncCount })
318
319
  ] }),
319
- config.lastSyncMessage && config.lastSyncStatus === "error" && /* @__PURE__ */ jsx("div", { className: "rounded border border-red-200 bg-red-50 p-2 text-xs text-red-700 mt-2", children: config.lastSyncMessage })
320
+ config.lastSyncMessage && config.lastSyncStatus === "error" && /* @__PURE__ */ jsx(Alert, { status: "error", style: "lighter", size: "xs", className: "mt-2", children: config.lastSyncMessage })
320
321
  ] })
321
322
  ]
322
323
  },
@@ -327,6 +328,7 @@ function CurrencyFetchingConfig() {
327
328
  /* @__PURE__ */ jsx(
328
329
  Button,
329
330
  {
331
+ type: "button",
330
332
  onClick: () => initializeMissingProviders(availableProviders),
331
333
  children: t("currencies.fetch.initialize_providers")
332
334
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/currencies/components/CurrencyFetchingConfig.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport { useState, useEffect, useCallback, useMemo } from 'react'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ninterface FetchConfig {\n id: string\n provider: string\n isEnabled: boolean\n syncTime: string | null\n lastSyncAt: string | null\n lastSyncStatus: string | null\n lastSyncMessage: string | null\n lastSyncCount: number | null\n updatedAt?: string | null\n}\n\nexport default function CurrencyFetchingConfig() {\n const t = useT()\n const [configs, setConfigs] = useState<FetchConfig[]>([])\n const [loading, setLoading] = useState(true)\n const [fetching, setFetching] = useState<string | null>(null)\n const [initializing, setInitializing] = useState(false)\n\n // Available providers that should be configured\n const availableProviders = useMemo(() => ['NBP', 'Raiffeisen Bank Polska'], [])\n\n const mutationContextId = 'currencies-fetch-configs:mutation'\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId?: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: mutationContextId,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const createProviderConfig = useCallback(async (provider: string) => {\n try {\n await runMutation({\n operation: () => apiCall('/api/currencies/fetch-configs', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n provider,\n isEnabled: false,\n syncTime: '09:00',\n }),\n }),\n context: {\n formId: mutationContextId,\n resourceKind: 'currencies.fetch_config',\n retryLastMutation,\n },\n mutationPayload: { provider, isEnabled: false, syncTime: '09:00' },\n })\n } catch (err: any) {\n // Ignore errors for duplicate providers\n if (!err.message?.includes('already configured')) {\n throw err\n }\n }\n }, [mutationContextId, retryLastMutation, runMutation])\n\n const initializeMissingProviders = useCallback(async (providers: string[]) => {\n setInitializing(true)\n try {\n for (const provider of providers) {\n await createProviderConfig(provider)\n }\n // Reload configs after initialization\n const { result } = await apiCall('/api/currencies/fetch-configs')\n if (result?.configs) {\n setConfigs(result.configs as FetchConfig[])\n }\n } catch (err: any) {\n console.error('Failed to initialize providers:', err)\n } finally {\n setInitializing(false)\n }\n }, [createProviderConfig])\n\n const loadConfigs = useCallback(async () => {\n try {\n const { result } = await apiCall('/api/currencies/fetch-configs')\n\n if (result?.configs) {\n const existingConfigs = result.configs as FetchConfig[]\n setConfigs(existingConfigs)\n\n // Check if we need to initialize any missing providers\n const existingProviders = existingConfigs.map((c) => c.provider)\n const missingProviders = availableProviders.filter(\n (p) => !existingProviders.includes(p)\n )\n\n // Auto-initialize missing providers\n if (missingProviders.length > 0) {\n await initializeMissingProviders(missingProviders)\n }\n }\n } catch (err: any) {\n flash(err.message || t('currencies.fetch.error_load_configs'), 'error')\n } finally {\n setLoading(false)\n }\n }, [availableProviders, initializeMissingProviders, t])\n\n const toggleEnabled = useCallback(async (configId: string, currentValue: boolean, updatedAt?: string | null) => {\n try {\n const call = await runMutation({\n operation: async () => {\n const response = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(updatedAt),\n () =>\n apiCall('/api/currencies/fetch-configs', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: configId,\n isEnabled: !currentValue,\n }),\n }),\n )\n if (!response.ok) {\n throw Object.assign(new Error('[internal] currencies.fetchConfig.toggle failed'), {\n status: response.status,\n ...((response.result as Record<string, unknown> | null) ?? {}),\n })\n }\n return response\n },\n context: {\n formId: mutationContextId,\n resourceKind: 'currencies.fetch_config',\n resourceId: configId,\n retryLastMutation,\n },\n mutationPayload: { id: configId, isEnabled: !currentValue },\n })\n\n const result = call.result as { config?: FetchConfig } | null\n if (result?.config) {\n setConfigs((prev) =>\n prev.map((c) => (c.id === configId ? (result.config as FetchConfig) : c))\n )\n flash(\n currentValue\n ? t('currencies.fetch.provider_disabled')\n : t('currencies.fetch.provider_enabled'),\n 'success'\n )\n }\n } catch (err: any) {\n if (extractOptimisticLockConflict(err)) return\n flash(err.message || t('currencies.fetch.error_update_config'), 'error')\n }\n }, [mutationContextId, retryLastMutation, runMutation, t])\n\n const updateSyncTime = useCallback(async (configId: string, syncTime: string, updatedAt?: string | null) => {\n try {\n const call = await runMutation({\n operation: async () => {\n const response = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(updatedAt),\n () =>\n apiCall('/api/currencies/fetch-configs', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: configId,\n syncTime: syncTime || null,\n }),\n }),\n )\n if (!response.ok) {\n throw Object.assign(new Error('[internal] currencies.fetchConfig.updateSyncTime failed'), {\n status: response.status,\n ...((response.result as Record<string, unknown> | null) ?? {}),\n })\n }\n return response\n },\n context: {\n formId: mutationContextId,\n resourceKind: 'currencies.fetch_config',\n resourceId: configId,\n retryLastMutation,\n },\n mutationPayload: { id: configId, syncTime: syncTime || null },\n })\n\n const result = call.result as { config?: FetchConfig } | null\n if (result?.config) {\n setConfigs((prev) =>\n prev.map((c) => (c.id === configId ? (result.config as FetchConfig) : c))\n )\n }\n } catch (err: any) {\n if (extractOptimisticLockConflict(err)) return\n flash(err.message || t('currencies.fetch.error_update_sync_time'), 'error')\n }\n }, [mutationContextId, retryLastMutation, runMutation, t])\n\n const fetchNow = useCallback(async (provider: string) => {\n setFetching(provider)\n\n try {\n const call = await runMutation({\n operation: () => apiCall('/api/currencies/fetch-rates', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n providers: [provider],\n }),\n }),\n context: {\n formId: mutationContextId,\n resourceKind: 'currencies.fetch_rates',\n retryLastMutation,\n },\n mutationPayload: { providers: [provider] },\n })\n\n const result = call.result as { byProvider?: Record<string, { count: number; errors?: string[] }> } | null\n if (result) {\n const byProvider = result.byProvider as Record<string, { count: number; errors?: string[] }>\n const count = byProvider?.[provider]?.count || 0\n\n if (count === 0) {\n flash(t('currencies.fetch.sync_no_rates'), 'warning')\n } else {\n flash(`${t('currencies.fetch.sync_success')}: ${count} rates fetched`, 'success')\n }\n await loadConfigs()\n }\n } catch (err: any) {\n flash(err.message || t('currencies.fetch.sync_error'), 'error')\n } finally {\n setFetching(null)\n }\n }, [mutationContextId, retryLastMutation, runMutation, t, loadConfigs])\n\n useEffect(() => {\n loadConfigs()\n }, [loadConfigs])\n\n function formatLastSync(date: string | null): string {\n if (!date) return 'Never'\n return new Date(date).toLocaleString()\n }\n\n function getStatusBadge(status: string | null) {\n if (!status) return null\n\n const styles: Record<string, string> = {\n success: 'bg-green-100 text-green-800 px-2 py-1 rounded text-xs',\n error: 'bg-red-100 text-red-800 px-2 py-1 rounded text-xs',\n partial: 'bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-xs',\n }\n\n return (\n <span className={styles[status] || styles.error}>\n {status}\n </span>\n )\n }\n\n function getProviderName(provider: string): string {\n if (provider === 'NBP') return t('currencies.fetch.provider_nbp')\n if (provider === 'Raiffeisen Bank Polska') return t('currencies.fetch.provider_raiffeisen')\n return provider\n }\n\n function getProviderDescription(provider: string): string {\n if (provider === 'NBP') {\n return t('currencies.fetch.provider_nbp_description')\n }\n if (provider === 'Raiffeisen Bank Polska') {\n return t('currencies.fetch.provider_raiffeisen_description')\n }\n return ''\n }\n\n if (loading || initializing) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-4\">\n <header className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('currencies.fetch.title')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('currencies.fetch.description')}\n </p>\n </header>\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n {initializing ? t('currencies.fetch.initializing_providers') : t('currencies.fetch.loading')}\n </div>\n </section>\n )\n }\n\n return (\n <section className=\"space-y-6 rounded-lg border bg-background p-4\">\n <header className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('currencies.fetch.title')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('currencies.fetch.description')}\n </p>\n </header>\n\n <div className=\"space-y-3\">\n {configs.map((config) => (\n <div\n key={config.id}\n className=\"rounded-lg border bg-card p-4\"\n >\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1\">\n <h3 className=\"text-sm font-medium\">\n {getProviderName(config.provider)}\n </h3>\n <p className=\"text-xs text-muted-foreground mt-0.5\">\n {getProviderDescription(config.provider)}\n </p>\n </div>\n\n <Switch\n checked={config.isEnabled}\n onCheckedChange={() => toggleEnabled(config.id, config.isEnabled, config.updatedAt)}\n />\n </div>\n\n {config.isEnabled && (\n <>\n <div className=\"flex items-baseline gap-3 flex-wrap mt-3\">\n <div className=\"flex items-baseline gap-2\">\n <label className=\"text-xs text-muted-foreground whitespace-nowrap\">\n {t('currencies.fetch.sync_time')}:\n </label>\n <input\n type=\"time\"\n value={config.syncTime || '09:00'}\n onChange={(e) => updateSyncTime(config.id, e.target.value, config.updatedAt)}\n className=\"rounded border bg-background px-2 py-1.5 text-sm\"\n />\n </div>\n\n <div className=\"flex items-baseline gap-2\">\n <label className=\"text-xs text-muted-foreground whitespace-nowrap\">\n {t('currencies.fetch.last_sync')}:\n </label>\n <span className=\"text-xs text-muted-foreground\">\n {formatLastSync(config.lastSyncAt)}\n </span>\n {getStatusBadge(config.lastSyncStatus)}\n </div>\n\n <Button\n onClick={() => fetchNow(config.provider)}\n disabled={fetching === config.provider}\n size=\"default\"\n className=\"ml-auto min-w-32\"\n >\n {fetching === config.provider ? (\n <div>\n <Spinner className=\"mr-1.5\" size='sm' />\n {t('currencies.fetch.fetching')}\n </div>\n ) : (\n t('currencies.fetch.fetch_now')\n )}\n </Button>\n </div>\n\n {config.lastSyncCount !== null && (\n <div className=\"text-xs text-muted-foreground mt-2\">\n {t('currencies.fetch.last_sync_count')}: <span className=\"font-medium\">{config.lastSyncCount}</span> rates\n </div>\n )}\n\n {config.lastSyncMessage && config.lastSyncStatus === 'error' && (\n <div className=\"rounded border border-red-200 bg-red-50 p-2 text-xs text-red-700 mt-2\">\n {config.lastSyncMessage}\n </div>\n )}\n </>\n )}\n </div>\n ))}\n\n {configs.length === 0 && (\n <div className=\"rounded border bg-background/80 p-6 text-center\">\n <p className=\"text-sm text-muted-foreground\">\n {t('currencies.fetch.no_providers')}\n </p>\n <Button\n onClick={() => initializeMissingProviders(availableProviders)}\n >\n {t('currencies.fetch.initialize_providers')}\n </Button>\n </div>\n )}\n </div>\n </section>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport { useState, useEffect, useCallback, useMemo } from 'react'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'\nimport { Alert } from '@open-mercato/ui/primitives/alert'\nimport { TimeInput } from '@open-mercato/ui/backend/inputs/TimeInput'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ninterface FetchConfig {\n id: string\n provider: string\n isEnabled: boolean\n syncTime: string | null\n lastSyncAt: string | null\n lastSyncStatus: string | null\n lastSyncMessage: string | null\n lastSyncCount: number | null\n updatedAt?: string | null\n}\n\nconst STATUS_BADGE_VARIANT: Record<string, StatusBadgeVariant> = {\n success: 'success',\n error: 'error',\n partial: 'warning',\n}\n\nexport default function CurrencyFetchingConfig() {\n const t = useT()\n const [configs, setConfigs] = useState<FetchConfig[]>([])\n const [loading, setLoading] = useState(true)\n const [fetching, setFetching] = useState<string | null>(null)\n const [initializing, setInitializing] = useState(false)\n\n // Available providers that should be configured\n const availableProviders = useMemo(() => ['NBP', 'Raiffeisen Bank Polska'], [])\n\n const mutationContextId = 'currencies-fetch-configs:mutation'\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId?: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: mutationContextId,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const createProviderConfig = useCallback(async (provider: string) => {\n try {\n await runMutation({\n operation: () => apiCall('/api/currencies/fetch-configs', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n provider,\n isEnabled: false,\n syncTime: '09:00',\n }),\n }),\n context: {\n formId: mutationContextId,\n resourceKind: 'currencies.fetch_config',\n retryLastMutation,\n },\n mutationPayload: { provider, isEnabled: false, syncTime: '09:00' },\n })\n } catch (err: any) {\n // Ignore errors for duplicate providers\n if (!err.message?.includes('already configured')) {\n throw err\n }\n }\n }, [mutationContextId, retryLastMutation, runMutation])\n\n const initializeMissingProviders = useCallback(async (providers: string[]) => {\n setInitializing(true)\n try {\n for (const provider of providers) {\n await createProviderConfig(provider)\n }\n // Reload configs after initialization\n const { result } = await apiCall('/api/currencies/fetch-configs')\n if (result?.configs) {\n setConfigs(result.configs as FetchConfig[])\n }\n } catch (err: any) {\n console.error('Failed to initialize providers:', err)\n } finally {\n setInitializing(false)\n }\n }, [createProviderConfig])\n\n const loadConfigs = useCallback(async () => {\n try {\n const { result } = await apiCall('/api/currencies/fetch-configs')\n\n if (result?.configs) {\n const existingConfigs = result.configs as FetchConfig[]\n setConfigs(existingConfigs)\n\n // Check if we need to initialize any missing providers\n const existingProviders = existingConfigs.map((c) => c.provider)\n const missingProviders = availableProviders.filter(\n (p) => !existingProviders.includes(p)\n )\n\n // Auto-initialize missing providers\n if (missingProviders.length > 0) {\n await initializeMissingProviders(missingProviders)\n }\n }\n } catch (err: any) {\n flash(err.message || t('currencies.fetch.error_load_configs'), 'error')\n } finally {\n setLoading(false)\n }\n }, [availableProviders, initializeMissingProviders, t])\n\n const toggleEnabled = useCallback(async (configId: string, currentValue: boolean, updatedAt?: string | null) => {\n try {\n const call = await runMutation({\n operation: async () => {\n const response = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(updatedAt),\n () =>\n apiCall('/api/currencies/fetch-configs', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: configId,\n isEnabled: !currentValue,\n }),\n }),\n )\n if (!response.ok) {\n throw Object.assign(new Error('[internal] currencies.fetchConfig.toggle failed'), {\n status: response.status,\n ...((response.result as Record<string, unknown> | null) ?? {}),\n })\n }\n return response\n },\n context: {\n formId: mutationContextId,\n resourceKind: 'currencies.fetch_config',\n resourceId: configId,\n retryLastMutation,\n },\n mutationPayload: { id: configId, isEnabled: !currentValue },\n })\n\n const result = call.result as { config?: FetchConfig } | null\n if (result?.config) {\n setConfigs((prev) =>\n prev.map((c) => (c.id === configId ? (result.config as FetchConfig) : c))\n )\n flash(\n currentValue\n ? t('currencies.fetch.provider_disabled')\n : t('currencies.fetch.provider_enabled'),\n 'success'\n )\n }\n } catch (err: any) {\n if (extractOptimisticLockConflict(err)) return\n flash(err.message || t('currencies.fetch.error_update_config'), 'error')\n }\n }, [mutationContextId, retryLastMutation, runMutation, t])\n\n const updateSyncTime = useCallback(async (configId: string, syncTime: string, updatedAt?: string | null) => {\n try {\n const call = await runMutation({\n operation: async () => {\n const response = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(updatedAt),\n () =>\n apiCall('/api/currencies/fetch-configs', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: configId,\n syncTime: syncTime || null,\n }),\n }),\n )\n if (!response.ok) {\n throw Object.assign(new Error('[internal] currencies.fetchConfig.updateSyncTime failed'), {\n status: response.status,\n ...((response.result as Record<string, unknown> | null) ?? {}),\n })\n }\n return response\n },\n context: {\n formId: mutationContextId,\n resourceKind: 'currencies.fetch_config',\n resourceId: configId,\n retryLastMutation,\n },\n mutationPayload: { id: configId, syncTime: syncTime || null },\n })\n\n const result = call.result as { config?: FetchConfig } | null\n if (result?.config) {\n setConfigs((prev) =>\n prev.map((c) => (c.id === configId ? (result.config as FetchConfig) : c))\n )\n }\n } catch (err: any) {\n if (extractOptimisticLockConflict(err)) return\n flash(err.message || t('currencies.fetch.error_update_sync_time'), 'error')\n }\n }, [mutationContextId, retryLastMutation, runMutation, t])\n\n const fetchNow = useCallback(async (provider: string) => {\n setFetching(provider)\n\n try {\n const call = await runMutation({\n operation: () => apiCall('/api/currencies/fetch-rates', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n providers: [provider],\n }),\n }),\n context: {\n formId: mutationContextId,\n resourceKind: 'currencies.fetch_rates',\n retryLastMutation,\n },\n mutationPayload: { providers: [provider] },\n })\n\n const result = call.result as { byProvider?: Record<string, { count: number; errors?: string[] }> } | null\n if (result) {\n const byProvider = result.byProvider as Record<string, { count: number; errors?: string[] }>\n const count = byProvider?.[provider]?.count || 0\n\n if (count === 0) {\n flash(t('currencies.fetch.sync_no_rates'), 'warning')\n } else {\n flash(t('currencies.fetch.sync_success', { count }), 'success')\n }\n await loadConfigs()\n }\n } catch (err: any) {\n flash(err.message || t('currencies.fetch.sync_error'), 'error')\n } finally {\n setFetching(null)\n }\n }, [mutationContextId, retryLastMutation, runMutation, t, loadConfigs])\n\n useEffect(() => {\n loadConfigs()\n }, [loadConfigs])\n\n function formatLastSync(date: string | null): string {\n if (!date) return t('currencies.fetch.last_sync_never')\n return new Date(date).toLocaleString()\n }\n\n function getStatusBadge(status: string | null) {\n if (!status) return null\n\n return (\n <StatusBadge variant={STATUS_BADGE_VARIANT[status] ?? 'neutral'}>\n {status}\n </StatusBadge>\n )\n }\n\n function getProviderName(provider: string): string {\n if (provider === 'NBP') return t('currencies.fetch.provider_nbp')\n if (provider === 'Raiffeisen Bank Polska') return t('currencies.fetch.provider_raiffeisen')\n return provider\n }\n\n function getProviderDescription(provider: string): string {\n if (provider === 'NBP') {\n return t('currencies.fetch.provider_nbp_description')\n }\n if (provider === 'Raiffeisen Bank Polska') {\n return t('currencies.fetch.provider_raiffeisen_description')\n }\n return ''\n }\n\n if (loading || initializing) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-4\">\n <header className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('currencies.fetch.title')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('currencies.fetch.description')}\n </p>\n </header>\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n {initializing ? t('currencies.fetch.initializing_providers') : t('currencies.fetch.loading')}\n </div>\n </section>\n )\n }\n\n return (\n <section className=\"space-y-6 rounded-lg border bg-background p-4\">\n <header className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('currencies.fetch.title')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('currencies.fetch.description')}\n </p>\n </header>\n\n <div className=\"space-y-3\">\n {configs.map((config) => (\n <div\n key={config.id}\n className=\"rounded-lg border bg-card p-4\"\n >\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1\">\n <h3 className=\"text-sm font-medium\">\n {getProviderName(config.provider)}\n </h3>\n <p className=\"text-xs text-muted-foreground mt-0.5\">\n {getProviderDescription(config.provider)}\n </p>\n </div>\n\n <Switch\n checked={config.isEnabled}\n onCheckedChange={() => toggleEnabled(config.id, config.isEnabled, config.updatedAt)}\n />\n </div>\n\n {config.isEnabled && (\n <>\n <div className=\"flex items-baseline gap-3 flex-wrap mt-3\">\n <div className=\"flex items-baseline gap-2\">\n <label className=\"text-xs text-muted-foreground whitespace-nowrap\">\n {t('currencies.fetch.sync_time')}:\n </label>\n <TimeInput\n value={config.syncTime || '09:00'}\n onChange={(time) => updateSyncTime(config.id, time, config.updatedAt)}\n />\n </div>\n\n <div className=\"flex items-baseline gap-2\">\n <label className=\"text-xs text-muted-foreground whitespace-nowrap\">\n {t('currencies.fetch.last_sync')}:\n </label>\n <span className=\"text-xs text-muted-foreground\">\n {formatLastSync(config.lastSyncAt)}\n </span>\n {getStatusBadge(config.lastSyncStatus)}\n </div>\n\n <Button\n type=\"button\"\n onClick={() => fetchNow(config.provider)}\n disabled={fetching === config.provider}\n size=\"default\"\n className=\"ml-auto min-w-32\"\n >\n {fetching === config.provider ? (\n <div>\n <Spinner className=\"mr-1.5\" size='sm' />\n {t('currencies.fetch.fetching')}\n </div>\n ) : (\n t('currencies.fetch.fetch_now')\n )}\n </Button>\n </div>\n\n {config.lastSyncCount !== null && (\n <div className=\"text-xs text-muted-foreground mt-2\">\n {t('currencies.fetch.last_sync_count')}: <span className=\"font-medium\">{config.lastSyncCount}</span>\n </div>\n )}\n\n {config.lastSyncMessage && config.lastSyncStatus === 'error' && (\n <Alert status=\"error\" style=\"lighter\" size=\"xs\" className=\"mt-2\">\n {config.lastSyncMessage}\n </Alert>\n )}\n </>\n )}\n </div>\n ))}\n\n {configs.length === 0 && (\n <div className=\"rounded border bg-background/80 p-6 text-center\">\n <p className=\"text-sm text-muted-foreground\">\n {t('currencies.fetch.no_providers')}\n </p>\n <Button\n type=\"button\"\n onClick={() => initializeMissingProviders(availableProviders)}\n >\n {t('currencies.fetch.initialize_providers')}\n </Button>\n </div>\n )}\n </div>\n </section>\n )\n}\n"],
5
+ "mappings": ";AAiRM,SAuEQ,UAvER,KAyBE,YAzBF;AA/QN,SAAS,UAAU,WAAW,aAAa,eAAe;AAC1D,SAAS,aAAa;AACtB,SAAS,SAAS,mCAAmC;AACrD,SAAS,2BAA2B,qCAAqC;AACzE,SAAS,0BAA0B;AACnC,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,mBAA4C;AACrD,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAC1B,SAAS,YAAY;AAcrB,MAAM,uBAA2D;AAAA,EAC/D,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS;AACX;AAEe,SAAR,yBAA0C;AAC/C,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,SAAS,UAAU,IAAI,SAAwB,CAAC,CAAC;AACxD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,IAAI;AAC3C,QAAM,CAAC,UAAU,WAAW,IAAI,SAAwB,IAAI;AAC5D,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AAGtD,QAAM,qBAAqB,QAAQ,MAAM,CAAC,OAAO,wBAAwB,GAAG,CAAC,CAAC;AAE9E,QAAM,oBAAoB;AAC1B,QAAM,EAAE,aAAa,kBAAkB,IAAI,mBAKxC;AAAA,IACD,WAAW;AAAA,IACX,gBAAgB,EAAE,8BAA8B,4BAA4B;AAAA,EAC9E,CAAC;AAED,QAAM,uBAAuB,YAAY,OAAO,aAAqB;AACnE,QAAI;AACF,YAAM,YAAY;AAAA,QAChB,WAAW,MAAM,QAAQ,iCAAiC;AAAA,UACxD,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU;AAAA,YACnB;AAAA,YACA,WAAW;AAAA,YACX,UAAU;AAAA,UACZ,CAAC;AAAA,QACH,CAAC;AAAA,QACD,SAAS;AAAA,UACP,QAAQ;AAAA,UACR,cAAc;AAAA,UACd;AAAA,QACF;AAAA,QACA,iBAAiB,EAAE,UAAU,WAAW,OAAO,UAAU,QAAQ;AAAA,MACnE,CAAC;AAAA,IACH,SAAS,KAAU;AAEjB,UAAI,CAAC,IAAI,SAAS,SAAS,oBAAoB,GAAG;AAChD,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF,GAAG,CAAC,mBAAmB,mBAAmB,WAAW,CAAC;AAEtD,QAAM,6BAA6B,YAAY,OAAO,cAAwB;AAC5E,oBAAgB,IAAI;AACpB,QAAI;AACF,iBAAW,YAAY,WAAW;AAChC,cAAM,qBAAqB,QAAQ;AAAA,MACrC;AAEA,YAAM,EAAE,OAAO,IAAI,MAAM,QAAQ,+BAA+B;AAChE,UAAI,QAAQ,SAAS;AACnB,mBAAW,OAAO,OAAwB;AAAA,MAC5C;AAAA,IACF,SAAS,KAAU;AACjB,cAAQ,MAAM,mCAAmC,GAAG;AAAA,IACtD,UAAE;AACA,sBAAgB,KAAK;AAAA,IACvB;AAAA,EACF,GAAG,CAAC,oBAAoB,CAAC;AAEzB,QAAM,cAAc,YAAY,YAAY;AAC1C,QAAI;AACF,YAAM,EAAE,OAAO,IAAI,MAAM,QAAQ,+BAA+B;AAEhE,UAAI,QAAQ,SAAS;AACnB,cAAM,kBAAkB,OAAO;AAC/B,mBAAW,eAAe;AAG1B,cAAM,oBAAoB,gBAAgB,IAAI,CAAC,MAAM,EAAE,QAAQ;AAC/D,cAAM,mBAAmB,mBAAmB;AAAA,UAC1C,CAAC,MAAM,CAAC,kBAAkB,SAAS,CAAC;AAAA,QACtC;AAGA,YAAI,iBAAiB,SAAS,GAAG;AAC/B,gBAAM,2BAA2B,gBAAgB;AAAA,QACnD;AAAA,MACF;AAAA,IACF,SAAS,KAAU;AACjB,YAAM,IAAI,WAAW,EAAE,qCAAqC,GAAG,OAAO;AAAA,IACxE,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,oBAAoB,4BAA4B,CAAC,CAAC;AAEtD,QAAM,gBAAgB,YAAY,OAAO,UAAkB,cAAuB,cAA8B;AAC9G,QAAI;AACF,YAAM,OAAO,MAAM,YAAY;AAAA,QAC7B,WAAW,YAAY;AACrB,gBAAM,WAAW,MAAM;AAAA,YACrB,0BAA0B,SAAS;AAAA,YACnC,MACE,QAAQ,iCAAiC;AAAA,cACvC,QAAQ;AAAA,cACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,cAC9C,MAAM,KAAK,UAAU;AAAA,gBACnB,IAAI;AAAA,gBACJ,WAAW,CAAC;AAAA,cACd,CAAC;AAAA,YACH,CAAC;AAAA,UACL;AACA,cAAI,CAAC,SAAS,IAAI;AAChB,kBAAM,OAAO,OAAO,IAAI,MAAM,iDAAiD,GAAG;AAAA,cAChF,QAAQ,SAAS;AAAA,cACjB,GAAK,SAAS,UAA6C,CAAC;AAAA,YAC9D,CAAC;AAAA,UACH;AACA,iBAAO;AAAA,QACT;AAAA,QACA,SAAS;AAAA,UACP,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,YAAY;AAAA,UACZ;AAAA,QACF;AAAA,QACA,iBAAiB,EAAE,IAAI,UAAU,WAAW,CAAC,aAAa;AAAA,MAC5D,CAAC;AAED,YAAM,SAAS,KAAK;AACpB,UAAI,QAAQ,QAAQ;AAClB;AAAA,UAAW,CAAC,SACV,KAAK,IAAI,CAAC,MAAO,EAAE,OAAO,WAAY,OAAO,SAAyB,CAAE;AAAA,QAC1E;AACA;AAAA,UACE,eACI,EAAE,oCAAoC,IACtC,EAAE,mCAAmC;AAAA,UACzC;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,KAAU;AACjB,UAAI,8BAA8B,GAAG,EAAG;AACxC,YAAM,IAAI,WAAW,EAAE,sCAAsC,GAAG,OAAO;AAAA,IACzE;AAAA,EACF,GAAG,CAAC,mBAAmB,mBAAmB,aAAa,CAAC,CAAC;AAEzD,QAAM,iBAAiB,YAAY,OAAO,UAAkB,UAAkB,cAA8B;AAC1G,QAAI;AACF,YAAM,OAAO,MAAM,YAAY;AAAA,QAC7B,WAAW,YAAY;AACrB,gBAAM,WAAW,MAAM;AAAA,YACrB,0BAA0B,SAAS;AAAA,YACnC,MACE,QAAQ,iCAAiC;AAAA,cACvC,QAAQ;AAAA,cACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,cAC9C,MAAM,KAAK,UAAU;AAAA,gBACnB,IAAI;AAAA,gBACJ,UAAU,YAAY;AAAA,cACxB,CAAC;AAAA,YACH,CAAC;AAAA,UACL;AACA,cAAI,CAAC,SAAS,IAAI;AAChB,kBAAM,OAAO,OAAO,IAAI,MAAM,yDAAyD,GAAG;AAAA,cACxF,QAAQ,SAAS;AAAA,cACjB,GAAK,SAAS,UAA6C,CAAC;AAAA,YAC9D,CAAC;AAAA,UACH;AACA,iBAAO;AAAA,QACT;AAAA,QACA,SAAS;AAAA,UACP,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,YAAY;AAAA,UACZ;AAAA,QACF;AAAA,QACA,iBAAiB,EAAE,IAAI,UAAU,UAAU,YAAY,KAAK;AAAA,MAC9D,CAAC;AAED,YAAM,SAAS,KAAK;AACpB,UAAI,QAAQ,QAAQ;AAClB;AAAA,UAAW,CAAC,SACV,KAAK,IAAI,CAAC,MAAO,EAAE,OAAO,WAAY,OAAO,SAAyB,CAAE;AAAA,QAC1E;AAAA,MACF;AAAA,IACF,SAAS,KAAU;AACjB,UAAI,8BAA8B,GAAG,EAAG;AACxC,YAAM,IAAI,WAAW,EAAE,yCAAyC,GAAG,OAAO;AAAA,IAC5E;AAAA,EACF,GAAG,CAAC,mBAAmB,mBAAmB,aAAa,CAAC,CAAC;AAEzD,QAAM,WAAW,YAAY,OAAO,aAAqB;AACvD,gBAAY,QAAQ;AAEpB,QAAI;AACF,YAAM,OAAO,MAAM,YAAY;AAAA,QAC7B,WAAW,MAAM,QAAQ,+BAA+B;AAAA,UACtD,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU;AAAA,YACnB,WAAW,CAAC,QAAQ;AAAA,UACtB,CAAC;AAAA,QACH,CAAC;AAAA,QACD,SAAS;AAAA,UACP,QAAQ;AAAA,UACR,cAAc;AAAA,UACd;AAAA,QACF;AAAA,QACA,iBAAiB,EAAE,WAAW,CAAC,QAAQ,EAAE;AAAA,MAC3C,CAAC;AAED,YAAM,SAAS,KAAK;AACpB,UAAI,QAAQ;AACV,cAAM,aAAa,OAAO;AAC1B,cAAM,QAAQ,aAAa,QAAQ,GAAG,SAAS;AAE/C,YAAI,UAAU,GAAG;AACf,gBAAM,EAAE,gCAAgC,GAAG,SAAS;AAAA,QACtD,OAAO;AACL,gBAAM,EAAE,iCAAiC,EAAE,MAAM,CAAC,GAAG,SAAS;AAAA,QAChE;AACA,cAAM,YAAY;AAAA,MACpB;AAAA,IACF,SAAS,KAAU;AACjB,YAAM,IAAI,WAAW,EAAE,6BAA6B,GAAG,OAAO;AAAA,IAChE,UAAE;AACA,kBAAY,IAAI;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,mBAAmB,mBAAmB,aAAa,GAAG,WAAW,CAAC;AAEtE,YAAU,MAAM;AACd,gBAAY;AAAA,EACd,GAAG,CAAC,WAAW,CAAC;AAEhB,WAAS,eAAe,MAA6B;AACnD,QAAI,CAAC,KAAM,QAAO,EAAE,kCAAkC;AACtD,WAAO,IAAI,KAAK,IAAI,EAAE,eAAe;AAAA,EACvC;AAEA,WAAS,eAAe,QAAuB;AAC7C,QAAI,CAAC,OAAQ,QAAO;AAEpB,WACE,oBAAC,eAAY,SAAS,qBAAqB,MAAM,KAAK,WACnD,kBACH;AAAA,EAEJ;AAEA,WAAS,gBAAgB,UAA0B;AACjD,QAAI,aAAa,MAAO,QAAO,EAAE,+BAA+B;AAChE,QAAI,aAAa,yBAA0B,QAAO,EAAE,sCAAsC;AAC1F,WAAO;AAAA,EACT;AAEA,WAAS,uBAAuB,UAA0B;AACxD,QAAI,aAAa,OAAO;AACtB,aAAO,EAAE,2CAA2C;AAAA,IACtD;AACA,QAAI,aAAa,0BAA0B;AACzC,aAAO,EAAE,kDAAkD;AAAA,IAC7D;AACA,WAAO;AAAA,EACT;AAEA,MAAI,WAAW,cAAc;AAC3B,WACE,qBAAC,aAAQ,WAAU,iDACjB;AAAA,2BAAC,YAAO,WAAU,aAChB;AAAA,4BAAC,QAAG,WAAU,yBAAyB,YAAE,wBAAwB,GAAE;AAAA,QACnE,oBAAC,OAAE,WAAU,iCACV,YAAE,8BAA8B,GACnC;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,yDACb;AAAA,4BAAC,WAAQ,WAAU,WAAU;AAAA,QAC5B,eAAe,EAAE,yCAAyC,IAAI,EAAE,0BAA0B;AAAA,SAC7F;AAAA,OACF;AAAA,EAEJ;AAEA,SACE,qBAAC,aAAQ,WAAU,iDACjB;AAAA,yBAAC,YAAO,WAAU,aAChB;AAAA,0BAAC,QAAG,WAAU,yBAAyB,YAAE,wBAAwB,GAAE;AAAA,MACnE,oBAAC,OAAE,WAAU,iCACV,YAAE,8BAA8B,GACnC;AAAA,OACF;AAAA,IAEA,qBAAC,SAAI,WAAU,aACZ;AAAA,cAAQ,IAAI,CAAC,WACZ;AAAA,QAAC;AAAA;AAAA,UAEC,WAAU;AAAA,UAEV;AAAA,iCAAC,SAAI,WAAU,0CACb;AAAA,mCAAC,SAAI,WAAU,UACb;AAAA,oCAAC,QAAG,WAAU,uBACX,0BAAgB,OAAO,QAAQ,GAClC;AAAA,gBACA,oBAAC,OAAE,WAAU,wCACV,iCAAuB,OAAO,QAAQ,GACzC;AAAA,iBACF;AAAA,cAEA;AAAA,gBAAC;AAAA;AAAA,kBACC,SAAS,OAAO;AAAA,kBAChB,iBAAiB,MAAM,cAAc,OAAO,IAAI,OAAO,WAAW,OAAO,SAAS;AAAA;AAAA,cACpF;AAAA,eACF;AAAA,YAEC,OAAO,aACN,iCACE;AAAA,mCAAC,SAAI,WAAU,4CACb;AAAA,qCAAC,SAAI,WAAU,6BACb;AAAA,uCAAC,WAAM,WAAU,mDACd;AAAA,sBAAE,4BAA4B;AAAA,oBAAE;AAAA,qBACnC;AAAA,kBACA;AAAA,oBAAC;AAAA;AAAA,sBACC,OAAO,OAAO,YAAY;AAAA,sBAC1B,UAAU,CAAC,SAAS,eAAe,OAAO,IAAI,MAAM,OAAO,SAAS;AAAA;AAAA,kBACtE;AAAA,mBACF;AAAA,gBAEA,qBAAC,SAAI,WAAU,6BACb;AAAA,uCAAC,WAAM,WAAU,mDACd;AAAA,sBAAE,4BAA4B;AAAA,oBAAE;AAAA,qBACnC;AAAA,kBACA,oBAAC,UAAK,WAAU,iCACb,yBAAe,OAAO,UAAU,GACnC;AAAA,kBACC,eAAe,OAAO,cAAc;AAAA,mBACvC;AAAA,gBAEA;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAS,MAAM,SAAS,OAAO,QAAQ;AAAA,oBACvC,UAAU,aAAa,OAAO;AAAA,oBAC9B,MAAK;AAAA,oBACL,WAAU;AAAA,oBAET,uBAAa,OAAO,WACnB,qBAAC,SACC;AAAA,0CAAC,WAAQ,WAAU,UAAS,MAAK,MAAK;AAAA,sBACrC,EAAE,2BAA2B;AAAA,uBAChC,IAEA,EAAE,4BAA4B;AAAA;AAAA,gBAElC;AAAA,iBACF;AAAA,cAEC,OAAO,kBAAkB,QACxB,qBAAC,SAAI,WAAU,sCACZ;AAAA,kBAAE,kCAAkC;AAAA,gBAAE;AAAA,gBAAE,oBAAC,UAAK,WAAU,eAAe,iBAAO,eAAc;AAAA,iBAC/F;AAAA,cAGD,OAAO,mBAAmB,OAAO,mBAAmB,WACnD,oBAAC,SAAM,QAAO,SAAQ,OAAM,WAAU,MAAK,MAAK,WAAU,QACvD,iBAAO,iBACV;AAAA,eAEJ;AAAA;AAAA;AAAA,QAvEG,OAAO;AAAA,MAyEd,CACD;AAAA,MAEA,QAAQ,WAAW,KAClB,qBAAC,SAAI,WAAU,mDACb;AAAA,4BAAC,OAAE,WAAU,iCACV,YAAE,+BAA+B,GACpC;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM,2BAA2B,kBAAkB;AAAA,YAE3D,YAAE,uCAAuC;AAAA;AAAA,QAC5C;AAAA,SACF;AAAA,OAEJ;AAAA,KACF;AAEJ;",
6
6
  "names": []
7
7
  }