@open-mercato/core 0.4.2-canary-7c76659938 → 0.4.2-canary-70c8402224

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 (235) hide show
  1. package/dist/generated/entities/notification/index.js +57 -0
  2. package/dist/generated/entities/notification/index.js.map +7 -0
  3. package/dist/generated/entities.ids.generated.js +5 -1
  4. package/dist/generated/entities.ids.generated.js.map +2 -2
  5. package/dist/generated/entity-fields-registry.js +2 -0
  6. package/dist/generated/entity-fields-registry.js.map +2 -2
  7. package/dist/modules/api_docs/frontend/docs/api/page.js +3 -2
  8. package/dist/modules/api_docs/frontend/docs/api/page.js.map +2 -2
  9. package/dist/modules/auth/api/admin/nav.js +4 -3
  10. package/dist/modules/auth/api/admin/nav.js.map +2 -2
  11. package/dist/modules/auth/api/profile/route.js +155 -0
  12. package/dist/modules/auth/api/profile/route.js.map +7 -0
  13. package/dist/modules/auth/api/reset/confirm.js +25 -2
  14. package/dist/modules/auth/api/reset/confirm.js.map +2 -2
  15. package/dist/modules/auth/api/reset.js +23 -0
  16. package/dist/modules/auth/api/reset.js.map +2 -2
  17. package/dist/modules/auth/api/sidebar/preferences/route.js +14 -9
  18. package/dist/modules/auth/api/sidebar/preferences/route.js.map +2 -2
  19. package/dist/modules/auth/backend/auth/profile/page.js +99 -0
  20. package/dist/modules/auth/backend/auth/profile/page.js.map +7 -0
  21. package/dist/modules/auth/backend/auth/profile/page.meta.js +12 -0
  22. package/dist/modules/auth/backend/auth/profile/page.meta.js.map +7 -0
  23. package/dist/modules/auth/commands/users.js +55 -0
  24. package/dist/modules/auth/commands/users.js.map +2 -2
  25. package/dist/modules/auth/lib/setup-app.js +1 -0
  26. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  27. package/dist/modules/auth/notifications.js +112 -0
  28. package/dist/modules/auth/notifications.js.map +7 -0
  29. package/dist/modules/auth/services/authService.js +3 -3
  30. package/dist/modules/auth/services/authService.js.map +2 -2
  31. package/dist/modules/business_rules/notifications.js +28 -0
  32. package/dist/modules/business_rules/notifications.js.map +7 -0
  33. package/dist/modules/business_rules/subscribers/rule-execution-failed-notification.js +37 -0
  34. package/dist/modules/business_rules/subscribers/rule-execution-failed-notification.js.map +7 -0
  35. package/dist/modules/catalog/notifications.js +28 -0
  36. package/dist/modules/catalog/notifications.js.map +7 -0
  37. package/dist/modules/catalog/subscribers/low-stock-notification.js +38 -0
  38. package/dist/modules/catalog/subscribers/low-stock-notification.js.map +7 -0
  39. package/dist/modules/configs/cli.js +6 -0
  40. package/dist/modules/configs/cli.js.map +2 -2
  41. package/dist/modules/customers/commands/deals.js +31 -0
  42. package/dist/modules/customers/commands/deals.js.map +2 -2
  43. package/dist/modules/customers/notifications.js +48 -0
  44. package/dist/modules/customers/notifications.js.map +7 -0
  45. package/dist/modules/notifications/acl.js +11 -0
  46. package/dist/modules/notifications/acl.js.map +7 -0
  47. package/dist/modules/notifications/api/[id]/action/route.js +69 -0
  48. package/dist/modules/notifications/api/[id]/action/route.js.map +7 -0
  49. package/dist/modules/notifications/api/[id]/dismiss/route.js +15 -0
  50. package/dist/modules/notifications/api/[id]/dismiss/route.js.map +7 -0
  51. package/dist/modules/notifications/api/[id]/read/route.js +15 -0
  52. package/dist/modules/notifications/api/[id]/read/route.js.map +7 -0
  53. package/dist/modules/notifications/api/[id]/restore/route.js +53 -0
  54. package/dist/modules/notifications/api/[id]/restore/route.js.map +7 -0
  55. package/dist/modules/notifications/api/batch/route.js +17 -0
  56. package/dist/modules/notifications/api/batch/route.js.map +7 -0
  57. package/dist/modules/notifications/api/feature/route.js +17 -0
  58. package/dist/modules/notifications/api/feature/route.js.map +7 -0
  59. package/dist/modules/notifications/api/mark-all-read/route.js +35 -0
  60. package/dist/modules/notifications/api/mark-all-read/route.js.map +7 -0
  61. package/dist/modules/notifications/api/openapi.js +57 -0
  62. package/dist/modules/notifications/api/openapi.js.map +7 -0
  63. package/dist/modules/notifications/api/role/route.js +17 -0
  64. package/dist/modules/notifications/api/role/route.js.map +7 -0
  65. package/dist/modules/notifications/api/route.js +85 -0
  66. package/dist/modules/notifications/api/route.js.map +7 -0
  67. package/dist/modules/notifications/api/settings/route.js +96 -0
  68. package/dist/modules/notifications/api/settings/route.js.map +7 -0
  69. package/dist/modules/notifications/api/unread-count/route.js +38 -0
  70. package/dist/modules/notifications/api/unread-count/route.js.map +7 -0
  71. package/dist/modules/notifications/backend/config/notifications/page.js +10 -0
  72. package/dist/modules/notifications/backend/config/notifications/page.js.map +7 -0
  73. package/dist/modules/notifications/backend/config/notifications/page.meta.js +24 -0
  74. package/dist/modules/notifications/backend/config/notifications/page.meta.js.map +7 -0
  75. package/dist/modules/notifications/cli.js +16 -0
  76. package/dist/modules/notifications/cli.js.map +7 -0
  77. package/dist/modules/notifications/data/entities.js +112 -0
  78. package/dist/modules/notifications/data/entities.js.map +7 -0
  79. package/dist/modules/notifications/data/validators.js +94 -0
  80. package/dist/modules/notifications/data/validators.js.map +7 -0
  81. package/dist/modules/notifications/di.js +13 -0
  82. package/dist/modules/notifications/di.js.map +7 -0
  83. package/dist/modules/notifications/emails/NotificationEmail.js +58 -0
  84. package/dist/modules/notifications/emails/NotificationEmail.js.map +7 -0
  85. package/dist/modules/notifications/frontend/NotificationInboxPageClient.js +44 -0
  86. package/dist/modules/notifications/frontend/NotificationInboxPageClient.js.map +7 -0
  87. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +219 -0
  88. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +7 -0
  89. package/dist/modules/notifications/index.js +14 -0
  90. package/dist/modules/notifications/index.js.map +7 -0
  91. package/dist/modules/notifications/lib/deliveryConfig.js +105 -0
  92. package/dist/modules/notifications/lib/deliveryConfig.js.map +7 -0
  93. package/dist/modules/notifications/lib/events.js +12 -0
  94. package/dist/modules/notifications/lib/events.js.map +7 -0
  95. package/dist/modules/notifications/lib/notificationBuilder.js +66 -0
  96. package/dist/modules/notifications/lib/notificationBuilder.js.map +7 -0
  97. package/dist/modules/notifications/lib/notificationFactory.js +54 -0
  98. package/dist/modules/notifications/lib/notificationFactory.js.map +7 -0
  99. package/dist/modules/notifications/lib/notificationMapper.js +34 -0
  100. package/dist/modules/notifications/lib/notificationMapper.js.map +7 -0
  101. package/dist/modules/notifications/lib/notificationRecipients.js +35 -0
  102. package/dist/modules/notifications/lib/notificationRecipients.js.map +7 -0
  103. package/dist/modules/notifications/lib/notificationService.js +279 -0
  104. package/dist/modules/notifications/lib/notificationService.js.map +7 -0
  105. package/dist/modules/notifications/lib/routeHelpers.js +101 -0
  106. package/dist/modules/notifications/lib/routeHelpers.js.map +7 -0
  107. package/dist/modules/notifications/lib/safeHref.js +24 -0
  108. package/dist/modules/notifications/lib/safeHref.js.map +7 -0
  109. package/dist/modules/notifications/migrations/Migration20260123000001.js +70 -0
  110. package/dist/modules/notifications/migrations/Migration20260123000001.js.map +7 -0
  111. package/dist/modules/notifications/migrations/Migration20260126150000.js +37 -0
  112. package/dist/modules/notifications/migrations/Migration20260126150000.js.map +7 -0
  113. package/dist/modules/notifications/subscribers/deliver-notification.js +139 -0
  114. package/dist/modules/notifications/subscribers/deliver-notification.js.map +7 -0
  115. package/dist/modules/notifications/workers/create-notification.worker.js +70 -0
  116. package/dist/modules/notifications/workers/create-notification.worker.js.map +7 -0
  117. package/dist/modules/sales/commands/documents.js +53 -0
  118. package/dist/modules/sales/commands/documents.js.map +2 -2
  119. package/dist/modules/sales/commands/payments.js +26 -0
  120. package/dist/modules/sales/commands/payments.js.map +2 -2
  121. package/dist/modules/sales/notifications.client.js +51 -0
  122. package/dist/modules/sales/notifications.client.js.map +7 -0
  123. package/dist/modules/sales/notifications.js +88 -0
  124. package/dist/modules/sales/notifications.js.map +7 -0
  125. package/dist/modules/sales/subscribers/quote-expiring-notification.js +38 -0
  126. package/dist/modules/sales/subscribers/quote-expiring-notification.js.map +7 -0
  127. package/dist/modules/sales/widgets/notifications/SalesOrderCreatedRenderer.js +137 -0
  128. package/dist/modules/sales/widgets/notifications/SalesOrderCreatedRenderer.js.map +7 -0
  129. package/dist/modules/sales/widgets/notifications/SalesQuoteCreatedRenderer.js +137 -0
  130. package/dist/modules/sales/widgets/notifications/SalesQuoteCreatedRenderer.js.map +7 -0
  131. package/dist/modules/sales/widgets/notifications/index.js +7 -0
  132. package/dist/modules/sales/widgets/notifications/index.js.map +7 -0
  133. package/dist/modules/sales/widgets/notifications/useSalesDocumentTotals.js +60 -0
  134. package/dist/modules/sales/widgets/notifications/useSalesDocumentTotals.js.map +7 -0
  135. package/dist/modules/staff/commands/leave-requests.js +79 -0
  136. package/dist/modules/staff/commands/leave-requests.js.map +2 -2
  137. package/dist/modules/staff/notifications.js +75 -0
  138. package/dist/modules/staff/notifications.js.map +7 -0
  139. package/dist/modules/workflows/notifications.js +28 -0
  140. package/dist/modules/workflows/notifications.js.map +7 -0
  141. package/dist/modules/workflows/subscribers/task-assigned-notification.js +38 -0
  142. package/dist/modules/workflows/subscribers/task-assigned-notification.js.map +7 -0
  143. package/generated/entities/notification/index.ts +27 -0
  144. package/generated/entities.ids.generated.ts +5 -1
  145. package/generated/entity-fields-registry.ts +2 -0
  146. package/package.json +2 -2
  147. package/src/modules/api_docs/frontend/docs/api/page.tsx +3 -2
  148. package/src/modules/auth/api/admin/nav.ts +10 -6
  149. package/src/modules/auth/api/profile/route.ts +160 -0
  150. package/src/modules/auth/api/reset/confirm.ts +25 -2
  151. package/src/modules/auth/api/reset.ts +23 -0
  152. package/src/modules/auth/api/sidebar/preferences/route.ts +21 -12
  153. package/src/modules/auth/backend/auth/profile/page.meta.ts +8 -0
  154. package/src/modules/auth/backend/auth/profile/page.tsx +127 -0
  155. package/src/modules/auth/commands/users.ts +68 -0
  156. package/src/modules/auth/i18n/de.json +29 -1
  157. package/src/modules/auth/i18n/en.json +29 -1
  158. package/src/modules/auth/i18n/es.json +29 -1
  159. package/src/modules/auth/i18n/pl.json +29 -1
  160. package/src/modules/auth/lib/setup-app.ts +1 -0
  161. package/src/modules/auth/notifications.ts +109 -0
  162. package/src/modules/auth/services/authService.ts +4 -4
  163. package/src/modules/business_rules/i18n/en.json +3 -1
  164. package/src/modules/business_rules/notifications.ts +25 -0
  165. package/src/modules/business_rules/subscribers/rule-execution-failed-notification.ts +50 -0
  166. package/src/modules/catalog/i18n/en.json +3 -1
  167. package/src/modules/catalog/notifications.ts +25 -0
  168. package/src/modules/catalog/subscribers/low-stock-notification.ts +52 -0
  169. package/src/modules/configs/cli.ts +6 -0
  170. package/src/modules/customers/commands/deals.ts +39 -0
  171. package/src/modules/customers/i18n/en.json +5 -1
  172. package/src/modules/customers/notifications.ts +44 -0
  173. package/src/modules/notifications/acl.ts +7 -0
  174. package/src/modules/notifications/api/[id]/action/route.ts +70 -0
  175. package/src/modules/notifications/api/[id]/dismiss/route.ts +12 -0
  176. package/src/modules/notifications/api/[id]/read/route.ts +12 -0
  177. package/src/modules/notifications/api/[id]/restore/route.ts +53 -0
  178. package/src/modules/notifications/api/batch/route.ts +14 -0
  179. package/src/modules/notifications/api/feature/route.ts +14 -0
  180. package/src/modules/notifications/api/mark-all-read/route.ts +34 -0
  181. package/src/modules/notifications/api/openapi.ts +52 -0
  182. package/src/modules/notifications/api/role/route.ts +14 -0
  183. package/src/modules/notifications/api/route.ts +92 -0
  184. package/src/modules/notifications/api/settings/route.ts +98 -0
  185. package/src/modules/notifications/api/unread-count/route.ts +38 -0
  186. package/src/modules/notifications/backend/config/notifications/page.meta.ts +22 -0
  187. package/src/modules/notifications/backend/config/notifications/page.tsx +12 -0
  188. package/src/modules/notifications/cli.ts +18 -0
  189. package/src/modules/notifications/data/entities.ts +99 -0
  190. package/src/modules/notifications/data/validators.ts +110 -0
  191. package/src/modules/notifications/di.ts +11 -0
  192. package/src/modules/notifications/emails/NotificationEmail.tsx +98 -0
  193. package/src/modules/notifications/frontend/NotificationInboxPageClient.tsx +42 -0
  194. package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +231 -0
  195. package/src/modules/notifications/i18n/de.json +50 -0
  196. package/src/modules/notifications/i18n/en.json +50 -0
  197. package/src/modules/notifications/i18n/es.json +50 -0
  198. package/src/modules/notifications/i18n/pl.json +50 -0
  199. package/src/modules/notifications/index.ts +12 -0
  200. package/src/modules/notifications/lib/deliveryConfig.ts +145 -0
  201. package/src/modules/notifications/lib/events.ts +48 -0
  202. package/src/modules/notifications/lib/notificationBuilder.ts +121 -0
  203. package/src/modules/notifications/lib/notificationFactory.ts +76 -0
  204. package/src/modules/notifications/lib/notificationMapper.ts +33 -0
  205. package/src/modules/notifications/lib/notificationRecipients.ts +83 -0
  206. package/src/modules/notifications/lib/notificationService.ts +414 -0
  207. package/src/modules/notifications/lib/routeHelpers.ts +151 -0
  208. package/src/modules/notifications/lib/safeHref.ts +29 -0
  209. package/src/modules/notifications/migrations/.snapshot-open-mercato.json +300 -0
  210. package/src/modules/notifications/migrations/Migration20260123000001.ts +73 -0
  211. package/src/modules/notifications/migrations/Migration20260126150000.ts +39 -0
  212. package/src/modules/notifications/subscribers/deliver-notification.ts +175 -0
  213. package/src/modules/notifications/workers/create-notification.worker.ts +122 -0
  214. package/src/modules/sales/commands/documents.ts +65 -0
  215. package/src/modules/sales/commands/payments.ts +33 -0
  216. package/src/modules/sales/i18n/de.json +20 -0
  217. package/src/modules/sales/i18n/en.json +25 -1
  218. package/src/modules/sales/i18n/es.json +20 -0
  219. package/src/modules/sales/i18n/pl.json +20 -0
  220. package/src/modules/sales/notifications.client.ts +65 -0
  221. package/src/modules/sales/notifications.ts +82 -0
  222. package/src/modules/sales/subscribers/quote-expiring-notification.ts +53 -0
  223. package/src/modules/sales/widgets/notifications/SalesOrderCreatedRenderer.tsx +156 -0
  224. package/src/modules/sales/widgets/notifications/SalesQuoteCreatedRenderer.tsx +156 -0
  225. package/src/modules/sales/widgets/notifications/index.ts +2 -0
  226. package/src/modules/sales/widgets/notifications/useSalesDocumentTotals.ts +81 -0
  227. package/src/modules/staff/commands/leave-requests.ts +94 -0
  228. package/src/modules/staff/i18n/de.json +4 -0
  229. package/src/modules/staff/i18n/en.json +9 -1
  230. package/src/modules/staff/i18n/es.json +4 -0
  231. package/src/modules/staff/i18n/pl.json +4 -0
  232. package/src/modules/staff/notifications.ts +71 -0
  233. package/src/modules/workflows/i18n/en.json +3 -1
  234. package/src/modules/workflows/notifications.ts +25 -0
  235. package/src/modules/workflows/subscribers/task-assigned-notification.ts +53 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/notifications/migrations/Migration20260126150000.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations'\n\nexport class Migration20260126150000 extends Migration {\n async up(): Promise<void> {\n // Add i18n support fields to notifications table\n this.addSql(`\n alter table \"notifications\"\n add column if not exists \"title_key\" text,\n add column if not exists \"body_key\" text,\n add column if not exists \"title_variables\" jsonb,\n add column if not exists \"body_variables\" jsonb;\n `)\n\n // Add comments for clarity\n this.addSql(`\n comment on column \"notifications\".\"title_key\" is 'i18n key for notification title';\n `)\n this.addSql(`\n comment on column \"notifications\".\"body_key\" is 'i18n key for notification body';\n `)\n this.addSql(`\n comment on column \"notifications\".\"title_variables\" is 'Variables for i18n interpolation in title';\n `)\n this.addSql(`\n comment on column \"notifications\".\"body_variables\" is 'Variables for i18n interpolation in body';\n `)\n }\n\n async down(): Promise<void> {\n // Remove i18n support fields\n this.addSql(`\n alter table \"notifications\"\n drop column if exists \"title_key\",\n drop column if exists \"body_key\",\n drop column if exists \"title_variables\",\n drop column if exists \"body_variables\";\n `)\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,gCAAgC,UAAU;AAAA,EACrD,MAAM,KAAoB;AAExB,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAMX;AAGD,SAAK,OAAO;AAAA;AAAA,KAEX;AACD,SAAK,OAAO;AAAA;AAAA,KAEX;AACD,SAAK,OAAO;AAAA;AAAA,KAEX;AACD,SAAK,OAAO;AAAA;AAAA,KAEX;AAAA,EACH;AAAA,EAEA,MAAM,OAAsB;AAE1B,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAMX;AAAA,EACH;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,139 @@
