@open-mercato/core 0.6.6-develop.6094.1.28b081ea16 → 0.6.6-develop.6121.1.5797a901b3

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 (317) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/catalog_product/index.js +52 -0
  3. package/dist/generated/entities/catalog_product/index.js.map +2 -2
  4. package/dist/generated/entities/catalog_product_variant/index.js +4 -0
  5. package/dist/generated/entities/catalog_product_variant/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +28 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/catalog/api/products/route.js +103 -33
  9. package/dist/modules/catalog/api/products/route.js.map +2 -2
  10. package/dist/modules/catalog/api/variants/route.js +4 -0
  11. package/dist/modules/catalog/api/variants/route.js.map +2 -2
  12. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +46 -2
  13. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  14. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +4 -0
  15. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  16. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js +2 -0
  17. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js.map +2 -2
  18. package/dist/modules/catalog/backend/catalog/products/create/page.js +42 -2
  19. package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
  20. package/dist/modules/catalog/commands/products.js +251 -14
  21. package/dist/modules/catalog/commands/products.js.map +2 -2
  22. package/dist/modules/catalog/commands/variants.js +72 -1
  23. package/dist/modules/catalog/commands/variants.js.map +2 -2
  24. package/dist/modules/catalog/components/products/ProductComplianceSection.js +500 -0
  25. package/dist/modules/catalog/components/products/ProductComplianceSection.js.map +7 -0
  26. package/dist/modules/catalog/components/products/VariantBuilder.js +39 -1
  27. package/dist/modules/catalog/components/products/VariantBuilder.js.map +2 -2
  28. package/dist/modules/catalog/components/products/productForm.js +164 -2
  29. package/dist/modules/catalog/components/products/productForm.js.map +2 -2
  30. package/dist/modules/catalog/components/products/variantForm.js +2 -0
  31. package/dist/modules/catalog/components/products/variantForm.js.map +2 -2
  32. package/dist/modules/catalog/data/entities.js +93 -0
  33. package/dist/modules/catalog/data/entities.js.map +2 -2
  34. package/dist/modules/catalog/data/types.js +28 -0
  35. package/dist/modules/catalog/data/types.js.map +2 -2
  36. package/dist/modules/catalog/data/validators.js +100 -5
  37. package/dist/modules/catalog/data/validators.js.map +2 -2
  38. package/dist/modules/catalog/lib/gtin.js +61 -0
  39. package/dist/modules/catalog/lib/gtin.js.map +7 -0
  40. package/dist/modules/catalog/migrations/Migration20260611090000.js +17 -0
  41. package/dist/modules/catalog/migrations/Migration20260611090000.js.map +7 -0
  42. package/dist/modules/catalog/translations.js +1 -1
  43. package/dist/modules/catalog/translations.js.map +2 -2
  44. package/dist/modules/configs/api/cache/route.js +60 -10
  45. package/dist/modules/configs/api/cache/route.js.map +3 -3
  46. package/dist/modules/configs/components/CachePanel.js +52 -29
  47. package/dist/modules/configs/components/CachePanel.js.map +2 -2
  48. package/dist/modules/currencies/api/currencies/options/route.js +18 -4
  49. package/dist/modules/currencies/api/currencies/options/route.js.map +2 -2
  50. package/dist/modules/currencies/services/rateFetchingService.js +35 -18
  51. package/dist/modules/currencies/services/rateFetchingService.js.map +2 -2
  52. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js +16 -16
  53. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js.map +2 -2
  54. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +1 -1
  55. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
  56. package/dist/modules/customers/backend/calendar/page.js +10 -0
  57. package/dist/modules/customers/backend/calendar/page.js.map +7 -0
  58. package/dist/modules/customers/backend/calendar/page.meta.js +25 -0
  59. package/dist/modules/customers/backend/calendar/page.meta.js.map +7 -0
  60. package/dist/modules/customers/cli.js +1 -0
  61. package/dist/modules/customers/cli.js.map +2 -2
  62. package/dist/modules/customers/components/calendar/AgendaList.js +182 -0
  63. package/dist/modules/customers/components/calendar/AgendaList.js.map +7 -0
  64. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +331 -0
  65. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +7 -0
  66. package/dist/modules/customers/components/calendar/CalendarFooter.js +48 -0
  67. package/dist/modules/customers/components/calendar/CalendarFooter.js.map +7 -0
  68. package/dist/modules/customers/components/calendar/CalendarHeader.js +36 -0
  69. package/dist/modules/customers/components/calendar/CalendarHeader.js.map +7 -0
  70. package/dist/modules/customers/components/calendar/CalendarScreen.js +516 -0
  71. package/dist/modules/customers/components/calendar/CalendarScreen.js.map +7 -0
  72. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +163 -0
  73. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +7 -0
  74. package/dist/modules/customers/components/calendar/CalendarTabs.js +60 -0
  75. package/dist/modules/customers/components/calendar/CalendarTabs.js.map +7 -0
  76. package/dist/modules/customers/components/calendar/CalendarToolbar.js +262 -0
  77. package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +7 -0
  78. package/dist/modules/customers/components/calendar/EventBlock.js +149 -0
  79. package/dist/modules/customers/components/calendar/EventBlock.js.map +7 -0
  80. package/dist/modules/customers/components/calendar/EventPeekPopover.js +72 -0
  81. package/dist/modules/customers/components/calendar/EventPeekPopover.js.map +7 -0
  82. package/dist/modules/customers/components/calendar/MonthGrid.js +214 -0
  83. package/dist/modules/customers/components/calendar/MonthGrid.js.map +7 -0
  84. package/dist/modules/customers/components/calendar/ShortcutsDialog.js +44 -0
  85. package/dist/modules/customers/components/calendar/ShortcutsDialog.js.map +7 -0
  86. package/dist/modules/customers/components/calendar/TimeGrid.js +410 -0
  87. package/dist/modules/customers/components/calendar/TimeGrid.js.map +7 -0
  88. package/dist/modules/customers/components/calendar/UpcomingCards.js +175 -0
  89. package/dist/modules/customers/components/calendar/UpcomingCards.js.map +7 -0
  90. package/dist/modules/customers/components/calendar/editor/CategoryField.js +75 -0
  91. package/dist/modules/customers/components/calendar/editor/CategoryField.js.map +7 -0
  92. package/dist/modules/customers/components/calendar/editor/LocationField.js +30 -0
  93. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +7 -0
  94. package/dist/modules/customers/components/calendar/editor/PeopleField.js +135 -0
  95. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +7 -0
  96. package/dist/modules/customers/components/calendar/editor/RelatedToField.js +218 -0
  97. package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +7 -0
  98. package/dist/modules/customers/components/calendar/editor/RepeatField.js +119 -0
  99. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +7 -0
  100. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +83 -0
  101. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +7 -0
  102. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +44 -0
  103. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +7 -0
  104. package/dist/modules/customers/components/calendar/editor/hooks.js +132 -0
  105. package/dist/modules/customers/components/calendar/editor/hooks.js.map +7 -0
  106. package/dist/modules/customers/components/calendar/editor/inputs.js +160 -0
  107. package/dist/modules/customers/components/calendar/editor/inputs.js.map +7 -0
  108. package/dist/modules/customers/components/calendar/editor/lookups.js +107 -0
  109. package/dist/modules/customers/components/calendar/editor/lookups.js.map +7 -0
  110. package/dist/modules/customers/components/calendar/types.js +30 -0
  111. package/dist/modules/customers/components/calendar/types.js.map +7 -0
  112. package/dist/modules/customers/components/calendar/useCalendarItems.js +131 -0
  113. package/dist/modules/customers/components/calendar/useCalendarItems.js.map +7 -0
  114. package/dist/modules/customers/components/calendar/useCalendarPreferences.js +41 -0
  115. package/dist/modules/customers/components/calendar/useCalendarPreferences.js.map +7 -0
  116. package/dist/modules/customers/lib/calendar/categories.js +28 -0
  117. package/dist/modules/customers/lib/calendar/categories.js.map +7 -0
  118. package/dist/modules/customers/lib/calendar/conflicts.js +72 -0
  119. package/dist/modules/customers/lib/calendar/conflicts.js.map +7 -0
  120. package/dist/modules/customers/lib/calendar/editorPayload.js +271 -0
  121. package/dist/modules/customers/lib/calendar/editorPayload.js.map +7 -0
  122. package/dist/modules/customers/lib/calendar/grid.js +48 -0
  123. package/dist/modules/customers/lib/calendar/grid.js.map +7 -0
  124. package/dist/modules/customers/lib/calendar/layout.js +39 -0
  125. package/dist/modules/customers/lib/calendar/layout.js.map +7 -0
  126. package/dist/modules/customers/lib/calendar/mapItem.js +79 -0
  127. package/dist/modules/customers/lib/calendar/mapItem.js.map +7 -0
  128. package/dist/modules/customers/lib/calendar/preferences.js +71 -0
  129. package/dist/modules/customers/lib/calendar/preferences.js.map +7 -0
  130. package/dist/modules/customers/lib/calendar/range.js +46 -0
  131. package/dist/modules/customers/lib/calendar/range.js.map +7 -0
  132. package/dist/modules/customers/lib/calendar/recurrence.js +113 -0
  133. package/dist/modules/customers/lib/calendar/recurrence.js.map +7 -0
  134. package/dist/modules/dashboards/services/widgetDataService.js +5 -5
  135. package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
  136. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +61 -1
  137. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
  138. package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +33 -1
  139. package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
  140. package/dist/modules/dictionaries/api/[dictionaryId]/route.js +61 -1
  141. package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
  142. package/dist/modules/dictionaries/api/route.js +33 -1
  143. package/dist/modules/dictionaries/api/route.js.map +2 -2
  144. package/dist/modules/feature_toggles/lib/queries.js +36 -52
  145. package/dist/modules/feature_toggles/lib/queries.js.map +2 -2
  146. package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js +32 -1
  147. package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js.map +2 -2
  148. package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js +32 -1
  149. package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js.map +2 -2
  150. package/dist/modules/perspectives/api/[tableId]/route.js +73 -2
  151. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  152. package/dist/modules/perspectives/services/perspectiveService.js +5 -3
  153. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  154. package/dist/modules/planner/api/access.js +9 -0
  155. package/dist/modules/planner/api/access.js.map +2 -2
  156. package/dist/modules/planner/api/availability-date-specific.js +22 -1
  157. package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
  158. package/dist/modules/planner/api/availability-weekly.js +22 -1
  159. package/dist/modules/planner/api/availability-weekly.js.map +2 -2
  160. package/dist/modules/resources/api/resources/tags/assign/route.js +39 -0
  161. package/dist/modules/resources/api/resources/tags/assign/route.js.map +2 -2
  162. package/dist/modules/resources/api/resources/tags/unassign/route.js +39 -0
  163. package/dist/modules/resources/api/resources/tags/unassign/route.js.map +2 -2
  164. package/dist/modules/sales/api/settings/document-numbers/route.js +33 -1
  165. package/dist/modules/sales/api/settings/document-numbers/route.js.map +2 -2
  166. package/dist/modules/sales/api/settings/order-editing/route.js +35 -3
  167. package/dist/modules/sales/api/settings/order-editing/route.js.map +2 -2
  168. package/dist/modules/staff/api/leave-requests/accept/route.js +41 -0
  169. package/dist/modules/staff/api/leave-requests/accept/route.js.map +2 -2
  170. package/dist/modules/staff/api/leave-requests/reject/route.js +41 -0
  171. package/dist/modules/staff/api/leave-requests/reject/route.js.map +2 -2
  172. package/dist/modules/staff/api/team-members/self/route.js +40 -0
  173. package/dist/modules/staff/api/team-members/self/route.js.map +2 -2
  174. package/dist/modules/staff/api/team-members/tags/assign/route.js +41 -0
  175. package/dist/modules/staff/api/team-members/tags/assign/route.js.map +2 -2
  176. package/dist/modules/staff/api/team-members/tags/unassign/route.js +41 -0
  177. package/dist/modules/staff/api/team-members/tags/unassign/route.js.map +2 -2
  178. package/dist/modules/translations/api/[entityType]/[entityId]/route.js +61 -1
  179. package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
  180. package/dist/modules/translations/api/context.js +7 -0
  181. package/dist/modules/translations/api/context.js.map +2 -2
  182. package/dist/modules/translations/api/put/locales.js +33 -1
  183. package/dist/modules/translations/api/put/locales.js.map +2 -2
  184. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +6 -7
  185. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  186. package/dist/modules/workflows/backend/instances/[id]/page.js.map +1 -1
  187. package/dist/modules/workflows/components/EdgeEditDialog.js.map +1 -1
  188. package/dist/modules/workflows/components/EdgeEditDialogCrudForm.js.map +1 -1
  189. package/dist/modules/workflows/components/NodeEditDialog.js.map +1 -1
  190. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +1 -1
  191. package/dist/modules/workflows/components/WorkflowGraph.js.map +1 -1
  192. package/dist/modules/workflows/components/WorkflowGraphImpl.js +17 -9
  193. package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +2 -2
  194. package/dist/modules/workflows/components/mobile/MobileVisualEditor.js.map +1 -1
  195. package/dist/modules/workflows/frontend/checkout-demo/deriveCurrentStep.js +8 -0
  196. package/dist/modules/workflows/frontend/checkout-demo/deriveCurrentStep.js.map +7 -0
  197. package/dist/modules/workflows/frontend/checkout-demo/page.js +2 -20
  198. package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
  199. package/dist/modules/workflows/lib/graph-utils.js +8 -0
  200. package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
  201. package/generated/entities/catalog_product/index.ts +26 -0
  202. package/generated/entities/catalog_product_variant/index.ts +2 -0
  203. package/generated/entity-fields-registry.ts +28 -0
  204. package/package.json +7 -7
  205. package/src/modules/catalog/api/products/route.ts +123 -39
  206. package/src/modules/catalog/api/variants/route.ts +4 -0
  207. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +43 -0
  208. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +14 -0
  209. package/src/modules/catalog/backend/catalog/products/[productId]/variants/create/page.tsx +2 -0
  210. package/src/modules/catalog/backend/catalog/products/create/page.tsx +41 -0
  211. package/src/modules/catalog/commands/products.ts +307 -2
  212. package/src/modules/catalog/commands/variants.ts +90 -1
  213. package/src/modules/catalog/components/products/ProductComplianceSection.tsx +497 -0
  214. package/src/modules/catalog/components/products/VariantBuilder.tsx +43 -0
  215. package/src/modules/catalog/components/products/productForm.ts +262 -0
  216. package/src/modules/catalog/components/products/variantForm.ts +4 -0
  217. package/src/modules/catalog/data/entities.ts +97 -0
  218. package/src/modules/catalog/data/types.ts +36 -0
  219. package/src/modules/catalog/data/validators.ts +159 -3
  220. package/src/modules/catalog/i18n/de.json +64 -0
  221. package/src/modules/catalog/i18n/en.json +64 -0
  222. package/src/modules/catalog/i18n/es.json +64 -0
  223. package/src/modules/catalog/i18n/pl.json +64 -0
  224. package/src/modules/catalog/lib/gtin.ts +66 -0
  225. package/src/modules/catalog/migrations/.snapshot-open-mercato.json +457 -0
  226. package/src/modules/catalog/migrations/Migration20260611090000.ts +17 -0
  227. package/src/modules/catalog/translations.ts +1 -1
  228. package/src/modules/configs/api/cache/route.ts +72 -10
  229. package/src/modules/configs/components/CachePanel.tsx +59 -29
  230. package/src/modules/currencies/api/currencies/options/route.ts +22 -4
  231. package/src/modules/currencies/services/rateFetchingService.ts +63 -26
  232. package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/page.tsx +18 -18
  233. package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +1 -1
  234. package/src/modules/customer_accounts/i18n/de.json +1 -1
  235. package/src/modules/customer_accounts/i18n/en.json +1 -1
  236. package/src/modules/customer_accounts/i18n/es.json +1 -1
  237. package/src/modules/customer_accounts/i18n/pl.json +1 -1
  238. package/src/modules/customers/backend/calendar/page.meta.ts +23 -0
  239. package/src/modules/customers/backend/calendar/page.tsx +12 -0
  240. package/src/modules/customers/cli.ts +1 -0
  241. package/src/modules/customers/components/calendar/AgendaList.tsx +219 -0
  242. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +386 -0
  243. package/src/modules/customers/components/calendar/CalendarFooter.tsx +61 -0
  244. package/src/modules/customers/components/calendar/CalendarHeader.tsx +40 -0
  245. package/src/modules/customers/components/calendar/CalendarScreen.tsx +590 -0
  246. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +207 -0
  247. package/src/modules/customers/components/calendar/CalendarTabs.tsx +63 -0
  248. package/src/modules/customers/components/calendar/CalendarToolbar.tsx +295 -0
  249. package/src/modules/customers/components/calendar/EventBlock.tsx +205 -0
  250. package/src/modules/customers/components/calendar/EventPeekPopover.tsx +100 -0
  251. package/src/modules/customers/components/calendar/MonthGrid.tsx +236 -0
  252. package/src/modules/customers/components/calendar/ShortcutsDialog.tsx +57 -0
  253. package/src/modules/customers/components/calendar/TimeGrid.tsx +510 -0
  254. package/src/modules/customers/components/calendar/UpcomingCards.tsx +220 -0
  255. package/src/modules/customers/components/calendar/editor/CategoryField.tsx +79 -0
  256. package/src/modules/customers/components/calendar/editor/LocationField.tsx +37 -0
  257. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +148 -0
  258. package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +242 -0
  259. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +132 -0
  260. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +110 -0
  261. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +50 -0
  262. package/src/modules/customers/components/calendar/editor/hooks.ts +162 -0
  263. package/src/modules/customers/components/calendar/editor/inputs.tsx +158 -0
  264. package/src/modules/customers/components/calendar/editor/lookups.ts +160 -0
  265. package/src/modules/customers/components/calendar/types.ts +163 -0
  266. package/src/modules/customers/components/calendar/useCalendarItems.ts +162 -0
  267. package/src/modules/customers/components/calendar/useCalendarPreferences.ts +52 -0
  268. package/src/modules/customers/i18n/de.json +195 -0
  269. package/src/modules/customers/i18n/en.json +195 -0
  270. package/src/modules/customers/i18n/es.json +195 -0
  271. package/src/modules/customers/i18n/pl.json +195 -0
  272. package/src/modules/customers/lib/calendar/categories.ts +26 -0
  273. package/src/modules/customers/lib/calendar/conflicts.ts +117 -0
  274. package/src/modules/customers/lib/calendar/editorPayload.ts +388 -0
  275. package/src/modules/customers/lib/calendar/grid.ts +50 -0
  276. package/src/modules/customers/lib/calendar/layout.ts +47 -0
  277. package/src/modules/customers/lib/calendar/mapItem.ts +94 -0
  278. package/src/modules/customers/lib/calendar/preferences.ts +95 -0
  279. package/src/modules/customers/lib/calendar/range.ts +45 -0
  280. package/src/modules/customers/lib/calendar/recurrence.ts +137 -0
  281. package/src/modules/dashboards/services/widgetDataService.ts +8 -6
  282. package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +61 -1
  283. package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +33 -1
  284. package/src/modules/dictionaries/api/[dictionaryId]/route.ts +65 -1
  285. package/src/modules/dictionaries/api/route.ts +36 -1
  286. package/src/modules/feature_toggles/lib/queries.ts +41 -60
  287. package/src/modules/perspectives/api/[tableId]/[perspectiveId]/route.ts +34 -1
  288. package/src/modules/perspectives/api/[tableId]/roles/[roleId]/route.ts +34 -1
  289. package/src/modules/perspectives/api/[tableId]/route.ts +107 -32
  290. package/src/modules/perspectives/services/perspectiveService.ts +9 -5
  291. package/src/modules/planner/api/access.ts +9 -0
  292. package/src/modules/planner/api/availability-date-specific.ts +22 -1
  293. package/src/modules/planner/api/availability-weekly.ts +22 -1
  294. package/src/modules/resources/api/resources/tags/assign/route.ts +42 -0
  295. package/src/modules/resources/api/resources/tags/unassign/route.ts +42 -0
  296. package/src/modules/sales/api/settings/document-numbers/route.ts +35 -1
  297. package/src/modules/sales/api/settings/order-editing/route.ts +37 -3
  298. package/src/modules/staff/api/leave-requests/accept/route.ts +45 -0
  299. package/src/modules/staff/api/leave-requests/reject/route.ts +45 -0
  300. package/src/modules/staff/api/team-members/self/route.ts +43 -0
  301. package/src/modules/staff/api/team-members/tags/assign/route.ts +45 -0
  302. package/src/modules/staff/api/team-members/tags/unassign/route.ts +45 -0
  303. package/src/modules/translations/api/[entityType]/[entityId]/route.ts +65 -1
  304. package/src/modules/translations/api/context.ts +7 -0
  305. package/src/modules/translations/api/put/locales.ts +35 -1
  306. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +11 -9
  307. package/src/modules/workflows/backend/instances/[id]/page.tsx +1 -1
  308. package/src/modules/workflows/components/EdgeEditDialog.tsx +1 -1
  309. package/src/modules/workflows/components/EdgeEditDialogCrudForm.tsx +1 -1
  310. package/src/modules/workflows/components/NodeEditDialog.tsx +1 -1
  311. package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +1 -1
  312. package/src/modules/workflows/components/WorkflowGraph.tsx +2 -2
  313. package/src/modules/workflows/components/WorkflowGraphImpl.tsx +27 -13
  314. package/src/modules/workflows/components/mobile/MobileVisualEditor.tsx +3 -3
  315. package/src/modules/workflows/frontend/checkout-demo/deriveCurrentStep.ts +12 -0
  316. package/src/modules/workflows/frontend/checkout-demo/page.tsx +3 -23
  317. package/src/modules/workflows/lib/graph-utils.ts +21 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/configs/components/CachePanel.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\n\nconst API_PATH = '/api/configs/cache'\n\ntype CrudCacheSegment = {\n segment: string\n resource: string | null\n method: string | null\n path: string | null\n keyCount: number\n}\n\ntype CrudCacheStats = {\n generatedAt: string\n totalKeys: number\n segments: CrudCacheSegment[]\n}\n\ntype FetchState = {\n loading: boolean\n error: string | null\n stats: CrudCacheStats | null\n}\n\nexport function CachePanel() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [state, setState] = React.useState<FetchState>({ loading: true, error: null, stats: null })\n const [canManage, setCanManage] = React.useState(false)\n const [checkingFeature, setCheckingFeature] = React.useState(true)\n const [purgingAll, setPurgingAll] = React.useState(false)\n const [segmentPurges, setSegmentPurges] = React.useState<Record<string, boolean>>({})\n\n const loadStats = React.useCallback(async () => {\n setState((current) => ({ ...current, loading: true, error: null }))\n try {\n const stats = await readApiResultOrThrow<CrudCacheStats>(API_PATH, undefined, {\n errorMessage: t('configs.cache.loadError', 'Failed to load cache statistics.'),\n })\n setState({ loading: false, error: null, stats })\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.loadError', 'Failed to load cache statistics.')\n setState({ loading: false, error: message, stats: null })\n }\n }, [t])\n\n React.useEffect(() => {\n loadStats().catch(() => {})\n }, [loadStats])\n\n React.useEffect(() => {\n let cancelled = false\n async function checkManageFeature() {\n try {\n const payload = await readApiResultOrThrow<{ ok?: boolean; granted?: unknown }>(\n '/api/auth/feature-check',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['configs.cache.manage'] }),\n },\n {\n errorMessage: t('configs.cache.loadError', 'Failed to load cache statistics.'),\n allowNullResult: true,\n },\n )\n if (cancelled) return\n const granted = Array.isArray(payload?.granted)\n ? (payload.granted as unknown[]).filter((feature) => typeof feature === 'string') as string[]\n : []\n const hasFeature = payload?.ok === true || granted.includes('configs.cache.manage')\n setCanManage(hasFeature)\n } catch {\n if (!cancelled) setCanManage(false)\n } finally {\n if (!cancelled) setCheckingFeature(false)\n }\n }\n checkManageFeature().catch(() => {})\n return () => {\n cancelled = true\n }\n }, [t])\n\n const handleRefresh = React.useCallback(() => {\n loadStats().catch(() => {})\n }, [loadStats])\n\n const handlePurgeAll = React.useCallback(async () => {\n if (!canManage || purgingAll) return\n const confirmed = await confirm({\n title: t('configs.cache.purgeAllConfirm', 'Purge all cached entries for this tenant?'),\n variant: 'destructive',\n })\n if (!confirmed) return\n setPurgingAll(true)\n try {\n const payload = await readApiResultOrThrow<{ stats?: CrudCacheStats }>(\n API_PATH,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action: 'purgeAll' }),\n },\n {\n errorMessage: t('configs.cache.purgeError', 'Failed to purge cache segment.'),\n allowNullResult: true,\n },\n )\n const stats = payload?.stats\n if (stats) {\n setState({ loading: false, error: null, stats })\n } else {\n handleRefresh()\n }\n flash(t('configs.cache.purgeAllSuccess', 'Cache cleared.'), 'success')\n setSegmentPurges({})\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.purgeError', 'Failed to purge cache segment.')\n flash(message, 'error')\n } finally {\n setPurgingAll(false)\n }\n }, [canManage, confirm, purgingAll, t, handleRefresh]);\n\n\n const handlePurgeSegment = React.useCallback(async (segment: string) => {\n if (!canManage || segmentPurges[segment]) return\n const confirmed = await confirm({\n title: t('configs.cache.purgeSegmentConfirm', 'Purge cached entries for this segment?'),\n variant: 'destructive',\n })\n if (!confirmed) return\n setSegmentPurges((prev) => ({ ...prev, [segment]: true }))\n try {\n const payload = await readApiResultOrThrow<{ stats?: CrudCacheStats; deleted?: number }>(\n API_PATH,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action: 'purgeSegment', segment }),\n },\n {\n errorMessage: t('configs.cache.purgeError', 'Failed to purge cache segment.'),\n allowNullResult: true,\n },\n )\n const stats = payload?.stats\n if (stats) {\n setState({ loading: false, error: null, stats })\n } else {\n handleRefresh()\n }\n const deleted = typeof payload?.deleted === 'number' ? payload.deleted : 0\n flash(\n t('configs.cache.purgeSegmentSuccess', {\n segment,\n count: deleted,\n }),\n 'success'\n )\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.purgeError', 'Failed to purge cache segment.')\n flash(message, 'error')\n } finally {\n setSegmentPurges((prev) => {\n const next = { ...prev }\n delete next[segment]\n return next\n })\n }\n }, [canManage, confirm, segmentPurges, t, handleRefresh]);\n\n if (state.loading) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-6\">\n {ConfirmDialogElement}\n <header className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}\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 {t('configs.cache.loading', 'Loading cache statistics\u2026')}\n </div>\n </section>\n )\n }\n\n if (state.error) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-6\">\n {ConfirmDialogElement}\n <header className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}\n </p>\n </header>\n <div className=\"rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700\">\n {state.error}\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"outline\" onClick={handleRefresh}>\n {t('configs.cache.retry', 'Retry')}\n </Button>\n </div>\n </section>\n )\n }\n\n const stats = state.stats\n const canShowActions = !checkingFeature && canManage\n\n return (\n <section className=\"space-y-6 rounded-lg border bg-background p-6\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}\n </p>\n {stats ? (\n <>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'configs.cache.generatedAt',\n 'Stats generated {{timestamp}}',\n { timestamp: new Date(stats.generatedAt).toLocaleString() }\n )}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'configs.cache.totalEntries',\n '{{count}} cached entries',\n { count: stats.totalKeys }\n )}\n </p>\n </>\n ) : null}\n </div>\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" onClick={handleRefresh}>\n {t('configs.cache.refresh', 'Refresh')}\n </Button>\n {canShowActions ? (\n <Button variant=\"destructive\" disabled={purgingAll} onClick={() => { void handlePurgeAll() }}>\n {purgingAll\n ? t('configs.cache.purgeAllLoading', 'Purging\u2026')\n : t('configs.cache.purgeAll', 'Purge all cache')}\n </Button>\n ) : null}\n </div>\n </header>\n <div className=\"space-y-4 rounded-lg border bg-card p-4\">\n {stats && stats.segments.length ? (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[560px] text-sm\">\n <thead>\n <tr className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.segment', 'Segment')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.path', 'Path')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.method', 'Method')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.count', 'Cached keys')}\n </th>\n {canShowActions ? (\n <th className=\"px-3 py-2 text-right\">\n {t('configs.cache.table.actions', 'Actions')}\n </th>\n ) : null}\n </tr>\n </thead>\n <tbody>\n {stats.segments.map((segment) => {\n const isPurging = !!segmentPurges[segment.segment]\n return (\n <tr key={segment.segment} className=\"border-t\">\n <td className=\"px-3 py-2 align-top font-medium\">\n <div className=\"flex flex-col\">\n <span>{segment.segment}</span>\n {segment.resource ? (\n <span className=\"text-xs text-muted-foreground\">{segment.resource}</span>\n ) : null}\n </div>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <code className=\"text-xs text-muted-foreground\">\n {segment.path ?? t('configs.cache.table.pathUnknown', 'n/a')}\n </code>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <span className=\"text-xs uppercase text-muted-foreground\">\n {segment.method ?? 'GET'}\n </span>\n </td>\n <td className=\"px-3 py-2 align-top\">\n {t('configs.cache.table.countValue', '{{count}} keys', { count: segment.keyCount })}\n </td>\n {canShowActions ? (\n <td className=\"px-3 py-2 align-top text-right\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={isPurging}\n onClick={() => { void handlePurgeSegment(segment.segment) }}\n >\n {isPurging\n ? t('configs.cache.purgeSegmentLoading', 'Purging\u2026')\n : t('configs.cache.purgeSegment', 'Purge segment')}\n </Button>\n </td>\n ) : null}\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.empty', 'No cached responses for this tenant.')}\n </p>\n )}\n </div>\n {ConfirmDialogElement}\n </section>\n )\n}\n\nexport default CachePanel\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\n\nconst API_PATH = '/api/configs/cache'\nconst CACHE_MUTATION_CONTEXT_ID = 'configs-cache-panel'\n\ntype CrudCacheSegment = {\n segment: string\n resource: string | null\n method: string | null\n path: string | null\n keyCount: number\n}\n\ntype CrudCacheStats = {\n generatedAt: string\n totalKeys: number\n segments: CrudCacheSegment[]\n}\n\ntype FetchState = {\n loading: boolean\n error: string | null\n stats: CrudCacheStats | null\n}\n\nexport function CachePanel() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [state, setState] = React.useState<FetchState>({ loading: true, error: null, stats: null })\n const [canManage, setCanManage] = React.useState(false)\n const [checkingFeature, setCheckingFeature] = React.useState(true)\n const [purgingAll, setPurgingAll] = React.useState(false)\n const [segmentPurges, setSegmentPurges] = React.useState<Record<string, boolean>>({})\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: CACHE_MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n const cacheMutationContext = React.useMemo(\n () => ({\n formId: CACHE_MUTATION_CONTEXT_ID,\n resourceKind: 'configs.cache',\n retryLastMutation,\n }),\n [retryLastMutation],\n )\n\n const loadStats = React.useCallback(async () => {\n setState((current) => ({ ...current, loading: true, error: null }))\n try {\n const stats = await readApiResultOrThrow<CrudCacheStats>(API_PATH, undefined, {\n errorMessage: t('configs.cache.loadError', 'Failed to load cache statistics.'),\n })\n setState({ loading: false, error: null, stats })\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.loadError', 'Failed to load cache statistics.')\n setState({ loading: false, error: message, stats: null })\n }\n }, [t])\n\n React.useEffect(() => {\n loadStats().catch(() => {})\n }, [loadStats])\n\n React.useEffect(() => {\n let cancelled = false\n async function checkManageFeature() {\n try {\n const payload = await readApiResultOrThrow<{ ok?: boolean; granted?: unknown }>(\n '/api/auth/feature-check',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['configs.cache.manage'] }),\n },\n {\n errorMessage: t('configs.cache.loadError', 'Failed to load cache statistics.'),\n allowNullResult: true,\n },\n )\n if (cancelled) return\n const granted = Array.isArray(payload?.granted)\n ? (payload.granted as unknown[]).filter((feature) => typeof feature === 'string') as string[]\n : []\n const hasFeature = payload?.ok === true || granted.includes('configs.cache.manage')\n setCanManage(hasFeature)\n } catch {\n if (!cancelled) setCanManage(false)\n } finally {\n if (!cancelled) setCheckingFeature(false)\n }\n }\n checkManageFeature().catch(() => {})\n return () => {\n cancelled = true\n }\n }, [t])\n\n const handleRefresh = React.useCallback(() => {\n loadStats().catch(() => {})\n }, [loadStats])\n\n const handlePurgeAll = React.useCallback(async () => {\n if (!canManage || purgingAll) return\n const confirmed = await confirm({\n title: t('configs.cache.purgeAllConfirm', 'Purge all cached entries for this tenant?'),\n variant: 'destructive',\n })\n if (!confirmed) return\n setPurgingAll(true)\n try {\n const payload = await runMutation({\n operation: () =>\n readApiResultOrThrow<{ stats?: CrudCacheStats }>(\n API_PATH,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action: 'purgeAll' }),\n },\n {\n errorMessage: t('configs.cache.purgeError', 'Failed to purge cache segment.'),\n allowNullResult: true,\n },\n ),\n context: cacheMutationContext,\n mutationPayload: { action: 'purgeAll' },\n })\n const stats = payload?.stats\n if (stats) {\n setState({ loading: false, error: null, stats })\n } else {\n handleRefresh()\n }\n flash(t('configs.cache.purgeAllSuccess', 'Cache cleared.'), 'success')\n setSegmentPurges({})\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.purgeError', 'Failed to purge cache segment.')\n flash(message, 'error')\n } finally {\n setPurgingAll(false)\n }\n }, [canManage, confirm, purgingAll, t, handleRefresh, runMutation, cacheMutationContext]);\n\n\n const handlePurgeSegment = React.useCallback(async (segment: string) => {\n if (!canManage || segmentPurges[segment]) return\n const confirmed = await confirm({\n title: t('configs.cache.purgeSegmentConfirm', 'Purge cached entries for this segment?'),\n variant: 'destructive',\n })\n if (!confirmed) return\n setSegmentPurges((prev) => ({ ...prev, [segment]: true }))\n try {\n const payload = await runMutation({\n operation: () =>\n readApiResultOrThrow<{ stats?: CrudCacheStats; deleted?: number }>(\n API_PATH,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action: 'purgeSegment', segment }),\n },\n {\n errorMessage: t('configs.cache.purgeError', 'Failed to purge cache segment.'),\n allowNullResult: true,\n },\n ),\n context: cacheMutationContext,\n mutationPayload: { action: 'purgeSegment', segment },\n })\n const stats = payload?.stats\n if (stats) {\n setState({ loading: false, error: null, stats })\n } else {\n handleRefresh()\n }\n const deleted = typeof payload?.deleted === 'number' ? payload.deleted : 0\n flash(\n t('configs.cache.purgeSegmentSuccess', {\n segment,\n count: deleted,\n }),\n 'success'\n )\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.purgeError', 'Failed to purge cache segment.')\n flash(message, 'error')\n } finally {\n setSegmentPurges((prev) => {\n const next = { ...prev }\n delete next[segment]\n return next\n })\n }\n }, [canManage, confirm, segmentPurges, t, handleRefresh, runMutation, cacheMutationContext]);\n\n if (state.loading) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-6\">\n {ConfirmDialogElement}\n <header className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}\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 {t('configs.cache.loading', 'Loading cache statistics\u2026')}\n </div>\n </section>\n )\n }\n\n if (state.error) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-6\">\n {ConfirmDialogElement}\n <header className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}\n </p>\n </header>\n <div className=\"rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700\">\n {state.error}\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"outline\" type=\"button\" onClick={handleRefresh}>\n {t('configs.cache.retry', 'Retry')}\n </Button>\n </div>\n </section>\n )\n }\n\n const stats = state.stats\n const canShowActions = !checkingFeature && canManage\n\n return (\n <section className=\"space-y-6 rounded-lg border bg-background p-6\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}\n </p>\n {stats ? (\n <>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'configs.cache.generatedAt',\n 'Stats generated {{timestamp}}',\n { timestamp: new Date(stats.generatedAt).toLocaleString() }\n )}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'configs.cache.totalEntries',\n '{{count}} cached entries',\n { count: stats.totalKeys }\n )}\n </p>\n </>\n ) : null}\n </div>\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" type=\"button\" onClick={handleRefresh}>\n {t('configs.cache.refresh', 'Refresh')}\n </Button>\n {canShowActions ? (\n <Button variant=\"destructive\" type=\"button\" disabled={purgingAll} onClick={() => { void handlePurgeAll() }}>\n {purgingAll\n ? t('configs.cache.purgeAllLoading', 'Purging\u2026')\n : t('configs.cache.purgeAll', 'Purge all cache')}\n </Button>\n ) : null}\n </div>\n </header>\n <div className=\"space-y-4 rounded-lg border bg-card p-4\">\n {stats && stats.segments.length ? (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[560px] text-sm\">\n <thead>\n <tr className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.segment', 'Segment')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.path', 'Path')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.method', 'Method')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.count', 'Cached keys')}\n </th>\n {canShowActions ? (\n <th className=\"px-3 py-2 text-right\">\n {t('configs.cache.table.actions', 'Actions')}\n </th>\n ) : null}\n </tr>\n </thead>\n <tbody>\n {stats.segments.map((segment) => {\n const isPurging = !!segmentPurges[segment.segment]\n return (\n <tr key={segment.segment} className=\"border-t\">\n <td className=\"px-3 py-2 align-top font-medium\">\n <div className=\"flex flex-col\">\n <span>{segment.segment}</span>\n {segment.resource ? (\n <span className=\"text-xs text-muted-foreground\">{segment.resource}</span>\n ) : null}\n </div>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <code className=\"text-xs text-muted-foreground\">\n {segment.path ?? t('configs.cache.table.pathUnknown', 'n/a')}\n </code>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <span className=\"text-xs uppercase text-muted-foreground\">\n {segment.method ?? 'GET'}\n </span>\n </td>\n <td className=\"px-3 py-2 align-top\">\n {t('configs.cache.table.countValue', '{{count}} keys', { count: segment.keyCount })}\n </td>\n {canShowActions ? (\n <td className=\"px-3 py-2 align-top text-right\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n type=\"button\"\n disabled={isPurging}\n onClick={() => { void handlePurgeSegment(segment.segment) }}\n >\n {isPurging\n ? t('configs.cache.purgeSegmentLoading', 'Purging\u2026')\n : t('configs.cache.purgeSegment', 'Purge segment')}\n </Button>\n </td>\n ) : null}\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.empty', 'No cached responses for this tenant.')}\n </p>\n )}\n </div>\n {ConfirmDialogElement}\n </section>\n )\n}\n\nexport default CachePanel\n"],
