@open-mercato/core 0.9.0 → 0.9.1-develop.7462.1.94c09f158f

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 (276) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_dictionary_entry/index.js +2 -0
  3. package/dist/generated/entities/customer_dictionary_entry/index.js.map +2 -2
  4. package/dist/generated/entities/customer_interaction/index.js +2 -0
  5. package/dist/generated/entities/customer_interaction/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/attachments/api/route.js +29 -3
  9. package/dist/modules/attachments/api/route.js.map +2 -2
  10. package/dist/modules/attachments/lib/ocrLimits.js +49 -0
  11. package/dist/modules/attachments/lib/ocrLimits.js.map +7 -0
  12. package/dist/modules/attachments/lib/ocrQueue.js +76 -2
  13. package/dist/modules/attachments/lib/ocrQueue.js.map +2 -2
  14. package/dist/modules/attachments/lib/ocrService.js +4 -0
  15. package/dist/modules/attachments/lib/ocrService.js.map +2 -2
  16. package/dist/modules/attachments/lib/pdfProcessing.js +14 -2
  17. package/dist/modules/attachments/lib/pdfProcessing.js.map +2 -2
  18. package/dist/modules/attachments/lib/scoped-upload-service.js +4 -2
  19. package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
  20. package/dist/modules/attachments/lib/textExtraction.js +13 -1
  21. package/dist/modules/attachments/lib/textExtraction.js.map +2 -2
  22. package/dist/modules/catalog/commands/categories.js +1 -1
  23. package/dist/modules/catalog/commands/categories.js.map +2 -2
  24. package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
  25. package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
  26. package/dist/modules/catalog/commands/products.js +52 -17
  27. package/dist/modules/catalog/commands/products.js.map +2 -2
  28. package/dist/modules/catalog/search.js +3 -1
  29. package/dist/modules/catalog/search.js.map +2 -2
  30. package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js +2 -1
  31. package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js.map +2 -2
  32. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
  33. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
  34. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
  35. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
  36. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
  37. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
  38. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +10 -7
  39. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +2 -2
  40. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
  41. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  42. package/dist/modules/communication_channels/lib/inbound-message-origin.js +1 -1
  43. package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
  44. package/dist/modules/communication_channels/lib/oauth-result.js +12 -0
  45. package/dist/modules/communication_channels/lib/oauth-result.js.map +7 -0
  46. package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
  47. package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
  48. package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
  49. package/dist/modules/communication_channels/lib/use-connect-channel.js.map +2 -2
  50. package/dist/modules/customers/api/activity-types/route.js +151 -0
  51. package/dist/modules/customers/api/activity-types/route.js.map +7 -0
  52. package/dist/modules/customers/api/deals/[id]/people/route.js +58 -5
  53. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  54. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
  55. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
  56. package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
  57. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  58. package/dist/modules/customers/api/interactions/route.js +3 -0
  59. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  60. package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
  61. package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
  62. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
  63. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
  64. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
  65. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
  66. package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
  67. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  68. package/dist/modules/customers/calendar-event-types.js +548 -0
  69. package/dist/modules/customers/calendar-event-types.js.map +7 -0
  70. package/dist/modules/customers/commands/dictionaries.js +70 -10
  71. package/dist/modules/customers/commands/dictionaries.js.map +2 -2
  72. package/dist/modules/customers/commands/interactions.js +187 -22
  73. package/dist/modules/customers/commands/interactions.js.map +3 -3
  74. package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
  75. package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
  76. package/dist/modules/customers/components/DictionarySettings.js +3 -1
  77. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  78. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
  79. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
  80. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
  81. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  82. package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
  83. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  84. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
  85. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
  86. package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
  87. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
  88. package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
  89. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  90. package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
  91. package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
  92. package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
  93. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
  94. package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
  95. package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
  96. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
  97. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  98. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
  99. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
  100. package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
  101. package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
  102. package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
  103. package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
  104. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
  105. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
  106. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
  107. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
  108. package/dist/modules/customers/components/calendar/types.js +1 -0
  109. package/dist/modules/customers/components/calendar/types.js.map +2 -2
  110. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +78 -343
  111. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  112. package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
  113. package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
  114. package/dist/modules/customers/components/detail/DealForm.js +7 -3
  115. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  116. package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
  117. package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
  118. package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
  119. package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
  120. package/dist/modules/customers/components/detail/PersonCard.js +5 -2
  121. package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
  122. package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
  123. package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
  124. package/dist/modules/customers/data/entities.js +6 -0
  125. package/dist/modules/customers/data/entities.js.map +2 -2
  126. package/dist/modules/customers/data/validators.js +39 -10
  127. package/dist/modules/customers/data/validators.js.map +2 -2
  128. package/dist/modules/customers/di.js +20 -1
  129. package/dist/modules/customers/di.js.map +2 -2
  130. package/dist/modules/customers/extension-points.js +13 -0
  131. package/dist/modules/customers/extension-points.js.map +2 -2
  132. package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
  133. package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
  134. package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
  135. package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
  136. package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
  137. package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
  138. package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
  139. package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
  140. package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
  141. package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
  142. package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
  143. package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
  144. package/dist/modules/customers/lib/calendar/timezone.js +111 -0
  145. package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
  146. package/dist/modules/customers/lib/interactionReadModel.js +1 -0
  147. package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
  148. package/dist/modules/customers/lib/todoCompatibility.js +1 -0
  149. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  150. package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
  151. package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
  152. package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
  153. package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
  154. package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
  155. package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
  156. package/dist/modules/messages/subscribers/message-notification.js +13 -1
  157. package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
  158. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
  159. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  160. package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
  161. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  162. package/dist/modules/planner/lib/availabilityMerge.js +101 -38
  163. package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
  164. package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
  165. package/dist/modules/sales/components/documents/LineItemDialog.js +159 -81
  166. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  167. package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
  168. package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
  169. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
  170. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  171. package/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
  172. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  173. package/generated/entities/customer_dictionary_entry/index.ts +1 -0
  174. package/generated/entities/customer_interaction/index.ts +1 -0
  175. package/generated/entity-fields-registry.ts +2 -0
  176. package/package.json +9 -8
  177. package/src/modules/attachments/api/route.ts +36 -3
  178. package/src/modules/attachments/lib/ocrLimits.ts +62 -0
  179. package/src/modules/attachments/lib/ocrQueue.ts +95 -2
  180. package/src/modules/attachments/lib/ocrService.ts +4 -0
  181. package/src/modules/attachments/lib/pdfProcessing.ts +14 -2
  182. package/src/modules/attachments/lib/scoped-upload-service.ts +4 -2
  183. package/src/modules/attachments/lib/textExtraction.ts +14 -1
  184. package/src/modules/auth/i18n/de.json +3 -0
  185. package/src/modules/auth/i18n/en.json +3 -0
  186. package/src/modules/auth/i18n/es.json +3 -0
  187. package/src/modules/auth/i18n/ko.json +3 -0
  188. package/src/modules/auth/i18n/pl.json +3 -0
  189. package/src/modules/catalog/commands/categories.ts +1 -1
  190. package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
  191. package/src/modules/catalog/commands/products.ts +58 -17
  192. package/src/modules/catalog/search.ts +5 -1
  193. package/src/modules/communication_channels/api/get/oauth/[provider]/callback/route.ts +2 -1
  194. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
  195. package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
  196. package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
  197. package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +12 -9
  198. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
  199. package/src/modules/communication_channels/i18n/de.json +4 -1
  200. package/src/modules/communication_channels/i18n/en.json +4 -1
  201. package/src/modules/communication_channels/i18n/es.json +4 -1
  202. package/src/modules/communication_channels/i18n/ko.json +4 -1
  203. package/src/modules/communication_channels/i18n/pl.json +4 -1
  204. package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
  205. package/src/modules/communication_channels/lib/oauth-result.ts +13 -0
  206. package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
  207. package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
  208. package/src/modules/customers/api/activity-types/route.ts +154 -0
  209. package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
  210. package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
  211. package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
  212. package/src/modules/customers/api/interactions/route.ts +4 -0
  213. package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
  214. package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
  215. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
  216. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
  217. package/src/modules/customers/calendar-event-types.ts +674 -0
  218. package/src/modules/customers/commands/dictionaries.ts +84 -8
  219. package/src/modules/customers/commands/interactions.ts +244 -21
  220. package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
  221. package/src/modules/customers/components/DictionarySettings.tsx +5 -3
  222. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
  223. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
  224. package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
  225. package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
  226. package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
  227. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
  228. package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
  229. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
  230. package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
  231. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
  232. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
  233. package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
  234. package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
  235. package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
  236. package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
  237. package/src/modules/customers/components/calendar/types.ts +1 -0
  238. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +94 -388
  239. package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
  240. package/src/modules/customers/components/detail/DealForm.tsx +7 -3
  241. package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
  242. package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
  243. package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
  244. package/src/modules/customers/components/detail/types.ts +1 -0
  245. package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -0
  246. package/src/modules/customers/data/entities.ts +8 -1
  247. package/src/modules/customers/data/validators.ts +42 -7
  248. package/src/modules/customers/di.ts +21 -0
  249. package/src/modules/customers/extension-points.ts +13 -0
  250. package/src/modules/customers/i18n/de.json +104 -8
  251. package/src/modules/customers/i18n/en.json +104 -8
  252. package/src/modules/customers/i18n/es.json +104 -8
  253. package/src/modules/customers/i18n/ko.json +104 -8
  254. package/src/modules/customers/i18n/pl.json +104 -8
  255. package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
  256. package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
  257. package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
  258. package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
  259. package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
  260. package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
  261. package/src/modules/customers/lib/calendar/timezone.ts +117 -0
  262. package/src/modules/customers/lib/interactionReadModel.ts +1 -0
  263. package/src/modules/customers/lib/todoCompatibility.ts +2 -0
  264. package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
  265. package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
  266. package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
  267. package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
  268. package/src/modules/messages/subscribers/message-notification.ts +13 -3
  269. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
  270. package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
  271. package/src/modules/planner/lib/availabilityMerge.ts +106 -41
  272. package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
  273. package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
  274. package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
  275. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
  276. package/src/modules/sync_excel/lib/adapters/customers.ts +33 -27