1
+ import { Notification } from "../data/entities.js";
2
+ import { NOTIFICATION_EVENTS } from "../lib/events.js";
3
+ import { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, resolveNotificationDeliveryConfig, resolveNotificationPanelUrl } from "../lib/deliveryConfig.js";
4
+ import { sendEmail } from "@open-mercato/shared/lib/email/send";
5
+ import NotificationEmail from "../emails/NotificationEmail.js";
6
+ import { loadDictionary } from "@open-mercato/shared/lib/i18n/server";
7
+ import { createFallbackTranslator } from "@open-mercato/shared/lib/i18n/translate";
8
+ import { defaultLocale } from "@open-mercato/shared/lib/i18n/config";
9
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
10
+ import { User } from "../../auth/data/entities.js";
11
+ const metadata = {
12
+ event: NOTIFICATION_EVENTS.CREATED,
13
+ persistent: true,
14
+ id: "notifications:deliver"
15
+ };
16
+ const DEBUG = process.env.NOTIFICATIONS_DEBUG === "true";
17
+ function debug(...args) {
18
+ if (DEBUG) {
19
+ console.log("[notifications]", ...args);
20
+ }
21
+ }
22
+ const buildPanelLink = (panelUrl, notificationId) => {
23
+ if (panelUrl.startsWith("http://") || panelUrl.startsWith("https://")) {
24
+ const url = new URL(panelUrl);
25
+ url.searchParams.set("notificationId", notificationId);
26
+ return url.toString();
27
+ }
28
+ const separator = panelUrl.includes("?") ? "&" : "?";
29
+ return `${panelUrl}${separator}notificationId=${encodeURIComponent(notificationId)}`;
30
+ };
31
+ const resolveNotificationCopy = async (notification) => {
32
+ const dict = await loadDictionary(defaultLocale);
33
+ const t = createFallbackTranslator(dict);
34
+ const title = notification.titleKey ? t(notification.titleKey, notification.title ?? notification.titleKey, notification.titleVariables ?? void 0) : notification.title;
35
+ const body = notification.bodyKey ? t(notification.bodyKey, notification.body ?? notification.bodyKey ?? "", notification.bodyVariables ?? void 0) : notification.body ?? null;
36
+ return { title, body, t };
37
+ };
38
+ const resolveRecipient = async (em, notification, encryptionService) => {
39
+ const where = {
40
+ id: notification.recipientUserId,
41
+ tenantId: notification.tenantId,
42
+ deletedAt: null
43
+ };
44
+ if (notification.organizationId) {
45
+ where.organizationId = notification.organizationId;
46
+ }
47
+ const record = await findOneWithDecryption(
48
+ em,
49
+ User,
50
+ where,
51
+ void 0,
52
+ {
53
+ tenantId: notification.tenantId,
54
+ organizationId: notification.organizationId ?? null,
55
+ encryptionService: encryptionService ?? null
56
+ }
57
+ );
58
+ if (!record) return null;
59
+ return {
60
+ email: typeof record.email === "string" ? record.email : null,
61
+ name: typeof record.name === "string" ? record.name : null
62
+ };
63
+ };
64
+ async function handle(payload, ctx) {
65
+ debug("deliver notification event", payload);
66
+ const deliveryConfig = await resolveNotificationDeliveryConfig(ctx, { defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG });
67
+ if (!deliveryConfig.strategies.email.enabled) {
68
+ debug("email delivery disabled");
69
+ return;
70
+ }
71
+ const em = ctx.resolve("em");
72
+ const notification = await em.findOne(Notification, {
73
+ id: payload.notificationId,
74
+ tenantId: payload.tenantId,
75
+ organizationId: payload.organizationId ?? null
76
+ });
77
+ if (!notification) {
78
+ debug("notification not found", payload.notificationId);
79
+ return;
80
+ }
81
+ let encryptionService = null;
82
+ try {
83
+ encryptionService = ctx.resolve("tenantEncryptionService");
84
+ } catch {
85
+ encryptionService = null;
86
+ }
87
+ const recipient = await resolveRecipient(em, notification, encryptionService);
88
+ if (!recipient?.email) {
89
+ debug("recipient has no email", notification.recipientUserId);
90
+ }
91
+ const { title, body, t } = await resolveNotificationCopy(notification);
92
+ const panelUrl = resolveNotificationPanelUrl(deliveryConfig);
93
+ if (!panelUrl) {
94
+ debug("missing panelUrl; check appUrl/panelPath settings");
95
+ return;
96
+ }
97
+ const panelLink = buildPanelLink(panelUrl, notification.id);
98
+ const actionLinks = (notification.actionData?.actions ?? []).map((action) => ({
99
+ id: action.id,
100
+ label: action.labelKey ? t(action.labelKey, action.label) : action.label,
101
+ href: panelLink
102
+ }));
103
+ if (deliveryConfig.strategies.email.enabled && recipient?.email) {
104
+ const subjectPrefix = deliveryConfig.strategies.email.subjectPrefix?.trim();
105
+ const subject = subjectPrefix ? `${subjectPrefix} ${title}` : title;
106
+ const copy = {
107
+ preview: t("notifications.delivery.email.preview", "New notification"),
108
+ heading: t("notifications.delivery.email.heading", "You have a new notification"),
109
+ bodyIntro: t("notifications.delivery.email.bodyIntro", "Review the notification details and take any required actions."),
110
+ actionNotice: t("notifications.delivery.email.actionNotice", "Actions are available in Open Mercato and are read-only in this email."),
111
+ openCta: t("notifications.delivery.email.openCta", "Open notification center"),
112
+ footer: t("notifications.delivery.email.footer", "Open Mercato notifications")
113
+ };
114
+ try {
115
+ debug("sending email", { to: recipient.email, from: deliveryConfig.strategies.email.from, subject });
116
+ await sendEmail({
117
+ to: recipient.email,
118
+ subject,
119
+ from: deliveryConfig.strategies.email.from,
120
+ replyTo: deliveryConfig.strategies.email.replyTo,
121
+ react: NotificationEmail({
122
+ title,
123
+ body,
124
+ actions: actionLinks,
125
+ panelUrl: panelLink,
126
+ copy
127
+ })
128
+ });
129
+ } catch (error) {
130
+ console.error("[notifications] email delivery failed", error);
131
+ }
132
+ }
133
+ return;
134
+ }
135
+ export {
136
+ handle as default,
137
+ metadata
138
+ };
139
+ //# sourceMappingURL=deliver-notification.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/notifications/subscribers/deliver-notification.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Notification } from '../data/entities'\nimport { NOTIFICATION_EVENTS } from '../lib/events'\nimport { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, resolveNotificationDeliveryConfig, resolveNotificationPanelUrl } from '../lib/deliveryConfig'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport NotificationEmail from '../emails/NotificationEmail'\nimport { loadDictionary } from '@open-mercato/shared/lib/i18n/server'\nimport { createFallbackTranslator } from '@open-mercato/shared/lib/i18n/translate'\nimport { defaultLocale } from '@open-mercato/shared/lib/i18n/config'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { User } from '../../auth/data/entities'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\n\nexport const metadata = {\n event: NOTIFICATION_EVENTS.CREATED,\n persistent: true,\n id: 'notifications:deliver',\n}\n\nconst DEBUG = process.env.NOTIFICATIONS_DEBUG === 'true'\n\nfunction debug(...args: unknown[]): void {\n if (DEBUG) {\n console.log('[notifications]', ...args)\n }\n}\n\ntype NotificationCreatedPayload = {\n notificationId: string\n recipientUserId: string\n tenantId: string\n organizationId?: string | null\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nconst buildPanelLink = (panelUrl: string, notificationId: string) => {\n if (panelUrl.startsWith('http://') || panelUrl.startsWith('https://')) {\n const url = new URL(panelUrl)\n url.searchParams.set('notificationId', notificationId)\n return url.toString()\n }\n const separator = panelUrl.includes('?') ? '&' : '?'\n return `${panelUrl}${separator}notificationId=${encodeURIComponent(notificationId)}`\n}\n\nconst resolveNotificationCopy = async (\n notification: Notification\n) => {\n const dict = await loadDictionary(defaultLocale)\n const t = createFallbackTranslator(dict)\n\n const title = notification.titleKey\n ? t(notification.titleKey, notification.title ?? notification.titleKey, notification.titleVariables ?? undefined)\n : notification.title\n\n const body = notification.bodyKey\n ? t(notification.bodyKey, notification.body ?? notification.bodyKey ?? '', notification.bodyVariables ?? undefined)\n : notification.body ?? null\n\n return { title, body, t }\n}\n\nconst resolveRecipient = async (\n em: EntityManager,\n notification: Notification,\n encryptionService?: TenantDataEncryptionService | null,\n) => {\n const where: Partial<User> & { deletedAt?: null } = {\n id: notification.recipientUserId,\n tenantId: notification.tenantId,\n deletedAt: null,\n }\n if (notification.organizationId) {\n where.organizationId = notification.organizationId\n }\n const record = await findOneWithDecryption(\n em,\n User,\n where,\n undefined,\n {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n encryptionService: encryptionService ?? null,\n },\n )\n if (!record) return null\n return {\n email: typeof record.email === 'string' ? record.email : null,\n name: typeof record.name === 'string' ? record.name : null,\n }\n}\n\n\nexport default async function handle(payload: NotificationCreatedPayload, ctx: ResolverContext) {\n debug('deliver notification event', payload)\n const deliveryConfig = await resolveNotificationDeliveryConfig(ctx, { defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG })\n if (!deliveryConfig.strategies.email.enabled) {\n debug('email delivery disabled')\n return\n }\n\n const em = ctx.resolve('em') as EntityManager\n const notification = await em.findOne(Notification, {\n id: payload.notificationId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n if (!notification) {\n debug('notification not found', payload.notificationId)\n return\n }\n\n let encryptionService: TenantDataEncryptionService | null = null\n try {\n encryptionService = ctx.resolve<TenantDataEncryptionService>('tenantEncryptionService')\n } catch {\n encryptionService = null\n }\n\n const recipient = await resolveRecipient(em, notification, encryptionService)\n if (!recipient?.email) {\n debug('recipient has no email', notification.recipientUserId)\n }\n const { title, body, t } = await resolveNotificationCopy(notification)\n const panelUrl = resolveNotificationPanelUrl(deliveryConfig)\n if (!panelUrl) {\n debug('missing panelUrl; check appUrl/panelPath settings')\n return\n }\n\n const panelLink = buildPanelLink(panelUrl, notification.id)\n const actionLinks = (notification.actionData?.actions ?? []).map((action) => ({\n id: action.id,\n label: action.labelKey ? t(action.labelKey, action.label) : action.label,\n href: panelLink,\n }))\n\n if (deliveryConfig.strategies.email.enabled && recipient?.email) {\n const subjectPrefix = deliveryConfig.strategies.email.subjectPrefix?.trim()\n const subject = subjectPrefix ? `${subjectPrefix} ${title}` : title\n const copy = {\n preview: t('notifications.delivery.email.preview', 'New notification'),\n heading: t('notifications.delivery.email.heading', 'You have a new notification'),\n bodyIntro: t('notifications.delivery.email.bodyIntro', 'Review the notification details and take any required actions.'),\n actionNotice: t('notifications.delivery.email.actionNotice', 'Actions are available in Open Mercato and are read-only in this email.'),\n openCta: t('notifications.delivery.email.openCta', 'Open notification center'),\n footer: t('notifications.delivery.email.footer', 'Open Mercato notifications'),\n }\n\n try {\n debug('sending email', { to: recipient.email, from: deliveryConfig.strategies.email.from, subject })\n await sendEmail({\n to: recipient.email,\n subject,\n from: deliveryConfig.strategies.email.from,\n replyTo: deliveryConfig.strategies.email.replyTo,\n react: NotificationEmail({\n title,\n body,\n actions: actionLinks,\n panelUrl: panelLink,\n copy,\n }),\n })\n } catch (error) {\n console.error('[notifications] email delivery failed', error)\n }\n }\n\n return\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,70 @@
1
+ import { buildNotificationEntity, emitNotificationCreated, emitNotificationCreatedBatch } from "../lib/notificationFactory.js";
2
+ import { getRecipientUserIdsForFeature, getRecipientUserIdsForRole } from "../lib/notificationRecipients.js";
3
+ function getKnex(em) {
4
+ return em.getConnection().getKnex();
5
+ }
6
+ const NOTIFICATIONS_QUEUE_NAME = "notifications";
7
+ const metadata = {
8
+ queue: NOTIFICATIONS_QUEUE_NAME,
9
+ id: "notifications:create",
10
+ concurrency: 5
11
+ };
12
+ async function handle(job, ctx) {
13
+ const { payload } = job;
14
+ if (payload.type === "create") {
15
+ const em = ctx.resolve("em").fork();
16
+ const eventBus = ctx.resolve("eventBus");
17
+ const { input, tenantId, organizationId } = payload;
18
+ const { recipientUserId, ...content } = input;
19
+ const notification = buildNotificationEntity(em, content, recipientUserId, { tenantId, organizationId });
20
+ await em.persistAndFlush(notification);
21
+ await emitNotificationCreated(eventBus, notification, { tenantId, organizationId });
22
+ } else if (payload.type === "create-role") {
23
+ const em = ctx.resolve("em").fork();
24
+ const eventBus = ctx.resolve("eventBus");
25
+ const { input, tenantId, organizationId } = payload;
26
+ const knex = getKnex(em);
27
+ const recipientUserIds = await getRecipientUserIdsForRole(knex, tenantId, input.roleId);
28
+ if (recipientUserIds.length === 0) {
29
+ return;
30
+ }
31
+ const { roleId: _roleId, ...content } = input;
32
+ const notifications = [];
33
+ for (const recipientUserId of recipientUserIds) {
34
+ const notification = buildNotificationEntity(em, content, recipientUserId, { tenantId, organizationId });
35
+ notifications.push(notification);
36
+ }
37
+ await em.persistAndFlush(notifications);
38
+ await emitNotificationCreatedBatch(eventBus, notifications, { tenantId, organizationId });
39
+ } else if (payload.type === "create-feature") {
40
+ const em = ctx.resolve("em").fork();
41
+ const eventBus = ctx.resolve("eventBus");
42
+ const { input, tenantId, organizationId } = payload;
43
+ const knex = getKnex(em);
44
+ const recipientUserIds = await getRecipientUserIdsForFeature(knex, tenantId, input.requiredFeature);
45
+ if (recipientUserIds.length === 0) {
46
+ return;
47
+ }
48
+ const notifications = [];
49
+ const { requiredFeature: _requiredFeature, ...content } = input;
50
+ for (const recipientUserId of recipientUserIds) {
51
+ const notification = buildNotificationEntity(em, content, recipientUserId, { tenantId, organizationId });
52
+ notifications.push(notification);
53
+ }
54
+ await em.persistAndFlush(notifications);
55
+ await emitNotificationCreatedBatch(eventBus, notifications, { tenantId, organizationId });
56
+ } else if (payload.type === "cleanup-expired") {
57
+ const em = ctx.resolve("em").fork();
58
+ const knex = getKnex(em);
59
+ await knex("notifications").where("expires_at", "<", knex.fn.now()).whereNotIn("status", ["actioned", "dismissed"]).update({
60
+ status: "dismissed",
61
+ dismissed_at: knex.fn.now()
62
+ });
63
+ }
64
+ }
65
+ export {
66
+ NOTIFICATIONS_QUEUE_NAME,
67
+ handle as default,
68
+ metadata
69
+ };
70
+ //# sourceMappingURL=create-notification.worker.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/notifications/workers/create-notification.worker.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport type { Knex } from 'knex'\nimport { Notification } from '../data/entities'\nimport type { CreateNotificationInput, CreateRoleNotificationInput, CreateFeatureNotificationInput } from '../data/validators'\nimport { buildNotificationEntity, emitNotificationCreated, emitNotificationCreatedBatch } from '../lib/notificationFactory'\nimport { getRecipientUserIdsForFeature, getRecipientUserIdsForRole } from '../lib/notificationRecipients'\n\nfunction getKnex(em: EntityManager): Knex {\n return (em.getConnection() as unknown as { getKnex: () => Knex }).getKnex()\n}\n\nexport const NOTIFICATIONS_QUEUE_NAME = 'notifications'\n\nexport type CreateNotificationJob = {\n type: 'create'\n input: CreateNotificationInput\n tenantId: string\n organizationId?: string | null\n}\n\nexport type CreateRoleNotificationJob = {\n type: 'create-role'\n input: CreateRoleNotificationInput\n tenantId: string\n organizationId?: string | null\n}\n\nexport type CreateFeatureNotificationJob = {\n type: 'create-feature'\n input: CreateFeatureNotificationInput\n tenantId: string\n organizationId?: string | null\n}\n\nexport type CleanupExpiredJob = {\n type: 'cleanup-expired'\n}\n\nexport type NotificationJob = CreateNotificationJob | CreateRoleNotificationJob | CreateFeatureNotificationJob | CleanupExpiredJob\n\nexport const metadata = {\n queue: NOTIFICATIONS_QUEUE_NAME,\n id: 'notifications:create',\n concurrency: 5,\n}\n\ntype HandlerContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport default async function handle(\n job: { payload: NotificationJob },\n ctx: HandlerContext\n): Promise<void> {\n const { payload } = job\n\n if (payload.type === 'create') {\n const em = (ctx.resolve('em') as EntityManager).fork()\n const eventBus = ctx.resolve('eventBus') as { emit: (event: string, payload: unknown) => Promise<void> }\n const { input, tenantId, organizationId } = payload\n const { recipientUserId, ...content } = input\n const notification = buildNotificationEntity(em, content, recipientUserId, { tenantId, organizationId })\n\n await em.persistAndFlush(notification)\n\n await emitNotificationCreated(eventBus, notification, { tenantId, organizationId })\n } else if (payload.type === 'create-role') {\n const em = (ctx.resolve('em') as EntityManager).fork()\n const eventBus = ctx.resolve('eventBus') as { emit: (event: string, payload: unknown) => Promise<void> }\n const { input, tenantId, organizationId } = payload\n\n const knex = getKnex(em)\n const recipientUserIds = await getRecipientUserIdsForRole(knex, tenantId, input.roleId)\n if (recipientUserIds.length === 0) {\n return\n }\n\n const { roleId: _roleId, ...content } = input\n const notifications: Notification[] = []\n for (const recipientUserId of recipientUserIds) {\n const notification = buildNotificationEntity(em, content, recipientUserId, { tenantId, organizationId })\n notifications.push(notification)\n }\n\n await em.persistAndFlush(notifications)\n\n await emitNotificationCreatedBatch(eventBus, notifications, { tenantId, organizationId })\n } else if (payload.type === 'create-feature') {\n const em = (ctx.resolve('em') as EntityManager).fork()\n const eventBus = ctx.resolve('eventBus') as { emit: (event: string, payload: unknown) => Promise<void> }\n const { input, tenantId, organizationId } = payload\n\n const knex = getKnex(em)\n const recipientUserIds = await getRecipientUserIdsForFeature(knex, tenantId, input.requiredFeature)\n\n if (recipientUserIds.length === 0) {\n return\n }\n\n const notifications: Notification[] = []\n const { requiredFeature: _requiredFeature, ...content } = input\n for (const recipientUserId of recipientUserIds) {\n const notification = buildNotificationEntity(em, content, recipientUserId, { tenantId, organizationId })\n notifications.push(notification)\n }\n\n await em.persistAndFlush(notifications)\n\n await emitNotificationCreatedBatch(eventBus, notifications, { tenantId, organizationId })\n } else if (payload.type === 'cleanup-expired') {\n const em = (ctx.resolve('em') as EntityManager).fork()\n const knex = getKnex(em)\n\n await knex('notifications')\n .where('expires_at', '<', knex.fn.now())\n .whereNotIn('status', ['actioned', 'dismissed'])\n .update({\n status: 'dismissed',\n dismissed_at: knex.fn.now(),\n })\n }\n}\n"],
5
+ "mappings": "AAIA,SAAS,yBAAyB,yBAAyB,oCAAoC;AAC/F,SAAS,+BAA+B,kCAAkC;AAE1E,SAAS,QAAQ,IAAyB;AACxC,SAAQ,GAAG,cAAc,EAAyC,QAAQ;AAC5E;AAEO,MAAM,2BAA2B;AA6BjC,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AACf;AAMA,eAAO,OACL,KACA,KACe;AACf,QAAM,EAAE,QAAQ,IAAI;AAEpB,MAAI,QAAQ,SAAS,UAAU;AAC7B,UAAM,KAAM,IAAI,QAAQ,IAAI,EAAoB,KAAK;AACrD,UAAM,WAAW,IAAI,QAAQ,UAAU;AACvC,UAAM,EAAE,OAAO,UAAU,eAAe,IAAI;AAC5C,UAAM,EAAE,iBAAiB,GAAG,QAAQ,IAAI;AACxC,UAAM,eAAe,wBAAwB,IAAI,SAAS,iBAAiB,EAAE,UAAU,eAAe,CAAC;AAEvG,UAAM,GAAG,gBAAgB,YAAY;AAErC,UAAM,wBAAwB,UAAU,cAAc,EAAE,UAAU,eAAe,CAAC;AAAA,EACpF,WAAW,QAAQ,SAAS,eAAe;AACzC,UAAM,KAAM,IAAI,QAAQ,IAAI,EAAoB,KAAK;AACrD,UAAM,WAAW,IAAI,QAAQ,UAAU;AACvC,UAAM,EAAE,OAAO,UAAU,eAAe,IAAI;AAE5C,UAAM,OAAO,QAAQ,EAAE;AACvB,UAAM,mBAAmB,MAAM,2BAA2B,MAAM,UAAU,MAAM,MAAM;AACtF,QAAI,iBAAiB,WAAW,GAAG;AACjC;AAAA,IACF;AAEA,UAAM,EAAE,QAAQ,SAAS,GAAG,QAAQ,IAAI;AACxC,UAAM,gBAAgC,CAAC;AACvC,eAAW,mBAAmB,kBAAkB;AAC9C,YAAM,eAAe,wBAAwB,IAAI,SAAS,iBAAiB,EAAE,UAAU,eAAe,CAAC;AACvG,oBAAc,KAAK,YAAY;AAAA,IACjC;AAEA,UAAM,GAAG,gBAAgB,aAAa;AAEtC,UAAM,6BAA6B,UAAU,eAAe,EAAE,UAAU,eAAe,CAAC;AAAA,EAC1F,WAAW,QAAQ,SAAS,kBAAkB;AAC5C,UAAM,KAAM,IAAI,QAAQ,IAAI,EAAoB,KAAK;AACrD,UAAM,WAAW,IAAI,QAAQ,UAAU;AACvC,UAAM,EAAE,OAAO,UAAU,eAAe,IAAI;AAE5C,UAAM,OAAO,QAAQ,EAAE;AACvB,UAAM,mBAAmB,MAAM,8BAA8B,MAAM,UAAU,MAAM,eAAe;AAElG,QAAI,iBAAiB,WAAW,GAAG;AACjC;AAAA,IACF;AAEA,UAAM,gBAAgC,CAAC;AACvC,UAAM,EAAE,iBAAiB,kBAAkB,GAAG,QAAQ,IAAI;AAC1D,eAAW,mBAAmB,kBAAkB;AAC9C,YAAM,eAAe,wBAAwB,IAAI,SAAS,iBAAiB,EAAE,UAAU,eAAe,CAAC;AACvG,oBAAc,KAAK,YAAY;AAAA,IACjC;AAEA,UAAM,GAAG,gBAAgB,aAAa;AAEtC,UAAM,6BAA6B,UAAU,eAAe,EAAE,UAAU,eAAe,CAAC;AAAA,EAC1F,WAAW,QAAQ,SAAS,mBAAmB;AAC7C,UAAM,KAAM,IAAI,QAAQ,IAAI,EAAoB,KAAK;AACrD,UAAM,OAAO,QAAQ,EAAE;AAEvB,UAAM,KAAK,eAAe,EACvB,MAAM,cAAc,KAAK,KAAK,GAAG,IAAI,CAAC,EACtC,WAAW,UAAU,CAAC,YAAY,WAAW,CAAC,EAC9C,OAAO;AAAA,MACN,QAAQ;AAAA,MACR,cAAc,KAAK,GAAG,IAAI;AAAA,IAC5B,CAAC;AAAA,EACL;AACF;",
6
+ "names": []
7
+ }
@@ -5,6 +5,8 @@ import { emitCrudSideEffects, requireId } from "@open-mercato/shared/lib/command
5
5
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
6
6
  import { deriveResourceFromCommandId, invalidateCrudCache } from "@open-mercato/shared/lib/crud/cache";