5
+ "mappings": "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",
6
6
  "names": ["stats"]
7
7
  }
@@ -6,6 +6,7 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
6
6
  import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
7
7
  import {
8
8
  buildCollectionTags,
9
+ debugCrudCache,
9
10
  isCrudCacheEnabled,
10
11
  resolveCrudCache
11
12
  } from "@open-mercato/shared/lib/crud/cache";
@@ -51,9 +52,17 @@ async function GET(req) {
51
52
  const cache = isCrudCacheEnabled() && !searchTerm ? resolveCrudCache(container) : null;
52
53
  const cacheKey = cache ? buildOptionsCacheKey({ orgId, includeInactive: includeInactiveFlag, limit }) : null;
53
54
  if (cache && cacheKey) {
54
- const cached = await runWithCacheTenant(tenantId, () => cache.get(cacheKey));
55
- if (cached) {
56
- return NextResponse.json(cached);
55
+ try {
56
+ const cached = await runWithCacheTenant(tenantId, () => cache.get(cacheKey));
57
+ if (cached) {
58
+ return NextResponse.json(cached);
59
+ }
60
+ } catch (err) {
61
+ debugCrudCache("get", {
62
+ resource: CURRENCY_OPTIONS_RESOURCE,
63
+ key: cacheKey,
64
+ error: err instanceof Error ? err.message : String(err)
65
+ });
57
66
  }
58
67
  }
59
68
  const filter = {
@@ -91,7 +100,12 @@ async function GET(req) {
91
100
  tags: buildCollectionTags(CURRENCY_OPTIONS_RESOURCE, tenantId, [orgId])
92
101
  })
93
102
  );
94
- } catch {
103
+ } catch (err) {
104
+ debugCrudCache("store", {
105
+ resource: CURRENCY_OPTIONS_RESOURCE,
106
+ key: cacheKey,
107
+ error: err instanceof Error ? err.message : String(err)
108
+ });
95
109
  }
