@open-mercato/core 0.9.0 → 0.9.1-develop.7459.1.99e3accd30

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 (243) 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/catalog/commands/categories.js +1 -1
  9. package/dist/modules/catalog/commands/categories.js.map +2 -2
  10. package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
  11. package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
  12. package/dist/modules/catalog/commands/products.js +52 -17
  13. package/dist/modules/catalog/commands/products.js.map +2 -2
  14. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
  15. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
  16. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
  17. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
  18. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
  19. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
  20. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
  21. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  22. package/dist/modules/communication_channels/lib/inbound-message-origin.js +1 -1
  23. package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
  24. package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
  25. package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
  26. package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
  27. package/dist/modules/communication_channels/lib/use-connect-channel.js.map +2 -2
  28. package/dist/modules/customers/api/activity-types/route.js +151 -0
  29. package/dist/modules/customers/api/activity-types/route.js.map +7 -0
  30. package/dist/modules/customers/api/deals/[id]/people/route.js +58 -5
  31. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  32. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
  33. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
  34. package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
  35. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  36. package/dist/modules/customers/api/interactions/route.js +3 -0
  37. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  38. package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
  39. package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
  40. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
  41. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
  42. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
  43. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
  44. package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
  45. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  46. package/dist/modules/customers/calendar-event-types.js +548 -0
  47. package/dist/modules/customers/calendar-event-types.js.map +7 -0
  48. package/dist/modules/customers/commands/dictionaries.js +70 -10
  49. package/dist/modules/customers/commands/dictionaries.js.map +2 -2
  50. package/dist/modules/customers/commands/interactions.js +187 -22
  51. package/dist/modules/customers/commands/interactions.js.map +3 -3
  52. package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
  53. package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
  54. package/dist/modules/customers/components/DictionarySettings.js +3 -1
  55. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  56. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
  57. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
  58. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
  59. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  60. package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
  61. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  62. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
  63. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
  64. package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
  65. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
  66. package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
  67. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  68. package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
  69. package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
  70. package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
  71. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
  72. package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
  73. package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
  74. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
  75. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  76. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
  77. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
  78. package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
  79. package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
  80. package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
  81. package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
  82. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
  83. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
  84. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
  85. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
  86. package/dist/modules/customers/components/calendar/types.js +1 -0
  87. package/dist/modules/customers/components/calendar/types.js.map +2 -2
  88. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +78 -343
  89. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  90. package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
  91. package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
  92. package/dist/modules/customers/components/detail/DealForm.js +7 -3
  93. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  94. package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
  95. package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
  96. package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
  97. package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
  98. package/dist/modules/customers/components/detail/PersonCard.js +5 -2
  99. package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
  100. package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
  101. package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
  102. package/dist/modules/customers/data/entities.js +6 -0
  103. package/dist/modules/customers/data/entities.js.map +2 -2
  104. package/dist/modules/customers/data/validators.js +39 -10
  105. package/dist/modules/customers/data/validators.js.map +2 -2
  106. package/dist/modules/customers/di.js +20 -1
  107. package/dist/modules/customers/di.js.map +2 -2
  108. package/dist/modules/customers/extension-points.js +13 -0
  109. package/dist/modules/customers/extension-points.js.map +2 -2
  110. package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
  111. package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
  112. package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
  113. package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
  114. package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
  115. package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
  116. package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
  117. package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
  118. package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
  119. package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
  120. package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
  121. package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
  122. package/dist/modules/customers/lib/calendar/timezone.js +111 -0
  123. package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
  124. package/dist/modules/customers/lib/interactionReadModel.js +1 -0
  125. package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
  126. package/dist/modules/customers/lib/todoCompatibility.js +1 -0
  127. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  128. package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
  129. package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
  130. package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
  131. package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
  132. package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
  133. package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
  134. package/dist/modules/messages/subscribers/message-notification.js +13 -1
  135. package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
  136. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
  137. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  138. package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
  139. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  140. package/dist/modules/planner/lib/availabilityMerge.js +101 -38
  141. package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
  142. package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
  143. package/dist/modules/sales/components/documents/LineItemDialog.js +159 -81
  144. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  145. package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
  146. package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
  147. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
  148. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  149. package/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
  150. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  151. package/generated/entities/customer_dictionary_entry/index.ts +1 -0
  152. package/generated/entities/customer_interaction/index.ts +1 -0
  153. package/generated/entity-fields-registry.ts +2 -0
  154. package/package.json +9 -8
  155. package/src/modules/auth/i18n/de.json +3 -0
  156. package/src/modules/auth/i18n/en.json +3 -0
  157. package/src/modules/auth/i18n/es.json +3 -0
  158. package/src/modules/auth/i18n/ko.json +3 -0
  159. package/src/modules/auth/i18n/pl.json +3 -0
  160. package/src/modules/catalog/commands/categories.ts +1 -1
  161. package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
  162. package/src/modules/catalog/commands/products.ts +58 -17
  163. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
  164. package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
  165. package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
  166. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
  167. package/src/modules/communication_channels/i18n/de.json +4 -0
  168. package/src/modules/communication_channels/i18n/en.json +4 -0
  169. package/src/modules/communication_channels/i18n/es.json +4 -0
  170. package/src/modules/communication_channels/i18n/ko.json +4 -0
  171. package/src/modules/communication_channels/i18n/pl.json +4 -0
  172. package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
  173. package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
  174. package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
  175. package/src/modules/customers/api/activity-types/route.ts +154 -0
  176. package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
  177. package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
  178. package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
  179. package/src/modules/customers/api/interactions/route.ts +4 -0
  180. package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
  181. package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
  182. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
  183. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
  184. package/src/modules/customers/calendar-event-types.ts +674 -0
  185. package/src/modules/customers/commands/dictionaries.ts +84 -8
  186. package/src/modules/customers/commands/interactions.ts +244 -21
  187. package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
  188. package/src/modules/customers/components/DictionarySettings.tsx +5 -3
  189. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
  190. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
  191. package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
  192. package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
  193. package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
  194. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
  195. package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
  196. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
  197. package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
  198. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
  199. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
  200. package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
  201. package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
  202. package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
  203. package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
  204. package/src/modules/customers/components/calendar/types.ts +1 -0
  205. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +94 -388
  206. package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
  207. package/src/modules/customers/components/detail/DealForm.tsx +7 -3
  208. package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
  209. package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
  210. package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
  211. package/src/modules/customers/components/detail/types.ts +1 -0
  212. package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -0
  213. package/src/modules/customers/data/entities.ts +8 -1
  214. package/src/modules/customers/data/validators.ts +42 -7
  215. package/src/modules/customers/di.ts +21 -0
  216. package/src/modules/customers/extension-points.ts +13 -0
  217. package/src/modules/customers/i18n/de.json +104 -8
  218. package/src/modules/customers/i18n/en.json +104 -8
  219. package/src/modules/customers/i18n/es.json +104 -8
  220. package/src/modules/customers/i18n/ko.json +104 -8
  221. package/src/modules/customers/i18n/pl.json +104 -8
  222. package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
  223. package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
  224. package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
  225. package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
  226. package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
  227. package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
  228. package/src/modules/customers/lib/calendar/timezone.ts +117 -0
  229. package/src/modules/customers/lib/interactionReadModel.ts +1 -0
  230. package/src/modules/customers/lib/todoCompatibility.ts +2 -0
  231. package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
  232. package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
  233. package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
  234. package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
  235. package/src/modules/messages/subscribers/message-notification.ts +13 -3
  236. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
  237. package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
  238. package/src/modules/planner/lib/availabilityMerge.ts +106 -41
  239. package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
  240. package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
  241. package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
  242. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
  243. package/src/modules/sync_excel/lib/adapters/customers.ts +33 -27