7
7
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
8
+ import { resolveNotificationService } from "../../notifications/lib/notificationService.js";
9
+ import { buildFeatureNotificationFromType } from "../../notifications/lib/notificationBuilder.js";
8
10
  import { setRecordCustomFields } from "@open-mercato/core/modules/entities/lib/helpers";
9
11
  import { loadCustomFieldValues } from "@open-mercato/shared/lib/crud/custom-fields";
10
12
  import { normalizeCustomFieldValues } from "@open-mercato/shared/lib/custom-fields/normalize";
@@ -61,6 +63,7 @@ import {
61
63
  import { resolveDictionaryEntryValue } from "../lib/dictionaries.js";
62
64
  import { resolveStatusEntryIdByValue } from "../lib/statusHelpers.js";
63
65
  import { loadSalesSettings } from "./settings.js";
66
+ import { notificationTypes } from "../notifications.js";
64
67
  const currencyCodeSchema = z.string().trim().toUpperCase().regex(/^[A-Z]{3}$/, { message: "currency_code_invalid" });
65
68
  const dateOnlySchema = z.string().trim().regex(/^\d{4}-\d{2}-\d{2}$/, { message: "invalid_date" }).refine((value) => !Number.isNaN(new Date(value).getTime()), { message: "invalid_date" });
66
69
  const addressSnapshotSchema = z.record(z.string(), z.unknown()).nullable().optional();
@@ -2322,6 +2325,31 @@ const createQuoteCommand = {
2322
2325
  tagIds: parsed.tags
2323
2326
  });
