@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
@@ -0,0 +1,674 @@
1
+ import { z } from 'zod'
2
+ import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
3
+
4
+ export const calendarEventBaseKindSchema = z.enum(['meeting', 'call', 'email', 'note', 'event', 'task'])
5
+
6
+ export const calendarEventTypeFieldsSchema = z.strictObject({
7
+ endTime: z.boolean(),
8
+ allDay: z.boolean(),
9
+ recurrence: z.boolean(),
10
+ location: z.enum(['none', 'location', 'phoneLink']),
11
+ people: z.enum(['none', 'attendees', 'participants', 'recipients', 'assignee']),
12
+ priority: z.boolean(),
13
+ resources: z.boolean(),
14
+ })
15
+
16
+ export const calendarEventTypeBehaviorSchema = z.strictObject({
17
+ schemaVersion: z.literal(1),
18
+ baseKind: calendarEventBaseKindSchema,
19
+ selectable: z.boolean(),
20
+ order: z.number().int().min(0).max(10_000),
21
+ fields: calendarEventTypeFieldsSchema,
22
+ customFieldsetIds: z.array(z.string().trim().min(1).max(100)).max(32).refine(
23
+ (values) => new Set(values).size === values.length,
24
+ 'Custom fieldset ids must be unique',
25
+ ),
26
+ })
27
+
28
+ export const calendarEventTypeKeySchema = z.string().trim().min(1).max(100).regex(
29
+ /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/,
30
+ 'Calendar event type keys must be lowercase dictionary keys',
31
+ )
32
+
33
+ export const calendarEventTypeDefinitionSchema = z.strictObject({
34
+ key: calendarEventTypeKeySchema,
35
+ label: z.string().trim().min(1).max(150),
36
+ labelKey: z.string().trim().min(1).max(200).optional(),
37
+ icon: z.string().trim().max(100).nullable().optional(),
38
+ color: z.string().trim().max(100).nullable().optional(),
39
+ behavior: calendarEventTypeBehaviorSchema,
40
+ adminConfigurable: z.boolean().optional(),
41
+ panelKey: z.string().trim().min(1).max(150).optional(),
42
+ })
43
+
44
+ export const calendarEventTypeBehaviorOverrideSchema = z.strictObject({
45
+ schemaVersion: z.literal(1).optional(),
46
+ baseKind: calendarEventBaseKindSchema.optional(),
47
+ selectable: z.boolean().optional(),
48
+ order: z.number().int().min(0).max(10_000).optional(),
49
+ fields: calendarEventTypeFieldsSchema.partial().strict().optional(),
50
+ customFieldsetIds: z.array(z.string().trim().min(1).max(100)).max(32).refine(
51
+ (values) => new Set(values).size === values.length,
52
+ 'Custom fieldset ids must be unique',
53
+ ).optional(),
54
+ })
55
+
56
+ export const calendarEventTypeDefinitionOverrideSchema = z.strictObject({
57
+ label: z.string().trim().min(1).max(150).optional(),
58
+ labelKey: z.string().trim().min(1).max(200).optional(),
59
+ icon: z.string().trim().max(100).nullable().optional(),
60
+ color: z.string().trim().max(100).nullable().optional(),
61
+ behavior: calendarEventTypeBehaviorOverrideSchema.optional(),
62
+ adminConfigurable: z.boolean().optional(),
63
+ panelKey: z.string().trim().min(1).max(150).optional(),
64
+ })
65
+
66
+ export const calendarEventTypePatchSchema = z.strictObject({
67
+ targetEventTypeKey: calendarEventTypeKeySchema,
68
+ replaceLabel: z.string().trim().min(1).max(150).optional(),
69
+ replaceLabelKey: z.string().trim().min(1).max(200).nullable().optional(),
70
+ replaceIcon: z.string().trim().max(100).nullable().optional(),
71
+ replaceColor: z.string().trim().max(100).nullable().optional(),
72
+ replaceBaseKind: calendarEventBaseKindSchema.optional(),
73
+ replaceSelectable: z.boolean().optional(),
74
+ replaceOrder: z.number().int().min(0).max(10_000).optional(),
75
+ replaceFields: calendarEventTypeFieldsSchema.partial().strict().optional(),
76
+ replaceAdminConfigurable: z.boolean().optional(),
77
+ replacePanelKey: z.string().trim().min(1).max(150).nullable().optional(),
78
+ replaceCustomFieldsetIds: calendarEventTypeBehaviorSchema.shape.customFieldsetIds.optional(),
79
+ deleteCustomFieldsetIds: calendarEventTypeBehaviorSchema.shape.customFieldsetIds.optional(),
80
+ appendCustomFieldsetIds: calendarEventTypeBehaviorSchema.shape.customFieldsetIds.optional(),
81
+ })
82
+
83
+ export const calendarEventTypeProvenanceSchema = z.strictObject({
84
+ moduleId: z.string().trim().min(1).max(100),
85
+ sourcePath: z.string().trim().min(1).max(500),
86
+ moduleOrder: z.number().int().min(0),
87
+ phase: z.enum(['core', 'widget', 'module', 'programmatic', 'fallback']),
88
+ })
89
+
90
+ export const calendarEventTypeTombstoneSchema = z.strictObject({
91
+ key: calendarEventTypeKeySchema,
92
+ provenance: calendarEventTypeProvenanceSchema,
93
+ })
94
+
95
+ export const calendarEventTypeHistoricalFallbackSchema = z.strictObject({
96
+ key: calendarEventTypeKeySchema,
97
+ reason: z.enum(['tombstoned', 'module-unavailable', 'unknown']),
98
+ definition: calendarEventTypeDefinitionSchema,
99
+ })
100
+
101
+ export type CalendarEventTypeWidget = InjectionWidgetModule & {
102
+ eventTypes: readonly CalendarEventTypeDefinition[]
103
+ eventTypeOverrides?: Readonly<Record<string, CalendarEventTypeDefinition | null>>
104
+ eventTypePatches?: readonly CalendarEventTypePatch[]
105
+ }
106
+
107
+ export type CalendarEventBaseKind = z.infer<typeof calendarEventBaseKindSchema>
108
+ export type CalendarEventTypeFields = z.infer<typeof calendarEventTypeFieldsSchema>
109
+ export type CalendarEventTypeBehavior = z.infer<typeof calendarEventTypeBehaviorSchema>
110
+ export type CalendarEventTypeDefinition = z.infer<typeof calendarEventTypeDefinitionSchema>
111
+ export type CalendarEventTypeBehaviorOverride = z.infer<typeof calendarEventTypeBehaviorOverrideSchema>
112
+ export type CalendarEventTypeDefinitionOverride = z.infer<typeof calendarEventTypeDefinitionOverrideSchema>
113
+ export type CalendarEventTypePatch = z.infer<typeof calendarEventTypePatchSchema>
114
+ export type CalendarEventTypeProvenance = z.infer<typeof calendarEventTypeProvenanceSchema>
115
+ export type CalendarEventTypeTombstone = z.infer<typeof calendarEventTypeTombstoneSchema>
116
+ export type CalendarEventTypeHistoricalFallback = z.infer<typeof calendarEventTypeHistoricalFallbackSchema>
117
+
118
+ export type EffectiveCalendarEventType = Readonly<CalendarEventTypeDefinition & {
119
+ provenance: Readonly<Record<string, CalendarEventTypeProvenance>>
120
+ historical: boolean
121
+ fallbackReason?: CalendarEventTypeHistoricalFallback['reason']
122
+ }>
123
+
124
+ export type CalendarEventPanelCapabilities = Readonly<{
125
+ resourcesEnabled: boolean
126
+ staffEnabled: boolean
127
+ }>
128
+
129
+ export type CalendarEventTypePanelProps = {
130
+ definition: EffectiveCalendarEventType
131
+ panelKey?: string
132
+ mode: 'create' | 'edit'
133
+ values: Readonly<Record<string, unknown>>
134
+ errors: Readonly<Record<string, string | undefined>>
135
+ disabled: boolean
136
+ capabilities: CalendarEventPanelCapabilities
137
+ setValue: (fieldId: string, value: unknown) => void
138
+ }
139
+
140
+ export type CalendarEventTypeDiagnostic = Readonly<{
141
+ code: 'invalid-source' | 'duplicate-definition' | 'unknown-override' | 'unknown-patch'
142
+ key?: string
143
+ moduleId: string
144
+ widgetId?: string
145
+ owner?: string
146
+ }>
147
+
148
+ const behavior = (
149
+ baseKind: CalendarEventBaseKind,
150
+ order: number,
151
+ fields: CalendarEventTypeFields,
152
+ ): CalendarEventTypeBehavior => ({
153
+ schemaVersion: 1,
154
+ baseKind,
155
+ selectable: true,
156
+ order,
157
+ fields,
158
+ customFieldsetIds: [],
159
+ })
160
+
161
+ const resources = true
162
+
163
+ export const calendarEventTypes: readonly CalendarEventTypeDefinition[] = deepFreeze([
164
+ { key: 'meeting', label: 'Meeting', labelKey: 'customers.calendar.editor.types.meeting', icon: 'lucide:users', color: '#f59e0b', behavior: behavior('meeting', 0, { endTime: true, allDay: true, recurrence: true, location: 'location', people: 'attendees', priority: false, resources }) },
165
+ { key: 'call', label: 'Call', labelKey: 'customers.calendar.editor.types.call', icon: 'lucide:phone-call', color: '#2563eb', behavior: behavior('call', 100, { endTime: true, allDay: true, recurrence: true, location: 'phoneLink', people: 'participants', priority: false, resources }) },
166
+ { key: 'email', label: 'Email', labelKey: 'customers.calendar.editor.types.email', icon: 'lucide:mail', color: '#16a34a', behavior: behavior('email', 200, { endTime: false, allDay: true, recurrence: true, location: 'none', people: 'recipients', priority: false, resources }) },
167
+ { key: 'note', label: 'Note', labelKey: 'customers.calendar.editor.types.note', icon: 'lucide:notebook', color: '#a855f7', behavior: behavior('note', 300, { endTime: false, allDay: false, recurrence: false, location: 'none', people: 'none', priority: false, resources }) },
168
+ { key: 'event', label: 'Event', labelKey: 'customers.calendar.editor.types.event', icon: 'lucide:calendar', color: '#6366f1', behavior: behavior('event', 400, { endTime: true, allDay: true, recurrence: true, location: 'location', people: 'attendees', priority: false, resources }) },
169
+ { key: 'task', label: 'Task', labelKey: 'customers.calendar.editor.types.task', icon: 'lucide:check-square', color: '#ef4444', behavior: behavior('task', 500, { endTime: true, allDay: true, recurrence: true, location: 'none', people: 'assignee', priority: true, resources }) },
170
+ ])
171
+
172
+ export const calendarEventTypeOverrides: Readonly<Record<string, CalendarEventTypeDefinitionOverride | null>> = Object.freeze({})
173
+
174
+ export type CalendarEventTypeWidgetContribution = Readonly<{
175
+ moduleId: string
176
+ widgetId: string
177
+ priority?: number
178
+ definitions?: unknown
179
+ overrides?: unknown
180
+ patches?: unknown
181
+ }>
182
+
183
+ export type CalendarOverrides = Readonly<{
184
+ eventTypes?: Readonly<Record<string, CalendarEventTypeDefinition | null>>
185
+ patches?: readonly CalendarEventTypePatch[]
186
+ }>
187
+
188
+ export type CalendarModuleOverrideEntry = Readonly<{
189
+ moduleId: string
190
+ overrides: CalendarOverrides
191
+ }>
192
+
193
+ type ProgrammaticSource = {
194
+ definitions: Map<string, CalendarEventTypeDefinition>
195
+ overrides: Map<string, CalendarEventTypeDefinition | null>
196
+ patches: Map<string, CalendarEventTypePatch>
197
+ }
198
+
199
+ type RegistryState = {
200
+ widgets: readonly ParsedWidgetContribution[]
201
+ modules: readonly CalendarModuleOverrideEntry[]
202
+ sources: Map<string, ProgrammaticSource>
203
+ historicalDefinitions: Map<string, CalendarEventTypeDefinition>
204
+ version: number
205
+ diagnostics: readonly CalendarEventTypeDiagnostic[]
206
+ }
207
+
208
+ const REGISTRY_KEY = Symbol.for('open-mercato.customers.calendar-event-types.v1')
209
+
210
+ function getRegistryState(): RegistryState {
211
+ const globalRegistry = globalThis as typeof globalThis & { [REGISTRY_KEY]?: RegistryState }
212
+ globalRegistry[REGISTRY_KEY] ??= {
213
+ widgets: Object.freeze([]),
214
+ modules: Object.freeze([]),
215
+ sources: new Map(),
216
+ historicalDefinitions: new Map(),
217
+ version: 0,
218
+ diagnostics: Object.freeze([]),
219
+ }
220
+ return globalRegistry[REGISTRY_KEY]
221
+ }
222
+
223
+ function deepFreeze<T>(value: T): T {
224
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
225
+ Object.freeze(value)
226
+ for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child)
227
+ }
228
+ return value
229
+ }
230
+
231
+ function sourceOf(moduleId: string, sourcePath: string, moduleOrder: number, phase: CalendarEventTypeProvenance['phase']): CalendarEventTypeProvenance {
232
+ return Object.freeze({ moduleId, sourcePath, moduleOrder, phase })
233
+ }
234
+
235
+ function allPropertyPaths(definition: CalendarEventTypeDefinition): string[] {
236
+ const paths = ['key', 'label', 'behavior.schemaVersion', 'behavior.baseKind', 'behavior.selectable', 'behavior.order']
237
+ if (definition.labelKey !== undefined) paths.push('labelKey')
238
+ if (definition.icon !== undefined) paths.push('icon')
239
+ if (definition.color !== undefined) paths.push('color')
240
+ if (definition.adminConfigurable !== undefined) paths.push('adminConfigurable')
241
+ if (definition.panelKey !== undefined) paths.push('panelKey')
242
+ for (const key of Object.keys(definition.behavior.fields)) paths.push(`behavior.fields.${key}`)
243
+ paths.push('behavior.customFieldsetIds')
244
+ return paths
245
+ }
246
+
247
+ const widgetContributionSchema = z.strictObject({
248
+ moduleId: z.string().trim().min(1).max(100),
249
+ widgetId: z.string().trim().min(1).max(200),
250
+ priority: z.number().int().optional(),
251
+ definitions: z.array(calendarEventTypeDefinitionSchema).optional(),
252
+ overrides: z.record(calendarEventTypeKeySchema, calendarEventTypeDefinitionSchema.nullable()).optional(),
253
+ patches: z.array(calendarEventTypePatchSchema).optional(),
254
+ }).superRefine((entry, context) => {
255
+ const keys = new Set<string>()
256
+ for (const definition of entry.definitions ?? []) {
257
+ if (keys.has(definition.key)) {
258
+ context.addIssue({ code: 'custom', message: 'Duplicate source definition', path: ['definitions'] })
259
+ }
260
+ keys.add(definition.key)
261
+ }
262
+ for (const [key, definition] of Object.entries(entry.overrides ?? {})) {
263
+ if (definition !== null && definition.key !== key) {
264
+ context.addIssue({ code: 'custom', message: 'Replacement key must match target key', path: ['overrides', key] })
265
+ }
266
+ }
267
+ })
268
+
269
+ type ParsedWidgetContribution = z.infer<typeof widgetContributionSchema>
270
+
271
+ const moduleOverrideEntrySchema = z.strictObject({
272
+ moduleId: z.string().trim().min(1).max(100),
273
+ overrides: z.strictObject({
274
+ eventTypes: z.record(calendarEventTypeKeySchema, calendarEventTypeDefinitionSchema.nullable()).optional(),
275
+ patches: z.array(calendarEventTypePatchSchema).optional(),
276
+ }),
277
+ }).superRefine((entry, context) => {
278
+ for (const [key, definition] of Object.entries(entry.overrides.eventTypes ?? {})) {
279
+ if (definition !== null && definition.key !== key) {
280
+ context.addIssue({ code: 'custom', message: 'Replacement key must match target key', path: ['overrides', 'eventTypes', key] })
281
+ }
282
+ }
283
+ })
284
+
285
+ function copyDefinition(definition: CalendarEventTypeDefinition): CalendarEventTypeDefinition {
286
+ return calendarEventTypeDefinitionSchema.parse(definition)
287
+ }
288
+
289
+ function copyPatch(patch: CalendarEventTypePatch): CalendarEventTypePatch {
290
+ return calendarEventTypePatchSchema.parse(patch)
291
+ }
292
+
293
+ function programmaticSource(sourceId: string): ProgrammaticSource {
294
+ return getRegistryState().sources.get(sourceId) ?? {
295
+ definitions: new Map(),
296
+ overrides: new Map(),
297
+ patches: new Map(),
298
+ }
299
+ }
300
+
301
+ function replaceProgrammaticSource(sourceId: string, source: ProgrammaticSource): void {
302
+ const state = getRegistryState()
303
+ archiveCurrentDefinitions()
304
+ state.sources.set(sourceId, source)
305
+ state.version += 1
306
+ }
307
+
308
+ function markDefinitionProvenance(
309
+ definition: CalendarEventTypeDefinition,
310
+ source: CalendarEventTypeProvenance,
311
+ ): Record<string, CalendarEventTypeProvenance> {
312
+ return Object.fromEntries(allPropertyPaths(definition).map((path) => [path, source]))
313
+ }
314
+
315
+ function applyPatch(
316
+ definition: CalendarEventTypeDefinition,
317
+ patch: CalendarEventTypePatch,
318
+ provenance: Record<string, CalendarEventTypeProvenance>,
319
+ source: CalendarEventTypeProvenance,
320
+ ): CalendarEventTypeDefinition {
321
+ const next: CalendarEventTypeDefinition = {
322
+ ...definition,
323
+ behavior: { ...definition.behavior, fields: { ...definition.behavior.fields }, customFieldsetIds: [...definition.behavior.customFieldsetIds] },
324
+ }
325
+ if (patch.replaceLabel !== undefined) { next.label = patch.replaceLabel; provenance.label = source }
326
+ if (patch.replaceLabelKey !== undefined) { next.labelKey = patch.replaceLabelKey ?? undefined; provenance.labelKey = source }
327
+ if (patch.replaceIcon !== undefined) { next.icon = patch.replaceIcon; provenance.icon = source }
328
+ if (patch.replaceColor !== undefined) { next.color = patch.replaceColor; provenance.color = source }
329
+ if (patch.replaceAdminConfigurable !== undefined) { next.adminConfigurable = patch.replaceAdminConfigurable; provenance.adminConfigurable = source }
330
+ if (patch.replacePanelKey !== undefined) { next.panelKey = patch.replacePanelKey ?? undefined; provenance.panelKey = source }
331
+ if (patch.replaceBaseKind !== undefined) { next.behavior.baseKind = patch.replaceBaseKind; provenance['behavior.baseKind'] = source }
332
+ if (patch.replaceSelectable !== undefined) { next.behavior.selectable = patch.replaceSelectable; provenance['behavior.selectable'] = source }
333
+ if (patch.replaceOrder !== undefined) { next.behavior.order = patch.replaceOrder; provenance['behavior.order'] = source }
334
+ if (patch.replaceFields) {
335
+ next.behavior.fields = { ...next.behavior.fields, ...patch.replaceFields }
336
+ for (const field of Object.keys(patch.replaceFields)) provenance[`behavior.fields.${field}`] = source
337
+ }
338
+ if (patch.replaceCustomFieldsetIds || patch.deleteCustomFieldsetIds || patch.appendCustomFieldsetIds) {
339
+ const fieldsets = patch.replaceCustomFieldsetIds ?? next.behavior.customFieldsetIds
340
+ const deleted = new Set(patch.deleteCustomFieldsetIds ?? [])
341
+ next.behavior.customFieldsetIds = [...new Set([...fieldsets.filter((id) => !deleted.has(id)), ...(patch.appendCustomFieldsetIds ?? [])])]
342
+ provenance['behavior.customFieldsetIds'] = source
343
+ }
344
+ return calendarEventTypeDefinitionSchema.parse(next)
345
+ }
346
+
347
+ function compose(excludedSources: ReadonlySet<object> = new Set(), rejectedSources: readonly CalendarEventTypeDiagnostic[] = []): {
348
+ items: readonly EffectiveCalendarEventType[]
349
+ tombstones: readonly CalendarEventTypeTombstone[]
350
+ diagnostics: readonly CalendarEventTypeDiagnostic[]
351
+ } {
352
+ const definitions = new Map<string, CalendarEventTypeDefinition>()
353
+ const owners = new Map<string, CalendarEventTypeProvenance>()
354
+ const provenance = new Map<string, Record<string, CalendarEventTypeProvenance>>()
355
+ const tombstones = new Map<string, CalendarEventTypeTombstone>()
356
+ const diagnostics: CalendarEventTypeDiagnostic[] = [...getRegistryState().diagnostics, ...rejectedSources]
357
+ const coreSource = Object.freeze({ moduleId: 'customers', sourcePath: 'calendar-event-types.ts', moduleOrder: 0, phase: 'core' as const })
358
+
359
+ for (const definition of calendarEventTypes) {
360
+ definitions.set(definition.key, definition)
361
+ owners.set(definition.key, coreSource)
362
+ provenance.set(definition.key, markDefinitionProvenance(definition, coreSource))
363
+ }
364
+ const state = getRegistryState()
365
+ const widgets = state.widgets.map((entry, moduleOrder) => ({ entry, moduleOrder })).filter(({ entry }) => !excludedSources.has(entry))
366
+ const modules = state.modules.map((entry, moduleOrder) => ({ entry, moduleOrder })).filter(({ entry }) => !excludedSources.has(entry))
367
+ const programmatic = [...state.sources.entries()].filter(([, contribution]) => !excludedSources.has(contribution))
368
+
369
+ for (const { entry, moduleOrder } of widgets) {
370
+ const source = sourceOf(entry.moduleId, `widgets/injection/${entry.widgetId}/widget.ts`, moduleOrder, 'widget')
371
+ for (const definition of entry.definitions ?? []) {
372
+ const existing = owners.get(definition.key)
373
+ if (existing) {
374
+ diagnostics.push({ code: 'duplicate-definition', key: definition.key, moduleId: entry.moduleId, widgetId: entry.widgetId, owner: existing.moduleId })
375
+ continue
376
+ }
377
+ definitions.set(definition.key, definition)
378
+ owners.set(definition.key, source)
379
+ provenance.set(definition.key, markDefinitionProvenance(definition, source))
380
+ }
381
+ }
382
+ for (const [sourceId, contribution] of programmatic) {
383
+ const source = sourceOf(sourceId, `di:${sourceId}`, 0, 'programmatic')
384
+ for (const definition of contribution.definitions.values()) {
385
+ const existing = owners.get(definition.key)
386
+ if (existing && existing.moduleId !== sourceId) {
387
+ diagnostics.push({ code: 'duplicate-definition', key: definition.key, moduleId: sourceId, owner: existing.moduleId })
388
+ continue
389
+ }
390
+ definitions.set(definition.key, definition)
391
+ owners.set(definition.key, source)
392
+ provenance.set(definition.key, markDefinitionProvenance(definition, source))
393
+ }
394
+ }
395
+
396
+ const applyOverrides = (
397
+ overrides: Readonly<Record<string, CalendarEventTypeDefinition | null>>,
398
+ source: CalendarEventTypeProvenance,
399
+ widgetId?: string,
400
+ ): void => {
401
+ for (const [key, override] of Object.entries(overrides)) {
402
+ if (!definitions.has(key)) {
403
+ if (override === null) {
404
+ diagnostics.push({ code: 'unknown-override', key, moduleId: source.moduleId, widgetId })
405
+ continue
406
+ }
407
+ diagnostics.push({ code: 'unknown-override', key, moduleId: source.moduleId, widgetId })
408
+ }
409
+ if (override === null) {
410
+ tombstones.set(key, deepFreeze({ key, provenance: source }))
411
+ } else {
412
+ definitions.set(key, override)
413
+ owners.set(key, source)
414
+ provenance.set(key, markDefinitionProvenance(override, source))
415
+ tombstones.delete(key)
416
+ }
417
+ }
418
+ }
419
+ for (const { entry, moduleOrder } of widgets) {
420
+ applyOverrides(entry.overrides ?? {}, sourceOf(entry.moduleId, `widgets/injection/${entry.widgetId}/widget.ts`, moduleOrder, 'widget'), entry.widgetId)
421
+ }
422
+ for (const { entry, moduleOrder } of modules) {
423
+ applyOverrides(entry.overrides.eventTypes ?? {}, sourceOf(entry.moduleId, 'modules.ts', moduleOrder, 'module'))
424
+ }
425
+ for (const [sourceId, contribution] of programmatic) {
426
+ applyOverrides(Object.fromEntries(contribution.overrides), sourceOf(sourceId, `di:${sourceId}`, 0, 'programmatic'))
427
+ }
428
+
429
+ const applyPatches = (patches: readonly CalendarEventTypePatch[], source: CalendarEventTypeProvenance, widgetId?: string): CalendarEventTypeDiagnostic | null => {
430
+ for (const patch of patches) {
431
+ const key = patch.targetEventTypeKey
432
+ const definition = definitions.get(key)
433
+ if (!definition || tombstones.has(key)) {
434
+ diagnostics.push({ code: 'unknown-patch', key, moduleId: source.moduleId, widgetId })
435
+ continue
436
+ }
437
+ const propertyProvenance = { ...provenance.get(key) }
438
+ try {
439
+ const patched = applyPatch(definition, patch, propertyProvenance, source)
440
+ definitions.set(key, patched)
441
+ provenance.set(key, propertyProvenance)
442
+ } catch {
443
+ return { code: 'invalid-source', key, moduleId: source.moduleId, widgetId }
444
+ }
445
+ }
446
+ return null
447
+ }
448
+ for (const { entry, moduleOrder } of widgets) {
449
+ const failure = applyPatches(entry.patches ?? [], sourceOf(entry.moduleId, `widgets/injection/${entry.widgetId}/widget.ts`, moduleOrder, 'widget'), entry.widgetId)
450
+ if (failure) return compose(new Set([...excludedSources, entry]), [...rejectedSources, failure])
451
+ }
452
+ for (const { entry, moduleOrder } of modules) {
453
+ const failure = applyPatches(entry.overrides.patches ?? [], sourceOf(entry.moduleId, 'modules.ts', moduleOrder, 'module'))
454
+ if (failure) return compose(new Set([...excludedSources, entry]), [...rejectedSources, failure])
455
+ }
456
+ for (const [sourceId, contribution] of programmatic) {
457
+ const failure = applyPatches([...contribution.patches.values()], sourceOf(sourceId, `di:${sourceId}`, 0, 'programmatic'))
458
+ if (failure) return compose(new Set([...excludedSources, contribution]), [...rejectedSources, failure])
459
+ }
460
+
461
+ const items = [...definitions.values()]
462
+ .filter((definition) => !tombstones.has(definition.key))
463
+ .map((definition) => deepFreeze({
464
+ ...definition,
465
+ provenance: Object.freeze({ ...(provenance.get(definition.key) ?? {}) }),
466
+ historical: false,
467
+ }))
468
+ .sort((left, right) => left.behavior.order - right.behavior.order || left.key.localeCompare(right.key))
469
+ return deepFreeze({ items, tombstones: [...tombstones.values()], diagnostics })
470
+ }
471
+
472
+ function archiveCurrentDefinitions(): void {
473
+ const state = getRegistryState()
474
+ for (const item of compose().items) {
475
+ const { provenance: _provenance, historical: _historical, fallbackReason: _fallbackReason, ...definition } = item
476
+ state.historicalDefinitions.set(item.key, deepFreeze(definition))
477
+ }
478
+ }
479
+
480
+ export function registerWidgetCalendarEventTypeContributions(entries: readonly CalendarEventTypeWidgetContribution[]): void {
481
+ const state = getRegistryState()
482
+ const diagnostics: CalendarEventTypeDiagnostic[] = []
483
+ const parsed: ParsedWidgetContribution[] = []
484
+ for (const entry of entries) {
485
+ const result = widgetContributionSchema.safeParse(entry)
486
+ if (result.success) parsed.push(result.data)
487
+ else diagnostics.push({ code: 'invalid-source', moduleId: entry.moduleId, widgetId: entry.widgetId })
488
+ }
489
+ const moduleOrder = new Map<string, number>()
490
+ entries.forEach((entry) => { if (!moduleOrder.has(entry.moduleId)) moduleOrder.set(entry.moduleId, moduleOrder.size) })
491
+ parsed.sort((left, right) => (moduleOrder.get(left.moduleId) ?? 0) - (moduleOrder.get(right.moduleId) ?? 0)
492
+ || (left.priority ?? 0) - (right.priority ?? 0) || left.widgetId.localeCompare(right.widgetId))
493
+ const moduleDiagnostics = state.diagnostics.filter((diagnostic) => !diagnostic.widgetId)
494
+ if (JSON.stringify(state.widgets) === JSON.stringify(parsed)
495
+ && JSON.stringify(state.diagnostics.filter((diagnostic) => diagnostic.widgetId)) === JSON.stringify(diagnostics)) return
496
+ archiveCurrentDefinitions()
497
+ state.widgets = deepFreeze(parsed)
498
+ state.diagnostics = deepFreeze([...diagnostics, ...moduleDiagnostics])
499
+ state.version += 1
500
+ }
501
+
502
+ export function registerCalendarModuleOverrides(entries: readonly CalendarModuleOverrideEntry[]): void {
503
+ const state = getRegistryState()
504
+ const diagnostics: CalendarEventTypeDiagnostic[] = []
505
+ const parsed: CalendarModuleOverrideEntry[] = []
506
+ for (const entry of entries) {
507
+ const result = moduleOverrideEntrySchema.safeParse(entry)
508
+ if (result.success) parsed.push(result.data)
509
+ else diagnostics.push({ code: 'invalid-source', moduleId: entry.moduleId })
510
+ }
511
+ const widgetDiagnostics = state.diagnostics.filter((diagnostic) => diagnostic.widgetId)
512
+ if (JSON.stringify(state.modules) === JSON.stringify(parsed)
513
+ && JSON.stringify(state.diagnostics.filter((diagnostic) => !diagnostic.widgetId)) === JSON.stringify(diagnostics)) return
514
+ archiveCurrentDefinitions()
515
+ state.modules = deepFreeze(parsed)
516
+ state.diagnostics = deepFreeze([...widgetDiagnostics, ...diagnostics])
517
+ state.version += 1
518
+ }
519
+
520
+ export type CalendarEventTypeRegistrySnapshot = Readonly<{
521
+ version: number
522
+ items: readonly EffectiveCalendarEventType[]
523
+ tombstones: readonly CalendarEventTypeTombstone[]
524
+ diagnostics: readonly CalendarEventTypeDiagnostic[]
525
+ }>
526
+
527
+ export interface CalendarEventTypeRegistry {
528
+ upsert(sourceId: string, definition: CalendarEventTypeDefinition): void
529
+ replace(sourceId: string, key: string, definition: CalendarEventTypeDefinition | null): void
530
+ patch(sourceId: string, patch: CalendarEventTypePatch): void
531
+ remove(sourceId: string, key: string): void
532
+ removeSource(sourceId: string): void
533
+ snapshot(): CalendarEventTypeRegistrySnapshot
534
+ }
535
+
536
+ function validatedSourceId(sourceId: string): string {
537
+ return z.string().trim().min(1).max(100).parse(sourceId)
538
+ }
539
+
540
+ function baseOwnerOf(key: string): string | undefined {
541
+ if (calendarEventTypes.some((definition) => definition.key === key)) return 'customers'
542
+ const invalidSources = compose().diagnostics.filter((diagnostic) => diagnostic.code === 'invalid-source')
543
+ for (const widget of getRegistryState().widgets) {
544
+ if (invalidSources.some((diagnostic) => diagnostic.moduleId === widget.moduleId && diagnostic.widgetId === widget.widgetId)) continue
545
+ if (widget.definitions?.some((definition) => definition.key === key)) return widget.moduleId
546
+ }
547
+ for (const [sourceId, contribution] of getRegistryState().sources) {
548
+ if (invalidSources.some((diagnostic) => diagnostic.moduleId === sourceId && !diagnostic.widgetId)) continue
549
+ if (contribution.definitions.has(key)) return sourceId
550
+ }
551
+ return compose().items.find((definition) => definition.key === key)?.provenance.key?.moduleId
552
+ }
553
+
554
+ export function createCalendarEventTypeRegistry(): CalendarEventTypeRegistry {
555
+ return {
556
+ upsert(sourceId, definition) {
557
+ const id = validatedSourceId(sourceId)
558
+ const nextDefinition = copyDefinition(definition)
559
+ const owner = baseOwnerOf(nextDefinition.key)
560
+ if (owner && owner !== id) {
561
+ throw new Error(`[internal] Calendar event type "${nextDefinition.key}" is owned by "${owner}"`)
562
+ }
563
+ const current = programmaticSource(id)
564
+ if (JSON.stringify(current.definitions.get(nextDefinition.key)) === JSON.stringify(nextDefinition)) return
565
+ replaceProgrammaticSource(id, { ...current, definitions: new Map(current.definitions).set(nextDefinition.key, nextDefinition) })
566
+ },
567
+ replace(sourceId, key, definition) {
568
+ const id = validatedSourceId(sourceId)
569
+ const parsedKey = calendarEventTypeKeySchema.parse(key)
570
+ const nextDefinition = definition === null ? null : copyDefinition(definition)
571
+ if (nextDefinition && nextDefinition.key !== parsedKey) throw new Error('[internal] Replacement key must match target key')
572
+ const current = programmaticSource(id)
573
+ if (JSON.stringify(current.overrides.get(parsedKey)) === JSON.stringify(nextDefinition)) return
574
+ replaceProgrammaticSource(id, { ...current, overrides: new Map(current.overrides).set(parsedKey, nextDefinition) })
575
+ },
576
+ patch(sourceId, patch) {
577
+ const id = validatedSourceId(sourceId)
578
+ const nextPatch = copyPatch(patch)
579
+ const current = programmaticSource(id)
580
+ if (JSON.stringify(current.patches.get(nextPatch.targetEventTypeKey)) === JSON.stringify(nextPatch)) return
581
+ replaceProgrammaticSource(id, { ...current, patches: new Map(current.patches).set(nextPatch.targetEventTypeKey, nextPatch) })
582
+ },
583
+ remove(sourceId, key) {
584
+ const id = validatedSourceId(sourceId)
585
+ const parsedKey = calendarEventTypeKeySchema.parse(key)
586
+ const current = getRegistryState().sources.get(id)
587
+ if (!current || !(current.definitions.has(parsedKey) || current.overrides.has(parsedKey) || current.patches.has(parsedKey))) return
588
+ const next = { definitions: new Map(current.definitions), overrides: new Map(current.overrides), patches: new Map(current.patches) }
589
+ next.definitions.delete(parsedKey)
590
+ next.overrides.delete(parsedKey)
591
+ next.patches.delete(parsedKey)
592
+ replaceProgrammaticSource(id, next)
593
+ },
594
+ removeSource(sourceId) {
595
+ const id = validatedSourceId(sourceId)
596
+ const state = getRegistryState()
597
+ if (!state.sources.has(id)) return
598
+ archiveCurrentDefinitions()
599
+ state.sources.delete(id)
600
+ state.version += 1
601
+ },
602
+ snapshot() {
603
+ const state = getRegistryState()
604
+ return deepFreeze({ version: state.version, ...compose() })
605
+ },
606
+ }
607
+ }
608
+
609
+ export function getCalendarEventTypeRegistryVersion(): number {
610
+ return getRegistryState().version
611
+ }
612
+
613
+ export function getCalendarEventTypes(): readonly EffectiveCalendarEventType[] {
614
+ return compose().items
615
+ }
616
+
617
+ export function resolveCalendarEventTypes(): readonly EffectiveCalendarEventType[] {
618
+ return getCalendarEventTypes()
619
+ }
620
+
621
+ export function getCalendarEventTypeTombstones(): readonly CalendarEventTypeTombstone[] {
622
+ return compose().tombstones
623
+ }
624
+
625
+ export function getCalendarEventTypeDiagnostics(): readonly CalendarEventTypeDiagnostic[] {
626
+ return compose().diagnostics
627
+ }
628
+
629
+ export function resolveCalendarEventType(
630
+ rawKey: string,
631
+ options: { includeHistorical?: boolean } = {},
632
+ ): EffectiveCalendarEventType | undefined {
633
+ const parsedKey = calendarEventTypeKeySchema.safeParse(rawKey.trim().toLowerCase())
634
+ if (!parsedKey.success) return undefined
635
+ const key = parsedKey.data
636
+ const current = compose()
637
+ const resolved = current.items.find((item) => item.key === key)
638
+ if (resolved || !options.includeHistorical) return resolved
639
+
640
+ const stored = getRegistryState().historicalDefinitions.get(key)
641
+ const reason: CalendarEventTypeHistoricalFallback['reason'] = current.tombstones.some((entry) => entry.key === key)
642
+ ? 'tombstoned'
643
+ : stored
644
+ ? 'module-unavailable'
645
+ : 'unknown'
646
+ const definition = stored ?? {
647
+ ...calendarEventTypes[0],
648
+ key,
649
+ label: rawKey,
650
+ labelKey: undefined,
651
+ behavior: { ...calendarEventTypes[0]!.behavior, selectable: false },
652
+ }
653
+ const fallbackSource = Object.freeze({ moduleId: 'customers', sourcePath: 'calendar-event-types.ts', moduleOrder: 0, phase: 'fallback' as const })
654
+ const historicalDefinition = calendarEventTypeDefinitionSchema.parse({
655
+ ...definition,
656
+ behavior: { ...definition.behavior, selectable: false },
657
+ })
658
+ return deepFreeze({
659
+ ...historicalDefinition,
660
+ provenance: Object.fromEntries(allPropertyPaths(historicalDefinition).map((path) => [path, fallbackSource])),
661
+ historical: true,
662
+ fallbackReason: reason,
663
+ })
664
+ }
665
+
666
+ export function resetCalendarEventTypeRegistryForTests(): void {
667
+ const state = getRegistryState()
668
+ state.widgets = Object.freeze([])
669
+ state.modules = Object.freeze([])
670
+ state.sources.clear()
671
+ state.historicalDefinitions.clear()
672
+ state.version = 0
673
+ state.diagnostics = Object.freeze([])
674
+ }