@@ -50,6 +50,12 @@ import {
50
50
  } from '../lib/interactionStatus'
51
51
  import { canChangeEmailVisibility } from '../lib/visibilityFilter'
52
52
  import { createLogger } from '@open-mercato/shared/lib/logger'
53
+ import { loadCustomFieldDefinitionIndex } from '@open-mercato/shared/lib/crud/custom-fields'
54
+ import type { OrganizationHierarchyService } from '@open-mercato/shared/lib/auth/principal-service'
55
+ import { resolveCatalogEventType, resolveScopedCalendarEventTypes } from '../lib/calendar/eventTypeResolver'
56
+ import { clearInapplicableCoreFields, findInapplicableCoreFields, omitHiddenCoreClears, preserveHiddenCoreValuesOnSameTypeEdit } from '../lib/calendar/interactionApplicability'
57
+ import { calendarEventTypes, type CalendarEventTypeBehavior } from '../calendar-event-types'
58
+ import { canonicalCalendarTimezone } from '../lib/calendar/timezone'
53
59
 
54
60
  const logger = createLogger('customers')
55
61
 
@@ -110,6 +116,7 @@ type InteractionSnapshot = {
110
116
  appearanceColor: string | null
111
117
  source: string | null
112
118
  durationMinutes: number | null
119
+ timezone?: string | null
113
120
  location: string | null
114
121
  allDay: boolean | null
115
122
  recurrenceRule: string | null
@@ -166,6 +173,7 @@ async function loadInteractionSnapshot(em: EntityManager, id: string): Promise<I
166
173
  appearanceColor: interaction.appearanceColor ?? null,
167
174
  source: interaction.source ?? null,
168
175
  durationMinutes: interaction.durationMinutes ?? null,
176
+ timezone: interaction.timezone ?? null,
169
177
  location: interaction.location ?? null,
170
178
  allDay: interaction.allDay ?? null,
171
179
  recurrenceRule: interaction.recurrenceRule ?? null,
@@ -199,6 +207,88 @@ async function setInteractionCustomFields(
199
207
  })