2324
2327
  await em.flush();
2328
+ try {
2329
+ const notificationService = resolveNotificationService(ctx.container);
2330
+ const typeDef = notificationTypes.find((type) => type.type === "sales.quote.created");
2331
+ if (typeDef) {
2332
+ const totalAmount = quote.grandTotalGrossAmount && quote.currencyCode ? `${quote.grandTotalGrossAmount} ${quote.currencyCode}` : "";
2333
+ const totalDisplay = totalAmount ? ` (${totalAmount})` : "";
2334
+ const notificationInput = buildFeatureNotificationFromType(typeDef, {
2335
+ requiredFeature: "sales.quotes.manage",
2336
+ bodyVariables: {
2337
+ quoteNumber: quote.quoteNumber,
2338
+ total: totalDisplay,
2339
+ totalAmount
2340
+ },
2341
+ sourceEntityType: "sales:quote",
2342
+ sourceEntityId: quote.id,
2343
+ linkHref: `/backend/sales/quotes/${quote.id}`
2344
+ });
2345
+ await notificationService.createForFeature(notificationInput, {
2346
+ tenantId: quote.tenantId,
2347
+ organizationId: quote.organizationId ?? null
2348
+ });
2349
+ }
2350
+ } catch (err) {
2351
+ console.error("[sales.quotes.create] Failed to create notification:", err);
2352
+ }
2325
2353
  return { quoteId: quote.id };