@@ -0,0 +1,557 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { loadCustomFieldSnapshot } from '@open-mercato/shared/lib/commands/customFieldSnapshots'
3
+ import { setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'
4
+ import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
5
+ import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
6
+ import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
7
+ import { E } from '#generated/entities.ids.generated'
8
+ import {
9
+ CatalogOffer,
10
+ CatalogOptionSchemaTemplate,
11
+ CatalogPriceKind,
12
+ CatalogProduct,
13
+ CatalogProductPrice,
14
+ CatalogProductUnitConversion,
15
+ CatalogProductVariant,
16
+ } from '../data/entities'
17
+ import type { CatalogGtinType, CatalogProductOptionSchema } from '../data/types'
18
+ import { cloneJson, emitCatalogQueryIndexEvent } from './shared'
19
+
20
+ type VariantDimensions = NonNullable<CatalogProductVariant['dimensions']>
21
+
22
+ export type ProductDeleteVariantSnapshot = {
23
+ id: string
24
+ productId: string
25
+ name: string | null
26
+ sku: string | null
27
+ barcode: string | null
28
+ gtinType: CatalogGtinType | null
29
+ hsCode: string | null
30
+ statusEntryId: string | null
31
+ isDefault: boolean
32
+ isActive: boolean
33
+ weightValue: string | null
34
+ weightUnit: string | null
35
+ dimensions: VariantDimensions | null
36
+ metadata: Record<string, unknown> | null
37
+ taxRateId: string | null
38
+ taxRate: string | null
39
+ optionValues: Record<string, string> | null
40
+ defaultMediaId: string | null
41
+ defaultMediaUrl: string | null
42
+ customFieldsetCode: string | null
43
+ createdAt: string
44
+ updatedAt: string
45
+ deletedAt: string | null
46
+ custom: Record<string, unknown> | null
47
+ }
48
+
49
+ export type ProductDeletePriceSnapshot = {
50
+ id: string
51
+ variantId: string | null
52
+ productId: string | null
53
+ offerId: string | null
54
+ priceKindId: string
55
+ currencyCode: string
56
+ kind: string
57
+ minQuantity: number
58
+ maxQuantity: number | null
59
+ unitPriceNet: string | null
60
+ unitPriceGross: string | null
61
+ taxRate: string | null
62
+ taxAmount: string | null
63
+ channelId: string | null
64
+ userId: string | null
65
+ userGroupId: string | null
66
+ customerId: string | null
67
+ customerGroupId: string | null
68
+ metadata: Record<string, unknown> | null
69
+ startsAt: string | null
70
+ endsAt: string | null
71
+ createdAt: string
72
+ updatedAt: string
73
+ }
74
+
75
+ export type ProductDeleteUnitConversionSnapshot = {
76
+ id: string
77
+ productId: string
78
+ unitCode: string
79
+ toBaseFactor: string
80
+ sortOrder: number
81
+ isActive: boolean
82
+ metadata: Record<string, unknown> | null
83
+ createdAt: string
84
+ updatedAt: string
85
+ deletedAt: string | null
86
+ }
87
+
88
+ export type ProductDeleteOptionSchemaSnapshot = {
89
+ id: string
90
+ name: string
91
+ code: string
92
+ description: string | null
93
+ schema: CatalogProductOptionSchema
94
+ metadata: Record<string, unknown> | null
95
+ isActive: boolean
96
+ createdAt: string
97
+ updatedAt: string
98
+ deletedAt: string | null
99
+ }
100
+
101
+ export type ProductDeleteChildrenSnapshot = {
102
+ variants: ProductDeleteVariantSnapshot[]
103
+ prices: ProductDeletePriceSnapshot[]
104
+ unitConversions: ProductDeleteUnitConversionSnapshot[]
105
+ optionSchemaTemplate: ProductDeleteOptionSchemaSnapshot | null
106
+ }
107
+
108
+ export type ProductDeleteOwner = {
109
+ id: string
110
+ organizationId: string
111
+ tenantId: string
112
+ optionSchemaId: string | null
113
+ offerIds: string[]
114
+ }
115
+
116
+ export type ProductDeleteChildrenRestorePlan = ProductDeleteChildrenSnapshot
117
+
118
+ type EntityRef = { id: string } | string | null | undefined
119
+
120
+ function refId(ref: EntityRef): string | null {
121
+ if (!ref) return null
122
+ return typeof ref === 'string' ? ref : ref.id
123
+ }
124
+
125
+ function toIso(value: Date | null | undefined): string | null {
126
+ return value ? value.toISOString() : null
127
+ }
128
+
129
+ function toDate(value: string | null | undefined): Date | null {
130
+ return value ? new Date(value) : null
131
+ }
132
+
133
+ export function isProductOwnedOptionSchemaTemplate(
134
+ template: CatalogOptionSchemaTemplate | string | null | undefined,
135
+ ): template is CatalogOptionSchemaTemplate {
136
+ if (!template || typeof template === 'string') return false
137
+ const metadata = template.metadata
138
+ if (!metadata || typeof metadata !== 'object') return false
139
+ const source = (metadata as Record<string, unknown>).source
140
+ return source === 'product'
141
+ }
142
+
143
+ export async function captureProductDeleteChildren(
144
+ em: EntityManager,
145
+ owner: Pick<ProductDeleteOwner, 'id' | 'organizationId' | 'tenantId' | 'optionSchemaId'>,
146
+ ): Promise<ProductDeleteChildrenSnapshot> {
147
+ const scope = { tenantId: owner.tenantId, organizationId: owner.organizationId }
148
+ const variantRecords = await findWithDecryption(em, CatalogProductVariant, { product: owner.id }, {}, scope)
149
+ const variantIds = variantRecords.map((variant) => variant.id)
150
+ const priceRecords = await findWithDecryption(
151
+ em,
152
+ CatalogProductPrice,
153
+ variantIds.length ? { $or: [{ variant: { $in: variantIds } }, { product: owner.id }] } : { product: owner.id },
154
+ {},
155
+ scope,
156
+ )
157
+ const conversionRecords = await findWithDecryption(em, CatalogProductUnitConversion, { product: owner.id }, {}, scope)
158
+ const template = owner.optionSchemaId
159
+ ? await findOneWithDecryption(em, CatalogOptionSchemaTemplate, { id: owner.optionSchemaId }, {}, scope)
160
+ : null
161
+ const variants: ProductDeleteVariantSnapshot[] = []
162
+ for (const variant of variantRecords) {
163
+ const custom = await loadCustomFieldSnapshot(em, {
164
+ entityId: E.catalog.catalog_product_variant,
165
+ recordId: variant.id,
166
+ tenantId: variant.tenantId,
167
+ organizationId: variant.organizationId,
168
+ })
169
+ variants.push({
170
+ id: variant.id,
171
+ productId: refId(variant.product) ?? owner.id,
172
+ name: variant.name ?? null,
173
+ sku: variant.sku ?? null,
174
+ barcode: variant.barcode ?? null,
175
+ gtinType: variant.gtinType ?? null,
176
+ hsCode: variant.hsCode ?? null,
177
+ statusEntryId: variant.statusEntryId ?? null,
178
+ isDefault: variant.isDefault,
179
+ isActive: variant.isActive,
180
+ weightValue: variant.weightValue ?? null,
181
+ weightUnit: variant.weightUnit ?? null,
182
+ dimensions: variant.dimensions ? cloneJson(variant.dimensions) : null,
183
+ metadata: variant.metadata ? cloneJson(variant.metadata) : null,
184
+ taxRateId: variant.taxRateId ?? null,
185
+ taxRate: variant.taxRate ?? null,
186
+ optionValues: variant.optionValues ? cloneJson(variant.optionValues) : null,
187
+ defaultMediaId: variant.defaultMediaId ?? null,
188
+ defaultMediaUrl: variant.defaultMediaUrl ?? null,
189
+ customFieldsetCode: variant.customFieldsetCode ?? null,
190
+ createdAt: variant.createdAt.toISOString(),
191
+ updatedAt: variant.updatedAt.toISOString(),
192
+ deletedAt: toIso(variant.deletedAt),
193
+ custom: Object.keys(custom).length ? custom : null,
194
+ })
195
+ }
196
+ const prices: ProductDeletePriceSnapshot[] = []
197
+ for (const price of priceRecords) {
198
+ const priceKindId = refId(price.priceKind)
199
+ if (!priceKindId) throw new Error(`[internal] Catalog price ${price.id} has no price kind`)
200
+ prices.push({
201
+ id: price.id,
202
+ variantId: refId(price.variant),
203
+ productId: refId(price.product),
204
+ offerId: refId(price.offer),
205
+ priceKindId,
206
+ currencyCode: price.currencyCode,
207
+ kind: price.kind,
208
+ minQuantity: price.minQuantity,
209
+ maxQuantity: price.maxQuantity ?? null,
210
+ unitPriceNet: price.unitPriceNet ?? null,
211
+ unitPriceGross: price.unitPriceGross ?? null,
212
+ taxRate: price.taxRate ?? null,
213
+ taxAmount: price.taxAmount ?? null,
214
+ channelId: price.channelId ?? null,
215
+ userId: price.userId ?? null,
216
+ userGroupId: price.userGroupId ?? null,
217
+ customerId: price.customerId ?? null,
218
+ customerGroupId: price.customerGroupId ?? null,
219
+ metadata: price.metadata ? cloneJson(price.metadata) : null,
220
+ startsAt: toIso(price.startsAt),
221
+ endsAt: toIso(price.endsAt),
222
+ createdAt: price.createdAt.toISOString(),
223
+ updatedAt: price.updatedAt.toISOString(),
224
+ })
225
+ }
226
+ const unitConversions: ProductDeleteUnitConversionSnapshot[] = conversionRecords.map((conversion) => ({
227
+ id: conversion.id,
228
+ productId: refId(conversion.product) ?? owner.id,
229
+ unitCode: conversion.unitCode,
230
+ toBaseFactor: conversion.toBaseFactor,
231
+ sortOrder: conversion.sortOrder,
232
+ isActive: conversion.isActive,
233
+ metadata: conversion.metadata ? cloneJson(conversion.metadata) : null,
234
+ createdAt: conversion.createdAt.toISOString(),
235
+ updatedAt: conversion.updatedAt.toISOString(),
236
+ deletedAt: toIso(conversion.deletedAt),
237
+ }))
238
+ return {
239
+ variants,
240
+ prices,
241
+ unitConversions,
242
+ optionSchemaTemplate: isProductOwnedOptionSchemaTemplate(template)
243
+ ? {
244
+ id: template.id,
245
+ name: template.name,
246
+ code: template.code,
247
+ description: template.description ?? null,
248
+ schema: cloneJson(template.schema),
249
+ metadata: template.metadata ? cloneJson(template.metadata) : null,
250
+ isActive: template.isActive,
251
+ createdAt: template.createdAt.toISOString(),
252
+ updatedAt: template.updatedAt.toISOString(),
253
+ deletedAt: toIso(template.deletedAt),
254
+ }
255
+ : null,
256
+ }
257
+ }
258
+
259
+ function restoreConflict(kind: string, id: string): Error {
260
+ return new Error(`[internal] Cannot restore catalog product ${kind} ${id}: it does not belong to the restored product`)
261
+ }
262
+
263
+ function sameScope(row: { tenantId: string; organizationId: string }, owner: ProductDeleteOwner): boolean {
264
+ return row.tenantId === owner.tenantId && row.organizationId === owner.organizationId
265
+ }
266
+
267
+ function asList<T>(value: T[] | null | undefined): T[] {
268
+ return Array.isArray(value) ? value : []
269
+ }
270
+
271
+ export async function planProductDeleteChildrenRestore(
272
+ em: EntityManager,
273
+ owner: ProductDeleteOwner,
274
+ children: ProductDeleteChildrenSnapshot | null | undefined,
275
+ ): Promise<ProductDeleteChildrenRestorePlan | null> {
276
+ if (!children) return null
277
+ const scope = { tenantId: owner.tenantId, organizationId: owner.organizationId }
278
+ const variantSnapshots = asList(children.variants)
279
+ const priceSnapshots = asList(children.prices)
280
+ const conversionSnapshots = asList(children.unitConversions)
281
+ const templateSnapshot =
282
+ children.optionSchemaTemplate && children.optionSchemaTemplate.id === owner.optionSchemaId
283
+ ? children.optionSchemaTemplate
284
+ : null
285
+ const ownVariantIds = new Set(variantSnapshots.map((variant) => variant.id))
286
+ const offerIds = new Set(owner.offerIds)
287
+
288
+ const priceOwned = (price: { variantId: string | null; productId: string | null }) =>
289
+ (price.variantId !== null && ownVariantIds.has(price.variantId)) || price.productId === owner.id
290
+
291
+ for (const variant of variantSnapshots) {
292
+ if (variant.productId !== owner.id) throw restoreConflict('variant', variant.id)
293
+ }
294
+ for (const conversion of conversionSnapshots) {
295
+ if (conversion.productId !== owner.id) throw restoreConflict('unit conversion', conversion.id)
296
+ }
297
+ for (const price of priceSnapshots) {
298
+ if (!priceOwned(price)) throw restoreConflict('price', price.id)
299
+ }
300
+
301
+ const foreignVariantIds = new Set<string>()
302
+ const foreignProductIds = new Set<string>()
303
+ const trackVariant = (id: string | null) => {
304
+ if (id && !ownVariantIds.has(id)) foreignVariantIds.add(id)
305
+ }
306
+ const trackProduct = (id: string | null) => {
307
+ if (id && id !== owner.id) foreignProductIds.add(id)
308
+ }
309
+ for (const price of priceSnapshots) {
310
+ trackVariant(price.variantId)
311
+ trackProduct(price.productId)
312
+ }
313
+
314
+ const existingVariants = variantSnapshots.length
315
+ ? await findWithDecryption(em, CatalogProductVariant, { id: { $in: variantSnapshots.map((entry) => entry.id) } }, {}, scope)
316
+ : []
317
+ const existingPrices = priceSnapshots.length
318
+ ? await findWithDecryption(em, CatalogProductPrice, { id: { $in: priceSnapshots.map((entry) => entry.id) } }, {}, scope)
319
+ : []
320
+ const existingConversions = conversionSnapshots.length
321
+ ? await findWithDecryption(
322
+ em,
323
+ CatalogProductUnitConversion,
324
+ { id: { $in: conversionSnapshots.map((entry) => entry.id) } },
325
+ {},
326
+ scope,
327
+ )
328
+ : []
329
+ const existingTemplate = templateSnapshot
330
+ ? await findOneWithDecryption(em, CatalogOptionSchemaTemplate, { id: templateSnapshot.id }, {}, scope)
331
+ : null
332
+ const foreignVariants = foreignVariantIds.size
333
+ ? await findWithDecryption(
334
+ em,
335
+ CatalogProductVariant,
336
+ { id: { $in: Array.from(foreignVariantIds) }, tenantId: owner.tenantId, organizationId: owner.organizationId },
337
+ {},
338
+ scope,
339
+ )
340
+ : []
341
+ const foreignProducts = foreignProductIds.size
342
+ ? await findWithDecryption(
343
+ em,
344
+ CatalogProduct,
345
+ { id: { $in: Array.from(foreignProductIds) }, tenantId: owner.tenantId, organizationId: owner.organizationId },
346
+ {},
347
+ scope,
348
+ )
349
+ : []
350
+
351
+ for (const variant of existingVariants) {
352
+ if (!sameScope(variant, owner) || refId(variant.product) !== owner.id) throw restoreConflict('variant', variant.id)
353
+ }
354
+ for (const conversion of existingConversions) {
355
+ if (!sameScope(conversion, owner) || refId(conversion.product) !== owner.id) {
356
+ throw restoreConflict('unit conversion', conversion.id)
357
+ }
358
+ }
359
+ for (const price of existingPrices) {
360
+ if (!sameScope(price, owner) || !priceOwned({ variantId: refId(price.variant), productId: refId(price.product) })) {
361
+ throw restoreConflict('price', price.id)
362
+ }
363
+ }
364
+ if (existingTemplate && !sameScope(existingTemplate, owner)) {
365
+ throw restoreConflict('option schema', existingTemplate.id)
366
+ }
367
+
368
+ const existingVariantIds = new Set(existingVariants.map((entry) => entry.id))
369
+ const existingPriceIds = new Set(existingPrices.map((entry) => entry.id))
370
+ const existingConversionIds = new Set(existingConversions.map((entry) => entry.id))
371
+ const availableForeignVariantIds = new Set(foreignVariants.map((entry) => entry.id))
372
+ const availableForeignProductIds = new Set(foreignProducts.map((entry) => entry.id))
373
+ const variantAvailable = (id: string | null) =>
374
+ id === null || ownVariantIds.has(id) || availableForeignVariantIds.has(id)
375
+ const productAvailable = (id: string | null) =>
376
+ id === null || id === owner.id || availableForeignProductIds.has(id)
377
+
378
+ return {
379
+ optionSchemaTemplate: templateSnapshot && !existingTemplate ? templateSnapshot : null,
380
+ unitConversions: conversionSnapshots.filter((entry) => !existingConversionIds.has(entry.id)),
381
+ variants: variantSnapshots.filter((entry) => !existingVariantIds.has(entry.id)),
382
+ prices: priceSnapshots
383
+ .filter(
384
+ (entry) =>
385
+ !existingPriceIds.has(entry.id) && variantAvailable(entry.variantId) && productAvailable(entry.productId),
386
+ )
387
+ .map((entry) => ({ ...entry, offerId: entry.offerId && offerIds.has(entry.offerId) ? entry.offerId : null })),
388
+ }
389
+ }
390
+
391
+ export function restoreProductOptionSchemaTemplate(
392
+ em: EntityManager,
393
+ owner: ProductDeleteOwner,
394
+ plan: ProductDeleteChildrenRestorePlan | null,
395
+ ): void {
396
+ const snapshot = plan?.optionSchemaTemplate
397
+ if (!snapshot) return
398
+ const template = em.create(CatalogOptionSchemaTemplate, {
399
+ id: snapshot.id,
400
+ organizationId: owner.organizationId,
401
+ tenantId: owner.tenantId,
402
+ name: snapshot.name,
403
+ code: snapshot.code,
404
+ description: snapshot.description ?? null,
405
+ schema: cloneJson(snapshot.schema),
406
+ metadata: snapshot.metadata ? cloneJson(snapshot.metadata) : null,
407
+ isActive: snapshot.isActive,
408
+ createdAt: new Date(snapshot.createdAt),
409
+ updatedAt: new Date(snapshot.updatedAt),
410
+ deletedAt: toDate(snapshot.deletedAt),
411
+ })
412
+ em.persist(template)
413
+ }
414
+
415
+ /**
416
+ * Child rows are created on a fork that shares the undo transaction instead of on the
417
+ * EntityManager holding the restored product: the tenant encryption subscriber walks the
418
+ * loaded graph of every created entity, which is quadratic once a product has hundreds of
419
+ * prices attached to already-managed variants.
420
+ */
421
+ export function buildProductDeleteChildrenRestorePhases(
422
+ em: EntityManager,
423
+ owner: ProductDeleteOwner,
424
+ plan: ProductDeleteChildrenRestorePlan | null,
425
+ ): Array<() => Promise<void>> {
426
+ if (!plan) return []
427
+ const scope = { organizationId: owner.organizationId, tenantId: owner.tenantId }
428
+ let childEm: EntityManager | null = null
429
+ const resolveChildEm = (): EntityManager => {
430
+ childEm ??= em.fork({ keepTransactionContext: true })
431
+ return childEm
432
+ }
433
+ return [
434
+ async () => {
435
+ const target = resolveChildEm()
436
+ for (const snapshot of plan.unitConversions) {
437
+ target.persist(
438
+ target.create(CatalogProductUnitConversion, {
439
+ id: snapshot.id,
440
+ ...scope,
441
+ product: target.getReference(CatalogProduct, snapshot.productId),
442
+ unitCode: snapshot.unitCode,
443
+ toBaseFactor: snapshot.toBaseFactor,
444
+ sortOrder: snapshot.sortOrder,
445
+ isActive: snapshot.isActive,
446
+ metadata: snapshot.metadata ? cloneJson(snapshot.metadata) : null,
447
+ createdAt: new Date(snapshot.createdAt),
448
+ updatedAt: new Date(snapshot.updatedAt),
449
+ deletedAt: toDate(snapshot.deletedAt),
450
+ }),
451
+ )
452
+ }
453
+ await target.flush()
454
+ },
455
+ async () => {
456
+ const target = resolveChildEm()
457
+ for (const snapshot of plan.variants) {
458
+ target.persist(
459
+ target.create(CatalogProductVariant, {
460
+ id: snapshot.id,
461
+ ...scope,
462
+ product: target.getReference(CatalogProduct, snapshot.productId),
463
+ name: snapshot.name ?? null,
464
+ sku: snapshot.sku ?? null,
465
+ barcode: snapshot.barcode ?? null,
466
+ gtinType: snapshot.gtinType ?? null,
467
+ hsCode: snapshot.hsCode ?? null,
468
+ statusEntryId: snapshot.statusEntryId ?? null,
469
+ isDefault: snapshot.isDefault,
470
+ isActive: snapshot.isActive,
471
+ weightValue: snapshot.weightValue ?? null,
472
+ weightUnit: snapshot.weightUnit ?? null,
473
+ dimensions: snapshot.dimensions ? cloneJson(snapshot.dimensions) : null,
474
+ metadata: snapshot.metadata ? cloneJson(snapshot.metadata) : null,
475
+ taxRateId: snapshot.taxRateId ?? null,
476
+ taxRate: snapshot.taxRate ?? null,
477
+ optionValues: snapshot.optionValues ? cloneJson(snapshot.optionValues) : null,
478
+ defaultMediaId: snapshot.defaultMediaId ?? null,
479
+ defaultMediaUrl: snapshot.defaultMediaUrl ?? null,
480
+ customFieldsetCode: snapshot.customFieldsetCode ?? null,
481
+ createdAt: new Date(snapshot.createdAt),
482
+ updatedAt: new Date(snapshot.updatedAt),
483
+ deletedAt: toDate(snapshot.deletedAt),
484
+ }),
485
+ )
486
+ }
487
+ await target.flush()
488
+ },
489
+ async () => {
490
+ const target = resolveChildEm()
491
+ for (const snapshot of plan.prices) {
492
+ target.persist(
493
+ target.create(CatalogProductPrice, {
494
+ id: snapshot.id,
495
+ ...scope,
496
+ variant: snapshot.variantId ? target.getReference(CatalogProductVariant, snapshot.variantId) : null,
497
+ product: snapshot.productId ? target.getReference(CatalogProduct, snapshot.productId) : null,
498
+ offer: snapshot.offerId ? target.getReference(CatalogOffer, snapshot.offerId) : null,
499
+ priceKind: target.getReference(CatalogPriceKind, snapshot.priceKindId),
500
+ currencyCode: snapshot.currencyCode,
501
+ kind: snapshot.kind,
502
+ minQuantity: snapshot.minQuantity,
503
+ maxQuantity: snapshot.maxQuantity ?? null,
504
+ unitPriceNet: snapshot.unitPriceNet ?? null,
505
+ unitPriceGross: snapshot.unitPriceGross ?? null,
506
+ taxRate: snapshot.taxRate ?? null,
507
+ taxAmount: snapshot.taxAmount ?? null,
508
+ channelId: snapshot.channelId ?? null,
509
+ userId: snapshot.userId ?? null,
510
+ userGroupId: snapshot.userGroupId ?? null,
511
+ customerId: snapshot.customerId ?? null,
512
+ customerGroupId: snapshot.customerGroupId ?? null,
513
+ metadata: snapshot.metadata ? cloneJson(snapshot.metadata) : null,
514
+ startsAt: toDate(snapshot.startsAt),
515
+ endsAt: toDate(snapshot.endsAt),
516
+ createdAt: new Date(snapshot.createdAt),
517
+ updatedAt: new Date(snapshot.updatedAt),
518
+ }),
519
+ )
520
+ }
521
+ await target.flush()
522
+ },
523
+ ]
524
+ }
525
+
526
+ export async function emitProductDeleteChildrenRestoreSideEffects(opts: {
527
+ ctx: CommandRuntimeContext
528
+ dataEngine: DataEngine
529
+ owner: ProductDeleteOwner
530
+ children: ProductDeleteChildrenSnapshot | null | undefined
531
+ plan: ProductDeleteChildrenRestorePlan | null
532
+ }): Promise<void> {
533
+ const { ctx, dataEngine, owner, children, plan } = opts
534
+ const variants = asList(children?.variants)
535
+ const createdVariantIds = new Set(asList(plan?.variants).map((variant) => variant.id))
536
+ for (const variant of variants) {
537
+ if (!variant.custom || !Object.keys(variant.custom).length) continue
538
+ await setCustomFieldsIfAny({
539
+ dataEngine,
540
+ entityId: E.catalog.catalog_product_variant,
541
+ recordId: variant.id,
542
+ organizationId: owner.organizationId,
543
+ tenantId: owner.tenantId,
544
+ values: variant.custom,
545
+ })
546
+ }
547
+ for (const variant of variants) {
548
+ await emitCatalogQueryIndexEvent(ctx, {
549
+ entityType: E.catalog.catalog_product_variant,
550
+ recordId: variant.id,
551
+ organizationId: owner.organizationId,
552
+ tenantId: owner.tenantId,
553
+ action: 'created',
554
+ ...(createdVariantIds.has(variant.id) ? {} : { coverageBaseDelta: 0 }),
555
+ })
556
+ }
557
+ }
@@ -76,6 +76,16 @@ import {
76
76
  findWithDecryption,
77
77
  findOneWithDecryption,
78
78
  } from "@open-mercato/shared/lib/encryption/find";
79
+ import {
80
+ buildProductDeleteChildrenRestorePhases,
81
+ captureProductDeleteChildren,
82
+ emitProductDeleteChildrenRestoreSideEffects,
83
+ isProductOwnedOptionSchemaTemplate,
84
+ planProductDeleteChildrenRestore,
85
+ restoreProductOptionSchemaTemplate,
86
+ type ProductDeleteChildrenSnapshot,
87
+ type ProductDeleteOwner,
88
+ } from "./productDeleteChildren";
79
89
  import { canonicalizeUnitCode } from "../lib/unitCodes";
80
90
  import {
81
91
  resolveCanonicalUnitCode,
@@ -255,8 +265,22 @@ function resolveUnitPriceInput(
255
265
  type ProductUndoPayload = {
256
266
  before?: ProductSnapshot | null;
257
267
  after?: ProductSnapshot | null;
268
+ children?: ProductDeleteChildrenSnapshot | null;
269
+ };
270
+
271
+ type ProductDeleteSnapshots = {
272
+ before?: ProductSnapshot;
273
+ children?: ProductDeleteChildrenSnapshot;
258
274
  };
259
275
 
276
+ const PRODUCT_DELETE_CACHE_ALIASES: readonly string[] = [
277
+ "catalog.product",
278
+ "catalog.variant",
279
+ "catalog.price",
280
+ "catalog.product.unit.conversion",
281
+ "catalog.optionschema",
282
+ ];
283
+
260
284
  const productCrudEvents: CrudEventsConfig<CatalogProduct> = {
261
285
  module: "catalog",
262
286
  entity: "product",
@@ -1055,16 +1079,6 @@ async function emitProductVariantCleanupSideEffects(opts: {
1055
1079
  }
1056
1080
  }
1057
1081
 
1058
- function isProductOwnedOptionSchemaTemplate(
1059
- template: CatalogOptionSchemaTemplate | string | null | undefined,
1060
- ): template is CatalogOptionSchemaTemplate {
1061
- if (!template || typeof template === "string") return false;
1062
- const metadata = template.metadata;
1063
- if (!metadata || typeof metadata !== "object") return false;
1064
- const source = (metadata as Record<string, unknown>).source;
1065
- return source === "product";
1066
- }
1067
-
1068
1082
  async function resolveOptionSchemaTemplateForRemoval(
1069
1083
  em: EntityManager,
1070
1084
  product: CatalogProduct,
@@ -2200,11 +2214,14 @@ const deleteProductCommand: CommandHandler<
2200
2214
  const id = requireId(input, "Product id is required");
2201
2215
  const em = ctx.container.resolve("em") as EntityManager;
2202
2216
  const snapshot = await loadProductSnapshot(em, id);
2203
- if (snapshot) {
2204
- ensureTenantScope(ctx, snapshot.tenantId);
2205
- ensureOrganizationScope(ctx, snapshot.organizationId);
2206
- }
2207
- return snapshot ? { before: snapshot } : {};
2217
+ if (!snapshot) return {};
2218
+ ensureTenantScope(ctx, snapshot.tenantId);
2219
+ ensureOrganizationScope(ctx, snapshot.organizationId);
2220
+ const snapshots: ProductDeleteSnapshots = {
2221
+ before: snapshot,
2222
+ children: await captureProductDeleteChildren(em.fork(), snapshot),
2223
+ };
2224
+ return snapshots;
2208
2225
  },
2209
2226
  async execute(input, ctx) {
2210
2227
  const id = requireId(input, "Product id is required");
@@ -2298,16 +2315,34 @@ const deleteProductCommand: CommandHandler<
2298
2315
  payload: {
2299
2316
  undo: {
2300
2317
  before,
2318
+ children: (snapshots as ProductDeleteSnapshots).children ?? null,
2301
2319
  } satisfies ProductUndoPayload,
2302
2320
  },
2321
+ context: { cacheAliases: [...PRODUCT_DELETE_CACHE_ALIASES] },
2303
2322
  };
2304
2323
  },
2305
2324
  undo: async ({ logEntry, ctx }) => {
2306
2325
  const payload = extractUndoPayload<ProductUndoPayload>(logEntry);
2307
2326
  const before = payload?.before;
2308
2327
  if (!before) return;
2328
+ ensureTenantScope(ctx, before.tenantId);
2329
+ ensureOrganizationScope(ctx, before.organizationId);
2309
2330
  const em = (ctx.container.resolve("em") as EntityManager).fork();
2331
+ const children = payload?.children ?? null;
2332
+ const owner: ProductDeleteOwner = {
2333
+ id: before.id,
2334
+ organizationId: before.organizationId,
2335
+ tenantId: before.tenantId,
2336
+ optionSchemaId: before.optionSchemaId ?? null,
2337
+ offerIds: (before.offers ?? []).map((offer) => offer.id),
2338
+ };
2310
2339
  let record = await findOneWithDecryption(em, CatalogProduct, { id: before.id });
2340
+ const childrenPlan = await planProductDeleteChildrenRestore(
2341
+ em,
2342
+ owner,
2343
+ children,
2344
+ );
2345
+ restoreProductOptionSchemaTemplate(em, owner, childrenPlan);
2311
2346
  if (!record) {
2312
2347
  record = em.create(CatalogProduct, {
2313
2348
  id: before.id,
@@ -2346,8 +2381,6 @@ const deleteProductCommand: CommandHandler<
2346
2381
  });
2347
2382
  em.persist(record);
2348
2383
  }
2349
- ensureTenantScope(ctx, before.tenantId);
2350
- ensureOrganizationScope(ctx, before.organizationId);
2351
2384
  applyProductSnapshot(em, record, before);
2352
2385
  await withAtomicFlush(
2353
2386
  em,
@@ -2356,6 +2389,7 @@ const deleteProductCommand: CommandHandler<
2356
2389
  () => restoreOffersFromSnapshot(em, record, before.offers),
2357
2390
  () => syncCategoryAssignments(em, record, before.categoryIds),
2358
2391
  () => syncProductTags(em, record, before.tags),
2392
+ ...buildProductDeleteChildrenRestorePhases(em, owner, childrenPlan),
2359
2393
  ],
2360
2394
  { transaction: true },
2361
2395
  );
@@ -2370,6 +2404,13 @@ const deleteProductCommand: CommandHandler<
2370
2404
  values: before.custom,
2371
2405
  });
2372
2406
  }
2407
+ await emitProductDeleteChildrenRestoreSideEffects({
2408
+ ctx,
2409
+ dataEngine,
2410
+ owner,
2411
+ children,
2412
+ plan: childrenPlan,
2413
+ });
2373
2414
  await emitProductCrudUndoChange({
2374
2415
  dataEngine,
2375
2416
  action: "created",