200
208
  }
201
209
 
210
+ function requestsSelectableTypeEnforcement(input: unknown): boolean {
211
+ return input !== null && typeof input === 'object'
212
+ && 'enforceSelectableType' in input && input.enforceSelectableType === true
213
+ }
214
+
215
+ async function resolveInteractionTypeBehavior(
216
+ ctx: CommandRuntimeContext,
217
+ em: EntityManager,
218
+ tenantId: string,
219
+ organizationId: string,
220
+ key: string,
221
+ unchangedType: boolean,
222
+ enforceSelectableType: boolean,
223
+ ): Promise<{ behavior: CalendarEventTypeBehavior; organizationIds: string[] }> {
224
+ const hierarchy = ctx.container.resolve('organizationHierarchyService') as OrganizationHierarchyService
225
+ const ancestors = await hierarchy.resolveAncestorIds({ tenantId, organizationId })
226
+ if (ancestors === null) throw notFound('Organization not found')
227
+ const organizationIds = [organizationId, ...ancestors.slice().reverse()]
228
+ let catalog
229
+ try {
230
+ catalog = await resolveScopedCalendarEventTypes({
231
+ em,
232
+ tenantId,
233
+ organizationId,
234
+ readableOrganizationIds: organizationIds,
235
+ })
236
+ } catch {
237
+ const { translate } = await resolveTranslations()
238
+ throw new CrudHttpError(503, {
239
+ error: translate('customers.calendar.activityTypes.errors.loadFailed', 'Failed to load activity types'),
240
+ code: 'activity_type_catalog_unavailable',
241
+ retryable: true,
242
+ })
243
+ }
244
+ const selected = resolveCatalogEventType(catalog, key, { includeHistorical: unchangedType || !enforceSelectableType })
245
+ if (selected && (unchangedType || !enforceSelectableType || selected.selectable)) {
246
+ return { behavior: selected.behavior, organizationIds }
247
+ }
248
+ if (unchangedType) return { behavior: calendarEventTypes[0]!.behavior, organizationIds }
249
+ const { translate } = await resolveTranslations()
250
+ throw new CrudHttpError(400, {
251
+ error: translate('customers.calendar.activityTypes.errors.unavailable', 'Activity type is unavailable'),
252
+ code: 'activity_type_unavailable',
253
+ })
254
+ }
255
+
256
+ async function loadApplicableCustomFieldKeys(
257
+ em: EntityManager,
258
+ tenantId: string,
259
+ organizationIds: string[],
260
+ behavior: CalendarEventTypeBehavior,
261
+ ): Promise<{ defined: ReadonlySet<string>; allowed: ReadonlySet<string> }> {
262
+ const options = { em, entityIds: [INTERACTION_ENTITY_ID], tenantId, organizationIds }
263
+ const all = await loadCustomFieldDefinitionIndex(options)
264
+ const fieldsets = behavior.customFieldsetIds.map((id) => id === '__general__' ? '' : id)
265
+ const applicable = fieldsets.length
266
+ ? await loadCustomFieldDefinitionIndex({ ...options, fieldset: fieldsets })
267
+ : all
268
+ return {
269
+ defined: new Set([...all.keys()].map((key) => key.toLowerCase())),
270
+ allowed: new Set([...applicable.keys()].map((key) => key.toLowerCase())),
271
+ }
272
+ }
273
+
274
+ function inapplicableCustomFieldKeys(
275
+ values: Readonly<Record<string, unknown>>,
276
+ keys: { defined: ReadonlySet<string>; allowed: ReadonlySet<string> },
277
+ ): string[] {
278
+ return Object.entries(values)
279
+ .filter(([key, value]) => keys.defined.has(key.toLowerCase()) && !keys.allowed.has(key.toLowerCase())
280
+ && value !== null && value !== undefined && value !== '' && (!Array.isArray(value) || value.length > 0))
281
+ .map(([key]) => key)
282
+ }
283
+
284
+ function inapplicableFieldError(fields: string[], message: string): CrudHttpError {
285
+ return new CrudHttpError(400, {
286
+ error: message,
287
+ code: 'activity_type_field_not_applicable',
288
+ fields,
289
+ })
290
+ }
291
+
202
292
  async function emitLifecycleEvent(
203
293
  ctx: CommandRuntimeContext,
204
294
  eventId: string,
@@ -259,7 +349,20 @@ function createTransactionalDataEngine(ctx: CommandRuntimeContext, em: EntityMan
259
349
  async function runInTransaction<TResult>(
260
350
  em: EntityManager,
261
351
  operation: (trx: EntityManager) => Promise<TResult>,
352
+ beforeWrite?: (trx: EntityManager, input?: unknown) => Promise<void>,
353
+ input?: unknown,
262
354
  ): Promise<TResult> {
355
+ if (typeof em.isInTransaction === 'function' && em.isInTransaction()) {
356
+ // Joining a caller-owned transaction: this helper neither begins nor commits it,
357
+ // so the commit boundary — and with it the point the CRUD side effects become
358
+ // durable — belongs to that caller. The side effects this command emits after
359
+ // `runInTransaction` returns therefore fire BEFORE the outer commit, unlike the
360
+ // self-managed branch below. No in-repo caller passes an open transaction today;
361
+ // a future one must either emit its own side effects after its commit or open a
362
+ // nested savepoint instead.
363
+ await beforeWrite?.(em, input)
364
+ return operation(em)
365
+ }
263
366
  // Mirrors the SPEC-018 fix applied to withAtomicFlush: use explicit begin/commit/rollback
264
367
  // so the outer EntityManager stays bound to the transaction, and closures over `em` inside
265
368
  // `operation` participate in the same transaction. This avoids the em.transactional(cb)
@@ -274,6 +377,7 @@ async function runInTransaction<TResult>(
274
377
  }
275
378
  await em.begin()
276
379
  try {
380
+ await beforeWrite?.(em, input)
277
381
  const result = await operation(em)
278
382
  await em.commit()
279
383
  return result
@@ -328,6 +432,7 @@ type InteractionGraphValues = {
328
432
  appearanceIcon: string | null
329
433
  appearanceColor: string | null
330
434
  durationMinutes: number | null
435
+ timezone: string | null
331
436
  location: string | null
332
437
  allDay: boolean | null
333
438
  recurrenceRule: string | null
@@ -364,6 +469,7 @@ function buildInteractionGraph(em: EntityManager, values: InteractionGraphValues
364
469
  appearanceIcon: values.appearanceIcon,
365
470
  appearanceColor: values.appearanceColor,
366
471
  durationMinutes: values.durationMinutes,
472
+ timezone: values.timezone ?? null,
367
473
  location: values.location,
368
474
  allDay: values.allDay,
369
475
  recurrenceRule: values.recurrenceRule,
@@ -383,7 +489,7 @@ const createInteractionCommand: CommandHandler<InteractionCreateInput, { interac
383
489
  async execute(rawInput, ctx) {
384
490
  const { parsed, custom } = parseWithCustomFields(interactionCreateSchema, rawInput)
385
491
 
386
- const em = (ctx.container.resolve('em') as EntityManager).fork()
492
+ const em = ctx.transactionalEm ?? (ctx.container.resolve('em') as EntityManager).fork()
387
493
  const normalizedAuthor = normalizeAuthorUserId(parsed.authorUserId ?? null, ctx.auth)
388
494
  const { interaction, entityId, nextInteractionId } = await runInTransaction(em, async (trx) => {
389
495
  const entity = await requireTimelineParentEntity(trx, parsed.entityId, { tenantId: parsed.tenantId, organizationId: parsed.organizationId })
@@ -394,6 +500,23 @@ const createInteractionCommand: CommandHandler<InteractionCreateInput, { interac
394
500
  await requireDealInScope(trx, parsed.dealId, entity.tenantId, entity.organizationId)
395
501
  }
396
502
 
503
+ const enforceSelectableType = requestsSelectableTypeEnforcement(rawInput)
504
+ const { behavior, organizationIds } = await resolveInteractionTypeBehavior(
505
+ ctx, trx, entity.tenantId, entity.organizationId, parsed.interactionType, false, enforceSelectableType,
506
+ )
507
+ const inapplicableCore = findInapplicableCoreFields(behavior, parsed)
508
+ const customKeys = Object.keys(custom).length
509
+ ? await loadApplicableCustomFieldKeys(trx, entity.tenantId, organizationIds, behavior)
510
+ : null
511
+ const inapplicableCustom = customKeys ? inapplicableCustomFieldKeys(custom, customKeys) : []
512
+ if (enforceSelectableType && (inapplicableCore.length || inapplicableCustom.length)) {
513
+ const { translate } = await resolveTranslations()
514
+ throw inapplicableFieldError(
515
+ [...inapplicableCore, ...inapplicableCustom.map((key) => `cf_${key}`)],
516
+ translate('customers.calendar.activityTypes.errors.fieldNotApplicable', 'Field is not applicable to this activity type'),
517
+ )
518
+ }
519
+
397
520
  const interaction = buildInteractionGraph(trx, {
398
521
  ...(parsed.id ? { id: parsed.id } : {}),
399
522
  organizationId: entity.organizationId,
@@ -413,6 +536,7 @@ const createInteractionCommand: CommandHandler<InteractionCreateInput, { interac
413
536
  appearanceIcon: parsed.appearanceIcon ?? null,
414
537
  appearanceColor: parsed.appearanceColor ?? null,
415
538
  durationMinutes: parsed.durationMinutes ?? null,
539
+ timezone: canonicalCalendarTimezone(parsed.timezone) ?? null,
416
540
  location: parsed.location ?? null,
417
541
  allDay: parsed.allDay ?? null,
418
542
  recurrenceRule: parsed.recurrenceRule ?? null,
@@ -441,7 +565,7 @@ const createInteractionCommand: CommandHandler<InteractionCreateInput, { interac
441
565
  entityId: entity.id,
442
566
  nextInteractionId: projection.nextInteractionId,
443
567
  }
444
- })
568
+ }, ctx.beforeTransactionalWrite, rawInput)
445
569
 
446
570
  const de = (ctx.container.resolve('dataEngine') as DataEngine)
447
571
  await emitCrudSideEffects({
@@ -466,7 +590,7 @@ const createInteractionCommand: CommandHandler<InteractionCreateInput, { interac
466
590
  return { interactionId: interaction.id, entityId }
467
591
  },
468
592
  captureAfter: async (_input, result, ctx) => {
469
- const em = (ctx.container.resolve('em') as EntityManager).fork()
593
+ const em = ctx.transactionalEm ?? (ctx.container.resolve('em') as EntityManager).fork()
470
594
  return await loadInteractionSnapshot(em, result.interactionId)
471
595
  },
472
596
  buildLog: async ({ result, snapshots }) => {
@@ -544,6 +668,7 @@ const createInteractionCommand: CommandHandler<InteractionCreateInput, { interac
544
668
  appearanceIcon: after.interaction.appearanceIcon,
545
669
  appearanceColor: after.interaction.appearanceColor,
546
670
  durationMinutes: after.interaction.durationMinutes,
671
+ timezone: after.interaction.timezone ?? null,
547
672
  location: after.interaction.location,
548
673
  allDay: after.interaction.allDay,
549
674
  recurrenceRule: after.interaction.recurrenceRule,
@@ -572,6 +697,7 @@ const createInteractionCommand: CommandHandler<InteractionCreateInput, { interac
572
697
  interaction.appearanceIcon = after.interaction.appearanceIcon
573
698
  interaction.appearanceColor = after.interaction.appearanceColor
574
699
  interaction.durationMinutes = after.interaction.durationMinutes
700
+ interaction.timezone = after.interaction.timezone ?? null
575
701
  interaction.location = after.interaction.location
576
702
  interaction.allDay = after.interaction.allDay
577
703
  interaction.recurrenceRule = after.interaction.recurrenceRule
@@ -631,17 +757,20 @@ const createInteractionCommand: CommandHandler<InteractionCreateInput, { interac
631
757
 
632
758
  // ─── Update ─────────────────────────────────────────────────────────
633
759
 
760
+ async function prepareUpdateInteractionSnapshot(rawInput: InteractionUpdateInput, ctx: CommandRuntimeContext) {
761
+ const { parsed } = parseWithCustomFields(interactionUpdateSchema, rawInput)
762
+ const em = ctx.transactionalEm ?? (ctx.container.resolve('em') as EntityManager).fork()
763
+ const snapshot = await loadInteractionSnapshot(em, parsed.id)
764
+ return snapshot ? { before: snapshot } : {}
765
+ }
766
+
634
767
  const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interactionId: string }> = {
635
768
  id: 'customers.interactions.update',
636
- async prepare(rawInput, ctx) {
637
- const { parsed } = parseWithCustomFields(interactionUpdateSchema, rawInput)
638
- const em = (ctx.container.resolve('em') as EntityManager).fork()
639
- const snapshot = await loadInteractionSnapshot(em, parsed.id)
640
- return snapshot ? { before: snapshot } : {}
641
- },
769
+ prepare: prepareUpdateInteractionSnapshot,
770
+ prepareSnapshotInsideTransaction: true,
642
771
  async execute(rawInput, ctx) {
643
772
  const { parsed, custom } = parseWithCustomFields(interactionUpdateSchema, rawInput)
644
- const em = (ctx.container.resolve('em') as EntityManager).fork()
773
+ const em = ctx.transactionalEm ?? (ctx.container.resolve('em') as EntityManager).fork()
645
774
  const { interaction, projections } = await runInTransaction(em, async (trx) => {
646
775
  const interaction = await findOneWithDecryption(trx, CustomerInteraction, { id: parsed.id, deletedAt: null })
647
776
  if (!interaction) {
@@ -663,6 +792,84 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
663
792
  request: ctx.request ?? null,
664
793
  })
665
794
 
795
+ const nextType = parsed.interactionType ?? interaction.interactionType
796
+ const changingType = nextType !== interaction.interactionType
797
+ const enforceSelectableType = requestsSelectableTypeEnforcement(rawInput)
798
+ const { behavior, organizationIds } = await resolveInteractionTypeBehavior(
799
+ ctx, trx, interaction.tenantId, interaction.organizationId, nextType, !changingType, enforceSelectableType,
800
+ )
801
+ const currentCoreValues = {
802
+ durationMinutes: interaction.durationMinutes,
803
+ timezone: interaction.timezone ?? null,
804
+ allDay: interaction.allDay,
805
+ recurrenceRule: interaction.recurrenceRule,
806
+ recurrenceEnd: interaction.recurrenceEnd,
807
+ location: interaction.location,
808
+ participants: interaction.participants,
809
+ priority: interaction.priority,
810
+ linkedEntities: interaction.linkedEntities,
811
+ }
812
+ const typeChangeFields = changingType && enforceSelectableType ? omitHiddenCoreClears(behavior, parsed) : parsed
813
+ const coreValues = { ...currentCoreValues, ...typeChangeFields }
814
+ const inapplicableCore = findInapplicableCoreFields(behavior, changingType ? coreValues : parsed)
815
+ const confirmsDiscard = parsed.confirmDiscardInapplicableValues === true
816
+ const currentCustom = changingType && (enforceSelectableType || confirmsDiscard)
817
+ ? await loadCustomFieldSnapshot(trx, {
818
+ entityId: INTERACTION_ENTITY_ID,
819
+ recordId: interaction.id,
820
+ tenantId: interaction.tenantId,
821
+ organizationId: interaction.organizationId,
822
+ })
823
+ : {}
824
+ const customValues = changingType ? { ...currentCustom, ...custom } : custom
825
+ const customKeys = Object.keys(customValues).length
826
+ ? await loadApplicableCustomFieldKeys(trx, interaction.tenantId, organizationIds, behavior)
827
+ : null
828
+ const inapplicableCustom = customKeys ? inapplicableCustomFieldKeys(customValues, customKeys) : []
829
+ if (!changingType && enforceSelectableType && (inapplicableCore.length || inapplicableCustom.length)) {
830
+ const { translate } = await resolveTranslations()
831
+ throw inapplicableFieldError(
832
+ [...inapplicableCore, ...inapplicableCustom.map((key) => `cf_${key}`)],
833
+ translate('customers.calendar.activityTypes.errors.fieldNotApplicable', 'Field is not applicable to this activity type'),
834
+ )
835
+ }
836
+ if (changingType && enforceSelectableType && (inapplicableCore.length || inapplicableCustom.length) && !confirmsDiscard) {
837
+ const { translate } = await resolveTranslations()
838
+ throw new CrudHttpError(409, {
839
+ error: translate('customers.calendar.activityTypes.errors.changeConfirmationRequired', 'Changing type will clear existing values'),
840
+ code: 'calendar_type_change_confirmation_required',
841
+ fields: [...inapplicableCore, ...inapplicableCustom.map((key) => `cf_${key}`)],
842
+ })
843
+ }
844
+ // The type-change 409 is scoped to the selectable-type opt-in, and stored
845
+ // values are cleared only on an explicit `confirmDiscardInapplicableValues`.
846
+ // A legacy caller (the schedule dialog, the deal composer, the kanban quick
847
+ // actions, a direct API client) sends a partial payload and does not handle
848
+ // the 409, so judging it on the merged stored row rejected switches it has
849
+ // always been able to make — e.g. a Task carrying a priority becoming a
850
+ // Meeting. Those callers keep the pre-catalog behavior exactly: the fields
851
+ // they sent are written and nothing stored is cleared, even when the new
852
+ // type cannot display it.
853
+ const discardsInapplicableValues = changingType && confirmsDiscard
854
+ const clearedCore = discardsInapplicableValues
855
+ ? clearInapplicableCoreFields(behavior, coreValues)
856
+ : {}
857
+ const clearedCustom = discardsInapplicableValues
858
+ ? Object.fromEntries(inapplicableCustom.map((key) => [key, Array.isArray(customValues[key]) ? [] : null]))
859
+ : {}
860
+ // Narrowing a same-type edit (ignoring explicit clears of hidden core fields,
861
+ // dropping custom fields outside the type's fieldsets) is part of the opt-in
862
+ // contract too: a legacy caller's write is accepted as sent.
863
+ const sameTypeFields = changingType
864
+ ? typeChangeFields
865
+ : preserveHiddenCoreValuesOnSameTypeEdit(behavior, currentCoreValues, parsed, {
866
+ ignoreHiddenClears: enforceSelectableType,
867
+ })
868
+ const sameTypeCustom = changingType || !customKeys || !enforceSelectableType
869
+ ? custom
870
+ : Object.fromEntries(Object.entries(custom).filter(([key]) =>
871
+ !customKeys.defined.has(key.toLowerCase()) || customKeys.allowed.has(key.toLowerCase())))
872
+
666
873
  // Email visibility is an access-controlled field: only the interaction's
667
874
  // author may change a
668
875
  // private email's visibility (mirrors the dedicated PATCH .../visibility
@@ -718,24 +925,36 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
718
925
  if (parsed.body !== undefined) interaction.body = parsed.body ?? null
719
926
  if (parsed.status !== undefined) interaction.status = parsed.status
720
927
  if (parsed.scheduledAt !== undefined) interaction.scheduledAt = parsed.scheduledAt ?? null
928
+ if (parsed.timezone !== undefined) interaction.timezone = canonicalCalendarTimezone(parsed.timezone) ?? null
721
929
  if (parsed.occurredAt !== undefined) interaction.occurredAt = parsed.occurredAt ?? null
722
- if (parsed.priority !== undefined) interaction.priority = parsed.priority ?? null
930
+ if (sameTypeFields.priority !== undefined) interaction.priority = sameTypeFields.priority as number | null
723
931
  if (parsed.authorUserId !== undefined) interaction.authorUserId = parsed.authorUserId ?? null
724
932
  if (parsed.ownerUserId !== undefined) interaction.ownerUserId = parsed.ownerUserId ?? null
725
933
  if (parsed.appearanceIcon !== undefined) interaction.appearanceIcon = parsed.appearanceIcon ?? null
726
934
  if (parsed.appearanceColor !== undefined) interaction.appearanceColor = parsed.appearanceColor ?? null
727
935
  if (parsed.pinned !== undefined) interaction.pinned = parsed.pinned
728
- if (parsed.durationMinutes !== undefined) interaction.durationMinutes = parsed.durationMinutes ?? null
729
- if (parsed.location !== undefined) interaction.location = parsed.location ?? null
730
- if (parsed.allDay !== undefined) interaction.allDay = parsed.allDay ?? null
731
- if (parsed.recurrenceRule !== undefined) interaction.recurrenceRule = parsed.recurrenceRule ?? null
732
- if (parsed.recurrenceEnd !== undefined) interaction.recurrenceEnd = parsed.recurrenceEnd ?? null
733
- if (parsed.participants !== undefined) interaction.participants = parsed.participants ?? null
936
+ if (sameTypeFields.durationMinutes !== undefined) interaction.durationMinutes = sameTypeFields.durationMinutes as number | null
937
+ if (sameTypeFields.location !== undefined) interaction.location = sameTypeFields.location as string | null
938
+ if (sameTypeFields.allDay !== undefined) interaction.allDay = sameTypeFields.allDay as boolean | null
939
+ if (sameTypeFields.recurrenceRule !== undefined) interaction.recurrenceRule = sameTypeFields.recurrenceRule as string | null
940
+ if (sameTypeFields.recurrenceEnd !== undefined) interaction.recurrenceEnd = sameTypeFields.recurrenceEnd as Date | null
941
+ if (sameTypeFields.participants !== undefined) interaction.participants = sameTypeFields.participants as InteractionSnapshot['interaction']['participants']
734
942
  if (parsed.reminderMinutes !== undefined) interaction.reminderMinutes = parsed.reminderMinutes ?? null
735
943
  if (parsed.visibility !== undefined) interaction.visibility = parsed.visibility ?? null
736
- if (parsed.linkedEntities !== undefined) interaction.linkedEntities = parsed.linkedEntities ?? null
944
+ if (sameTypeFields.linkedEntities !== undefined) interaction.linkedEntities = sameTypeFields.linkedEntities as InteractionSnapshot['interaction']['linkedEntities']
737
945
  if (parsed.guestPermissions !== undefined) interaction.guestPermissions = parsed.guestPermissions ?? null
738
946
 
947
+ for (const [field, value] of Object.entries(clearedCore)) {
948
+ if (field === 'linkedEntities') interaction.linkedEntities = value as InteractionSnapshot['interaction']['linkedEntities']
949
+ else if (field === 'durationMinutes') interaction.durationMinutes = null
950
+ else if (field === 'allDay') interaction.allDay = null
951
+ else if (field === 'recurrenceRule') interaction.recurrenceRule = null
952
+ else if (field === 'recurrenceEnd') interaction.recurrenceEnd = null
953
+ else if (field === 'location') interaction.location = null
954
+ else if (field === 'participants') interaction.participants = null
955
+ else if (field === 'priority') interaction.priority = null
956
+ }
957
+
739
958
  await trx.flush()
740
959
 
741
960
  const entityId = typeof interaction.entity === 'string' ? interaction.entity : interaction.entity.id
@@ -744,7 +963,7 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
744
963
  interaction.id,
745
964
  interaction.organizationId,
746
965
  interaction.tenantId,
747
- custom,
966
+ { ...sameTypeCustom, ...clearedCustom },
748
967
  )
749
968
 
750
969
  const projection = await recomputeNextInteraction(trx, entityId)
@@ -760,7 +979,7 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
760
979
  }
761
980
 
762
981
  return { interaction, projections }
763
- })
982
+ }, ctx.beforeTransactionalWrite, rawInput)
764
983
 
765
984
  const de = (ctx.container.resolve('dataEngine') as DataEngine)
766
985
  await emitCrudSideEffects({
@@ -787,7 +1006,7 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
787
1006
  return { interactionId: interaction.id }
788
1007
  },
789
1008
  captureAfter: async (_input, result, ctx) => {
790
- const em = (ctx.container.resolve('em') as EntityManager).fork()
1009
+ const em = ctx.transactionalEm ?? (ctx.container.resolve('em') as EntityManager).fork()
791
1010
  return await loadInteractionSnapshot(em, result.interactionId)
792
1011
  },
793
1012
  buildLog: async ({ snapshots }) => {
@@ -842,6 +1061,7 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
842
1061
  appearanceIcon: before.interaction.appearanceIcon,
843
1062
  appearanceColor: before.interaction.appearanceColor,
844
1063
  durationMinutes: before.interaction.durationMinutes,
1064
+ timezone: before.interaction.timezone ?? null,
845
1065
  location: before.interaction.location,
846
1066
  allDay: before.interaction.allDay,
847
1067
  recurrenceRule: before.interaction.recurrenceRule,
@@ -871,6 +1091,7 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
871
1091
  interaction.appearanceIcon = before.interaction.appearanceIcon
872
1092
  interaction.appearanceColor = before.interaction.appearanceColor
873
1093
  interaction.durationMinutes = before.interaction.durationMinutes
1094
+ interaction.timezone = before.interaction.timezone ?? null
874
1095
  interaction.location = before.interaction.location
875
1096
  interaction.allDay = before.interaction.allDay
876
1097
  interaction.recurrenceRule = before.interaction.recurrenceRule
@@ -1322,6 +1543,7 @@ const deleteInteractionCommand: CommandHandler<{ body?: Record<string, unknown>;
1322
1543
  appearanceIcon: before.interaction.appearanceIcon,
1323
1544
  appearanceColor: before.interaction.appearanceColor,
1324
1545
  durationMinutes: before.interaction.durationMinutes,
1546
+ timezone: before.interaction.timezone ?? null,
1325
1547
  location: before.interaction.location,
1326
1548
  allDay: before.interaction.allDay,
1327
1549
  recurrenceRule: before.interaction.recurrenceRule,
@@ -1352,6 +1574,7 @@ const deleteInteractionCommand: CommandHandler<{ body?: Record<string, unknown>;
1352
1574
  interaction.appearanceIcon = before.interaction.appearanceIcon
1353
1575
  interaction.appearanceColor = before.interaction.appearanceColor
1354
1576
  interaction.durationMinutes = before.interaction.durationMinutes
1577
+ interaction.timezone = before.interaction.timezone ?? null
1355
1578
  interaction.location = before.interaction.location
1356
1579
  interaction.allDay = before.interaction.allDay
1357
1580
  interaction.recurrenceRule = before.interaction.recurrenceRule