2326
2354
  },
2327
2355
  captureAfter: async (_input, result, ctx) => {
@@ -2938,6 +2966,31 @@ const createOrderCommand = {
2938
2966
  tagIds: parsed.tags
2939
2967
  });
2940
2968
  await em.flush();
2969
+ try {
2970
+ const notificationService = resolveNotificationService(ctx.container);
2971
+ const typeDef = notificationTypes.find((type) => type.type === "sales.order.created");
2972
+ if (typeDef) {
2973
+ const totalAmount = order.grandTotalGrossAmount && order.currencyCode ? `${order.grandTotalGrossAmount} ${order.currencyCode}` : "";
2974
+ const totalDisplay = totalAmount ? ` (${totalAmount})` : "";
2975
+ const notificationInput = buildFeatureNotificationFromType(typeDef, {
2976
+ requiredFeature: "sales.orders.manage",
2977
+ bodyVariables: {
2978
+ orderNumber: order.orderNumber,
2979
+ total: totalDisplay,
2980
+ totalAmount
2981
+ },
2982
+ sourceEntityType: "sales:order",
2983
+ sourceEntityId: order.id,
2984
+ linkHref: `/backend/sales/orders/${order.id}`
2985
+ });
2986
+ await notificationService.createForFeature(notificationInput, {
2987
+ tenantId: order.tenantId,
2988
+ organizationId: order.organizationId ?? null
2989
+ });
2990
+ }
2991
+ } catch (err) {
2992
+ console.error("[sales.orders.create] Failed to create notification:", err);
2993
+ }
2941
2994
  return { orderId: order.id };
2942
2995
  },
2943
2996
  captureAfter: async (_input, result, ctx) => {