96
110
  }
97
111
  return NextResponse.json(payload);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/currencies/api/currencies/options/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport {\n buildCollectionTags,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { Currency } from '../../../data/entities'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['currencies.view'] },\n}\n\nconst CURRENCY_OPTIONS_RESOURCE = 'currencies.currency'\nconst CURRENCY_OPTIONS_TTL_MS = 5 * 60_000\n\nfunction buildOptionsCacheKey(params: {\n orgId: string | null\n includeInactive: boolean\n limit: number\n}): string {\n return `currencies:options:org=${params.orgId ?? 'null'}:active=${params.includeInactive ? 'all' : 'active'}:limit=${params.limit}`\n}\n\nconst optionsQuerySchema = z.object({\n q: z.string().optional(),\n query: z.string().optional(),\n search: z.string().optional(),\n includeInactive: z.enum(['true', 'false']).optional(),\n limit: z.coerce.number().min(1).max(100).default(50),\n}).loose()\n\ntype OptionsItem = {\n value: string\n label: string\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ items: [] }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = optionsQuerySchema.safeParse({\n q: url.searchParams.get('q') ?? undefined,\n query: url.searchParams.get('query') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n includeInactive: url.searchParams.get('includeInactive') ?? undefined,\n limit: url.searchParams.get('limit') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ items: [] }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n\n const { q, query, search, includeInactive, limit } = parsed.data\n const searchTerm = (q ?? query ?? search ?? '').trim()\n const tenantId = auth.tenantId\n const orgId = auth.orgId ?? null\n const includeInactiveFlag = includeInactive === 'true'\n\n // Only the unfiltered bootstrap call (no ILIKE search term) is the hot path\n // worth caching; search variants would multiply key cardinality with little\n // hit-rate benefit, so they always read straight through.\n const cache =\n isCrudCacheEnabled() && !searchTerm ? resolveCrudCache(container) : null\n const cacheKey = cache\n ? buildOptionsCacheKey({ orgId, includeInactive: includeInactiveFlag, limit })\n : null\n\n if (cache && cacheKey) {\n const cached = await runWithCacheTenant(tenantId, () => cache.get(cacheKey))\n if (cached) {\n return NextResponse.json(cached)\n }\n }\n\n const filter: any = {\n tenantId,\n deletedAt: null,\n }\n if (orgId) {\n filter.organizationId = orgId\n }\n\n if (!includeInactiveFlag) {\n filter.isActive = true\n }\n\n if (searchTerm) {\n const escaped = escapeLikePattern(searchTerm)\n filter.$or = [\n { code: { $ilike: `%${escaped}%` } },\n { name: { $ilike: `%${escaped}%` } },\n ]\n }\n\n const rows = await em.find(Currency, filter, {\n orderBy: { code: 'ASC' },\n limit,\n })\n\n const items: OptionsItem[] = rows.map((currency) => ({\n value: String(currency.code),\n label: `${currency.code} - ${currency.name}`,\n }))\n\n const payload = { items }\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(tenantId, () =>\n cache.set(cacheKey, payload, {\n ttl: CURRENCY_OPTIONS_TTL_MS,\n tags: buildCollectionTags(CURRENCY_OPTIONS_RESOURCE, tenantId, [orgId]),\n }),\n )\n } catch {}\n }\n\n return NextResponse.json(payload)\n}\n\nconst optionsResponseSchema = z.object({\n items: z.array(\n z.object({\n value: z.string(),\n label: z.string(),\n })\n ),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Currencies',\n summary: 'Currency options',\n methods: {\n GET: {\n summary: 'List currency options',\n description: 'Returns currencies formatted for select inputs.',\n query: optionsQuerySchema,\n responses: [\n { status: 200, description: 'Option list', schema: optionsResponseSchema },\n { status: 401, description: 'Unauthorized', schema: z.object({ items: z.array(z.any()) }) },\n { status: 400, description: 'Invalid query', schema: z.object({ items: z.array(z.any()) }) },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport {\n buildCollectionTags,\n debugCrudCache,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { Currency } from '../../../data/entities'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['currencies.view'] },\n}\n\nconst CURRENCY_OPTIONS_RESOURCE = 'currencies.currency'\nconst CURRENCY_OPTIONS_TTL_MS = 5 * 60_000\n\nfunction buildOptionsCacheKey(params: {\n orgId: string | null\n includeInactive: boolean\n limit: number\n}): string {\n return `currencies:options:org=${params.orgId ?? 'null'}:active=${params.includeInactive ? 'all' : 'active'}:limit=${params.limit}`\n}\n\nconst optionsQuerySchema = z.object({\n q: z.string().optional(),\n query: z.string().optional(),\n search: z.string().optional(),\n includeInactive: z.enum(['true', 'false']).optional(),\n limit: z.coerce.number().min(1).max(100).default(50),\n}).loose()\n\ntype OptionsItem = {\n value: string\n label: string\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ items: [] }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = optionsQuerySchema.safeParse({\n q: url.searchParams.get('q') ?? undefined,\n query: url.searchParams.get('query') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n includeInactive: url.searchParams.get('includeInactive') ?? undefined,\n limit: url.searchParams.get('limit') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ items: [] }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n\n const { q, query, search, includeInactive, limit } = parsed.data\n const searchTerm = (q ?? query ?? search ?? '').trim()\n const tenantId = auth.tenantId\n const orgId = auth.orgId ?? null\n const includeInactiveFlag = includeInactive === 'true'\n\n // Only the unfiltered bootstrap call (no ILIKE search term) is the hot path\n // worth caching; search variants would multiply key cardinality with little\n // hit-rate benefit, so they always read straight through.\n const cache =\n isCrudCacheEnabled() && !searchTerm ? resolveCrudCache(container) : null\n const cacheKey = cache\n ? buildOptionsCacheKey({ orgId, includeInactive: includeInactiveFlag, limit })\n : null\n\n if (cache && cacheKey) {\n try {\n const cached = await runWithCacheTenant(tenantId, () => cache.get(cacheKey))\n if (cached) {\n return NextResponse.json(cached)\n }\n } catch (err) {\n // A cache-backend read error must degrade to a fresh DB read, never a 500.\n debugCrudCache('get', {\n resource: CURRENCY_OPTIONS_RESOURCE,\n key: cacheKey,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n }\n\n const filter: any = {\n tenantId,\n deletedAt: null,\n }\n if (orgId) {\n filter.organizationId = orgId\n }\n\n if (!includeInactiveFlag) {\n filter.isActive = true\n }\n\n if (searchTerm) {\n const escaped = escapeLikePattern(searchTerm)\n filter.$or = [\n { code: { $ilike: `%${escaped}%` } },\n { name: { $ilike: `%${escaped}%` } },\n ]\n }\n\n const rows = await em.find(Currency, filter, {\n orderBy: { code: 'ASC' },\n limit,\n })\n\n const items: OptionsItem[] = rows.map((currency) => ({\n value: String(currency.code),\n label: `${currency.code} - ${currency.name}`,\n }))\n\n const payload = { items }\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(tenantId, () =>\n cache.set(cacheKey, payload, {\n ttl: CURRENCY_OPTIONS_TTL_MS,\n tags: buildCollectionTags(CURRENCY_OPTIONS_RESOURCE, tenantId, [orgId]),\n }),\n )\n } catch (err) {\n // A cache write must never break the request; log it for observability\n // instead of swallowing the failure silently (matches the CRUD factory).\n debugCrudCache('store', {\n resource: CURRENCY_OPTIONS_RESOURCE,\n key: cacheKey,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n }\n\n return NextResponse.json(payload)\n}\n\nconst optionsResponseSchema = z.object({\n items: z.array(\n z.object({\n value: z.string(),\n label: z.string(),\n })\n ),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Currencies',\n summary: 'Currency options',\n methods: {\n GET: {\n summary: 'List currency options',\n description: 'Returns currencies formatted for select inputs.',\n query: optionsQuerySchema,\n responses: [\n { status: 200, description: 'Option list', schema: optionsResponseSchema },\n { status: 401, description: 'Unauthorized', schema: z.object({ items: z.array(z.any()) }) },\n { status: 400, description: 'Invalid query', schema: z.object({ items: z.array(z.any()) }) },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -20,35 +20,52 @@ class RateFetchingService {
20
20
  const existingCurrencies = await this.getExistingCurrencies(scope);
21
21
  const currencyCodeSet = new Set(existingCurrencies.map((c) => c.code));
22
22
  const providerList = options.providers?.length ? options.providers : Array.from(this.providers.keys());
23
- for (const providerSource of providerList) {
24
- const provider = this.providers.get(providerSource);
25
- if (!provider) {
26
- result.errors.push(`Unknown provider: ${providerSource}`);
23
+ const outcomes = await Promise.all(
24
+ providerList.map(
25
+ (providerSource) => this.fetchFromProvider(providerSource, date, scope, currencyCodeSet)
26
+ )
27
+ );
28
+ for (const outcome of outcomes) {
29
+ if (outcome.kind === "skipped") {
30
+ result.errors.push(outcome.error);
27
31
  continue;
28
32
  }
29
- if (!provider.isAvailable()) {
30
- result.errors.push(`Provider not available: ${providerSource}`);
33
+ if (outcome.kind === "failed") {
34
+ result.errors.push(`${outcome.providerSource}: ${outcome.error}`);
35
+ result.byProvider[outcome.providerSource] = { count: 0, errors: [outcome.error] };
31
36
  continue;
32
37
  }
33
38
  try {
34
- const rates = await provider.fetchRates(date, scope, currencyCodeSet);
35
- const validRates = rates.filter(
36
- (r) => currencyCodeSet.has(r.fromCurrencyCode) && currencyCodeSet.has(r.toCurrencyCode)
37
- );
38
- const stored = await this.storeRates(validRates, scope);
39
- result.byProvider[providerSource] = { count: stored };
39
+ const stored = await this.storeRates(outcome.rates, scope);
40
+ result.byProvider[outcome.providerSource] = { count: stored };
40
41
  result.totalFetched += stored;
41
42
  } catch (err) {
42
- const errorMsg = `${providerSource}: ${err.message}`;
43
- result.errors.push(errorMsg);
44
- result.byProvider[providerSource] = {
45
- count: 0,
46
- errors: [err.message]
47
- };
43
+ const message = err instanceof Error ? err.message : String(err);
44
+ result.errors.push(`${outcome.providerSource}: ${message}`);
45
+ result.byProvider[outcome.providerSource] = { count: 0, errors: [message] };
48
46
  }
49
47
  }
50
48
  return result;
51
49
  }
50
+ async fetchFromProvider(providerSource, date, scope, currencyCodeSet) {
51
+ const provider = this.providers.get(providerSource);
52
+ if (!provider) {
53
+ return { kind: "skipped", providerSource, error: `Unknown provider: ${providerSource}` };
54
+ }
55
+ if (!provider.isAvailable()) {
56
+ return { kind: "skipped", providerSource, error: `Provider not available: ${providerSource}` };
57
+ }
58
+ try {
59
+ const rates = await provider.fetchRates(date, scope, currencyCodeSet);
60
+ const validRates = rates.filter(
61
+ (r) => currencyCodeSet.has(r.fromCurrencyCode) && currencyCodeSet.has(r.toCurrencyCode)
62
+ );
63
+ return { kind: "fetched", providerSource, rates: validRates };
64
+ } catch (err) {
65
+ const message = err instanceof Error ? err.message : String(err);
66
+ return { kind: "failed", providerSource, error: message };
67
+ }
68
+ }
52
69
  async getExistingCurrencies(scope) {
53
70
  return this.em.find(Currency, {
54
71
  tenantId: scope.tenantId,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/currencies/services/rateFetchingService.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { RateProvider, RateProviderResult } from './providers/base'\nimport { Currency, ExchangeRate } from '../data/entities'\n\nexport interface FetchResult {\n totalFetched: number\n byProvider: Record<string, { count: number; errors?: string[] }>\n errors: string[]\n}\n\nexport interface FetchOptions {\n providers?: string[]\n forceUpdate?: boolean\n}\n\nconst exchangeRateKey = (\n fromCurrencyCode: string,\n toCurrencyCode: string,\n date: Date,\n source: string\n): string => `${fromCurrencyCode}|${toCurrencyCode}|${date.getTime()}|${source}`\n\nexport class RateFetchingService {\n private providers: Map<string, RateProvider>\n\n constructor(private em: EntityManager) {\n this.providers = new Map()\n }\n\n /**\n * Register a rate provider\n */\n registerProvider(provider: RateProvider): void {\n this.providers.set(provider.source, provider)\n }\n\n async fetchRatesForDate(\n date: Date,\n scope: { tenantId: string; organizationId: string },\n options: FetchOptions = {}\n ): Promise<FetchResult> {\n const result: FetchResult = {\n totalFetched: 0,\n byProvider: {},\n errors: [],\n }\n\n // Get existing currencies for validation\n const existingCurrencies = await this.getExistingCurrencies(scope)\n const currencyCodeSet = new Set(existingCurrencies.map((c) => c.code))\n\n // Determine which providers to use\n const providerList = options.providers?.length\n ? options.providers\n : Array.from(this.providers.keys())\n\n for (const providerSource of providerList) {\n const provider = this.providers.get(providerSource)\n\n if (!provider) {\n result.errors.push(`Unknown provider: ${providerSource}`)\n continue\n }\n\n if (!provider.isAvailable()) {\n result.errors.push(`Provider not available: ${providerSource}`)\n continue\n }\n\n try {\n const rates = await provider.fetchRates(date, scope, currencyCodeSet)\n\n // Filter: only currencies that exist in both directions\n const validRates = rates.filter(\n (r) =>\n currencyCodeSet.has(r.fromCurrencyCode) &&\n currencyCodeSet.has(r.toCurrencyCode)\n )\n\n const stored = await this.storeRates(validRates, scope)\n\n result.byProvider[providerSource] = { count: stored }\n result.totalFetched += stored\n } catch (err: any) {\n const errorMsg = `${providerSource}: ${err.message}`\n result.errors.push(errorMsg)\n result.byProvider[providerSource] = {\n count: 0,\n errors: [err.message],\n }\n }\n }\n\n return result\n }\n\n private async getExistingCurrencies(scope: {\n tenantId: string\n organizationId: string\n }): Promise<Currency[]> {\n return this.em.find(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isActive: true,\n deletedAt: null,\n })\n }\n\n private async storeRates(\n rates: RateProviderResult[],\n scope: { tenantId: string; organizationId: string }\n ): Promise<number> {\n if (rates.length === 0) return 0\n\n let stored = 0\n\n await this.em.transactional(async (em) => {\n // Prefetch every existing rate that could match this batch in a single query,\n // then index by composite key so the per-rate loop never hits the database.\n const fromCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.fromCurrencyCode)))\n const toCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.toCurrencyCode)))\n const sources = Array.from(new Set(rates.map((rate) => rate.source)))\n const dates = Array.from(new Set(rates.map((rate) => rate.date.getTime()))).map(\n (time) => new Date(time)\n )\n\n const existingRates = await em.find(ExchangeRate, {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n fromCurrencyCode: { $in: fromCurrencyCodes },\n toCurrencyCode: { $in: toCurrencyCodes },\n date: { $in: dates },\n source: { $in: sources },\n })\n\n const existingByKey = new Map<string, ExchangeRate>()\n for (const existing of existingRates) {\n existingByKey.set(\n exchangeRateKey(existing.fromCurrencyCode, existing.toCurrencyCode, existing.date, existing.source),\n existing\n )\n }\n\n const now = new Date()\n for (const rate of rates) {\n const key = exchangeRateKey(rate.fromCurrencyCode, rate.toCurrencyCode, rate.date, rate.source)\n const existing = existingByKey.get(key)\n\n if (existing) {\n // Update existing rate\n existing.rate = rate.rate\n existing.type = rate.type ?? null\n existing.updatedAt = now\n em.persist(existing)\n } else {\n // Create new rate\n const newRate = em.create(ExchangeRate, {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n fromCurrencyCode: rate.fromCurrencyCode,\n toCurrencyCode: rate.toCurrencyCode,\n rate: rate.rate,\n date: rate.date,\n source: rate.source,\n type: rate.type ?? null,\n isActive: true,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(newRate)\n // Track so duplicate keys within the same batch update in memory instead of double-inserting.\n existingByKey.set(key, newRate)\n }\n\n stored++\n }\n\n // Flush all changes at once\n await em.flush()\n })\n\n return stored\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { RateProvider, RateProviderResult } from './providers/base'\nimport { Currency, ExchangeRate } from '../data/entities'\n\nexport interface FetchResult {\n totalFetched: number\n byProvider: Record<string, { count: number; errors?: string[] }>\n errors: string[]\n}\n\nexport interface FetchOptions {\n providers?: string[]\n forceUpdate?: boolean\n}\n\ntype ProviderFetchOutcome =\n | { kind: 'skipped'; providerSource: string; error: string }\n | { kind: 'failed'; providerSource: string; error: string }\n | { kind: 'fetched'; providerSource: string; rates: RateProviderResult[] }\n\nconst exchangeRateKey = (\n fromCurrencyCode: string,\n toCurrencyCode: string,\n date: Date,\n source: string\n): string => `${fromCurrencyCode}|${toCurrencyCode}|${date.getTime()}|${source}`\n\nexport class RateFetchingService {\n private providers: Map<string, RateProvider>\n\n constructor(private em: EntityManager) {\n this.providers = new Map()\n }\n\n /**\n * Register a rate provider\n */\n registerProvider(provider: RateProvider): void {\n this.providers.set(provider.source, provider)\n }\n\n async fetchRatesForDate(\n date: Date,\n scope: { tenantId: string; organizationId: string },\n options: FetchOptions = {}\n ): Promise<FetchResult> {\n const result: FetchResult = {\n totalFetched: 0,\n byProvider: {},\n errors: [],\n }\n\n // Get existing currencies for validation\n const existingCurrencies = await this.getExistingCurrencies(scope)\n const currencyCodeSet = new Set(existingCurrencies.map((c) => c.code))\n\n // Determine which providers to use\n const providerList = options.providers?.length\n ? options.providers\n : Array.from(this.providers.keys())\n\n // Fetch every provider concurrently: provider calls are independent network I/O,\n // so overlapping them caps total latency at the slowest provider instead of the sum\n // of all provider timeouts. Each provider is isolated in its own try/catch so a\n // single failure never rejects the batch.\n const outcomes = await Promise.all(\n providerList.map((providerSource) =>\n this.fetchFromProvider(providerSource, date, scope, currencyCodeSet)\n )\n )\n\n // Persist sequentially in provider order: a single EntityManager is not safe for\n // concurrent transactions, and stable ordering keeps DB writes and the byProvider\n // response shape deterministic regardless of which fetch resolved first.\n for (const outcome of outcomes) {\n if (outcome.kind === 'skipped') {\n result.errors.push(outcome.error)\n continue\n }\n\n if (outcome.kind === 'failed') {\n result.errors.push(`${outcome.providerSource}: ${outcome.error}`)\n result.byProvider[outcome.providerSource] = { count: 0, errors: [outcome.error] }\n continue\n }\n\n try {\n const stored = await this.storeRates(outcome.rates, scope)\n result.byProvider[outcome.providerSource] = { count: stored }\n result.totalFetched += stored\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n result.errors.push(`${outcome.providerSource}: ${message}`)\n result.byProvider[outcome.providerSource] = { count: 0, errors: [message] }\n }\n }\n\n return result\n }\n\n private async fetchFromProvider(\n providerSource: string,\n date: Date,\n scope: { tenantId: string; organizationId: string },\n currencyCodeSet: Set<string>\n ): Promise<ProviderFetchOutcome> {\n const provider = this.providers.get(providerSource)\n\n if (!provider) {\n return { kind: 'skipped', providerSource, error: `Unknown provider: ${providerSource}` }\n }\n\n if (!provider.isAvailable()) {\n return { kind: 'skipped', providerSource, error: `Provider not available: ${providerSource}` }\n }\n\n try {\n const rates = await provider.fetchRates(date, scope, currencyCodeSet)\n\n // Filter: only currencies that exist in both directions\n const validRates = rates.filter(\n (r) =>\n currencyCodeSet.has(r.fromCurrencyCode) &&\n currencyCodeSet.has(r.toCurrencyCode)\n )\n\n return { kind: 'fetched', providerSource, rates: validRates }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n return { kind: 'failed', providerSource, error: message }\n }\n }\n\n private async getExistingCurrencies(scope: {\n tenantId: string\n organizationId: string\n }): Promise<Currency[]> {\n return this.em.find(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isActive: true,\n deletedAt: null,\n })\n }\n\n private async storeRates(\n rates: RateProviderResult[],\n scope: { tenantId: string; organizationId: string }\n ): Promise<number> {\n if (rates.length === 0) return 0\n\n let stored = 0\n\n await this.em.transactional(async (em) => {\n // Prefetch every existing rate that could match this batch in a single query,\n // then index by composite key so the per-rate loop never hits the database.\n const fromCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.fromCurrencyCode)))\n const toCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.toCurrencyCode)))\n const sources = Array.from(new Set(rates.map((rate) => rate.source)))\n const dates = Array.from(new Set(rates.map((rate) => rate.date.getTime()))).map(\n (time) => new Date(time)\n )\n\n const existingRates = await em.find(ExchangeRate, {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n fromCurrencyCode: { $in: fromCurrencyCodes },\n toCurrencyCode: { $in: toCurrencyCodes },\n date: { $in: dates },\n source: { $in: sources },\n })\n\n const existingByKey = new Map<string, ExchangeRate>()\n for (const existing of existingRates) {\n existingByKey.set(\n exchangeRateKey(existing.fromCurrencyCode, existing.toCurrencyCode, existing.date, existing.source),\n existing\n )\n }\n\n const now = new Date()\n for (const rate of rates) {\n const key = exchangeRateKey(rate.fromCurrencyCode, rate.toCurrencyCode, rate.date, rate.source)\n const existing = existingByKey.get(key)\n\n if (existing) {\n // Update existing rate\n existing.rate = rate.rate\n existing.type = rate.type ?? null\n existing.updatedAt = now\n em.persist(existing)\n } else {\n // Create new rate\n const newRate = em.create(ExchangeRate, {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n fromCurrencyCode: rate.fromCurrencyCode,\n toCurrencyCode: rate.toCurrencyCode,\n rate: rate.rate,\n date: rate.date,\n source: rate.source,\n type: rate.type ?? null,\n isActive: true,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(newRate)\n // Track so duplicate keys within the same batch update in memory instead of double-inserting.\n existingByKey.set(key, newRate)\n }\n\n stored++\n }\n\n // Flush all changes at once\n await em.flush()\n })\n\n return stored\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -210,27 +210,27 @@ function CustomerUserDetailPage({ params }) {
210
210
  React.useEffect(() => {
211
211
  if (!data) return;
212
212
  let cancelled = false;
213
- async function loadCrmNames() {
214
- if (data.personEntityId) {
215
- try {
216
- const call = await apiCall(`/api/customers/people/${encodeURIComponent(data.personEntityId)}`);
217
- if (!cancelled && call.ok && call.result) {
218
- setPersonName([call.result.firstName, call.result.lastName].filter(Boolean).join(" ") || call.result.id || null);
219
- }
220
- } catch {
213
+ async function loadPersonName() {
214
+ if (!data.personEntityId) return;
215
+ try {
216
+ const call = await apiCall(`/api/customers/people/${encodeURIComponent(data.personEntityId)}`);
217
+ if (!cancelled && call.ok && call.result) {
218
+ setPersonName([call.result.firstName, call.result.lastName].filter(Boolean).join(" ") || call.result.id || null);
221
219
  }
220
+ } catch {
222
221
  }
223
- if (data.customerEntityId) {
224
- try {
225
- const call = await apiCall(`/api/customers/${encodeURIComponent(data.customerEntityId)}`);
226
- if (!cancelled && call.ok && call.result) {
227
- setCompanyName(call.result.name || call.result.id || null);
228
- }
229
- } catch {
222
+ }
223
+ async function loadCompanyName() {
224
+ if (!data.customerEntityId) return;
225
+ try {
226
+ const call = await apiCall(`/api/customers/${encodeURIComponent(data.customerEntityId)}`);
227
+ if (!cancelled && call.ok && call.result) {
228
+ setCompanyName(call.result.name || call.result.id || null);
230
229
  }
230
+ } catch {
231
231
  }
232
232
  }
233
- loadCrmNames();
233
+ void Promise.all([loadPersonName(), loadCompanyName()]);
234
234
  return () => {
235
235
  cancelled = true;
236
236
  };