@endora-commerce/mod-newsletter 0.100.0

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 (239) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/dist/admin/api/newsletter-client.d.ts +64 -0
  4. package/dist/admin/api/newsletter-client.d.ts.map +1 -0
  5. package/dist/admin/api/newsletter-client.js +71 -0
  6. package/dist/admin/api/newsletter-client.js.map +1 -0
  7. package/dist/admin/email-variables.d.ts +21 -0
  8. package/dist/admin/email-variables.d.ts.map +1 -0
  9. package/dist/admin/email-variables.js +48 -0
  10. package/dist/admin/email-variables.js.map +1 -0
  11. package/dist/admin/index.d.ts +44 -0
  12. package/dist/admin/index.d.ts.map +1 -0
  13. package/dist/admin/index.js +149 -0
  14. package/dist/admin/index.js.map +1 -0
  15. package/dist/admin/pages/AutomationBuilder.d.ts +8 -0
  16. package/dist/admin/pages/AutomationBuilder.d.ts.map +1 -0
  17. package/dist/admin/pages/AutomationBuilder.js +131 -0
  18. package/dist/admin/pages/AutomationBuilder.js.map +1 -0
  19. package/dist/admin/pages/AutomationsPage.d.ts +8 -0
  20. package/dist/admin/pages/AutomationsPage.d.ts.map +1 -0
  21. package/dist/admin/pages/AutomationsPage.js +43 -0
  22. package/dist/admin/pages/AutomationsPage.js.map +1 -0
  23. package/dist/admin/pages/BlocksPage.d.ts +8 -0
  24. package/dist/admin/pages/BlocksPage.d.ts.map +1 -0
  25. package/dist/admin/pages/BlocksPage.js +110 -0
  26. package/dist/admin/pages/BlocksPage.js.map +1 -0
  27. package/dist/admin/pages/CampaignEditor.d.ts +8 -0
  28. package/dist/admin/pages/CampaignEditor.d.ts.map +1 -0
  29. package/dist/admin/pages/CampaignEditor.js +144 -0
  30. package/dist/admin/pages/CampaignEditor.js.map +1 -0
  31. package/dist/admin/pages/CampaignStats.d.ts +8 -0
  32. package/dist/admin/pages/CampaignStats.d.ts.map +1 -0
  33. package/dist/admin/pages/CampaignStats.js +45 -0
  34. package/dist/admin/pages/CampaignStats.js.map +1 -0
  35. package/dist/admin/pages/CampaignsPage.d.ts +8 -0
  36. package/dist/admin/pages/CampaignsPage.d.ts.map +1 -0
  37. package/dist/admin/pages/CampaignsPage.js +28 -0
  38. package/dist/admin/pages/CampaignsPage.js.map +1 -0
  39. package/dist/admin/pages/ProviderSettingsPage.d.ts +8 -0
  40. package/dist/admin/pages/ProviderSettingsPage.d.ts.map +1 -0
  41. package/dist/admin/pages/ProviderSettingsPage.js +64 -0
  42. package/dist/admin/pages/ProviderSettingsPage.js.map +1 -0
  43. package/dist/admin/pages/SubscribersPage.d.ts +8 -0
  44. package/dist/admin/pages/SubscribersPage.d.ts.map +1 -0
  45. package/dist/admin/pages/SubscribersPage.js +55 -0
  46. package/dist/admin/pages/SubscribersPage.js.map +1 -0
  47. package/dist/admin/pages/TagsPage.d.ts +8 -0
  48. package/dist/admin/pages/TagsPage.d.ts.map +1 -0
  49. package/dist/admin/pages/TagsPage.js +114 -0
  50. package/dist/admin/pages/TagsPage.js.map +1 -0
  51. package/dist/backend/entities/newsletter-automation-run.entity.d.ts +19 -0
  52. package/dist/backend/entities/newsletter-automation-run.entity.d.ts.map +1 -0
  53. package/dist/backend/entities/newsletter-automation-run.entity.js +72 -0
  54. package/dist/backend/entities/newsletter-automation-run.entity.js.map +1 -0
  55. package/dist/backend/entities/newsletter-automation.entity.d.ts +22 -0
  56. package/dist/backend/entities/newsletter-automation.entity.d.ts.map +1 -0
  57. package/dist/backend/entities/newsletter-automation.entity.js +86 -0
  58. package/dist/backend/entities/newsletter-automation.entity.js.map +1 -0
  59. package/dist/backend/entities/newsletter-campaign-subscriber.entity.d.ts +6 -0
  60. package/dist/backend/entities/newsletter-campaign-subscriber.entity.d.ts.map +1 -0
  61. package/dist/backend/entities/newsletter-campaign-subscriber.entity.js +30 -0
  62. package/dist/backend/entities/newsletter-campaign-subscriber.entity.js.map +1 -0
  63. package/dist/backend/entities/newsletter-campaign.entity.d.ts +22 -0
  64. package/dist/backend/entities/newsletter-campaign.entity.d.ts.map +1 -0
  65. package/dist/backend/entities/newsletter-campaign.entity.js +102 -0
  66. package/dist/backend/entities/newsletter-campaign.entity.js.map +1 -0
  67. package/dist/backend/entities/newsletter-custom-field.entity.d.ts +12 -0
  68. package/dist/backend/entities/newsletter-custom-field.entity.d.ts.map +1 -0
  69. package/dist/backend/entities/newsletter-custom-field.entity.js +53 -0
  70. package/dist/backend/entities/newsletter-custom-field.entity.js.map +1 -0
  71. package/dist/backend/entities/newsletter-email-block-sales-channel.entity.d.ts +7 -0
  72. package/dist/backend/entities/newsletter-email-block-sales-channel.entity.d.ts.map +1 -0
  73. package/dist/backend/entities/newsletter-email-block-sales-channel.entity.js +35 -0
  74. package/dist/backend/entities/newsletter-email-block-sales-channel.entity.js.map +1 -0
  75. package/dist/backend/entities/newsletter-email-block.entity.d.ts +21 -0
  76. package/dist/backend/entities/newsletter-email-block.entity.d.ts.map +1 -0
  77. package/dist/backend/entities/newsletter-email-block.entity.js +78 -0
  78. package/dist/backend/entities/newsletter-email-block.entity.js.map +1 -0
  79. package/dist/backend/entities/newsletter-engagement-event.entity.d.ts +12 -0
  80. package/dist/backend/entities/newsletter-engagement-event.entity.d.ts.map +1 -0
  81. package/dist/backend/entities/newsletter-engagement-event.entity.js +54 -0
  82. package/dist/backend/entities/newsletter-engagement-event.entity.js.map +1 -0
  83. package/dist/backend/entities/newsletter-send-record.entity.d.ts +23 -0
  84. package/dist/backend/entities/newsletter-send-record.entity.d.ts.map +1 -0
  85. package/dist/backend/entities/newsletter-send-record.entity.js +89 -0
  86. package/dist/backend/entities/newsletter-send-record.entity.js.map +1 -0
  87. package/dist/backend/entities/newsletter-subscriber-tag.entity.d.ts +6 -0
  88. package/dist/backend/entities/newsletter-subscriber-tag.entity.d.ts.map +1 -0
  89. package/dist/backend/entities/newsletter-subscriber-tag.entity.js +31 -0
  90. package/dist/backend/entities/newsletter-subscriber-tag.entity.js.map +1 -0
  91. package/dist/backend/entities/newsletter-subscriber.entity.d.ts +63 -0
  92. package/dist/backend/entities/newsletter-subscriber.entity.d.ts.map +1 -0
  93. package/dist/backend/entities/newsletter-subscriber.entity.js +147 -0
  94. package/dist/backend/entities/newsletter-subscriber.entity.js.map +1 -0
  95. package/dist/backend/entities/newsletter-suppression.entity.d.ts +14 -0
  96. package/dist/backend/entities/newsletter-suppression.entity.d.ts.map +1 -0
  97. package/dist/backend/entities/newsletter-suppression.entity.js +51 -0
  98. package/dist/backend/entities/newsletter-suppression.entity.js.map +1 -0
  99. package/dist/backend/entities/newsletter-tag.entity.d.ts +12 -0
  100. package/dist/backend/entities/newsletter-tag.entity.d.ts.map +1 -0
  101. package/dist/backend/entities/newsletter-tag.entity.js +53 -0
  102. package/dist/backend/entities/newsletter-tag.entity.js.map +1 -0
  103. package/dist/backend/index.d.ts +120 -0
  104. package/dist/backend/index.d.ts.map +1 -0
  105. package/dist/backend/index.js +139 -0
  106. package/dist/backend/index.js.map +1 -0
  107. package/dist/backend/plugin.d.ts +114 -0
  108. package/dist/backend/plugin.d.ts.map +1 -0
  109. package/dist/backend/plugin.js +171 -0
  110. package/dist/backend/plugin.js.map +1 -0
  111. package/dist/backend/routes.admin.d.ts +27 -0
  112. package/dist/backend/routes.admin.d.ts.map +1 -0
  113. package/dist/backend/routes.admin.js +118 -0
  114. package/dist/backend/routes.admin.js.map +1 -0
  115. package/dist/backend/routes.self.d.ts +15 -0
  116. package/dist/backend/routes.self.d.ts.map +1 -0
  117. package/dist/backend/routes.self.js +28 -0
  118. package/dist/backend/routes.self.js.map +1 -0
  119. package/dist/backend/routes.storefront.d.ts +24 -0
  120. package/dist/backend/routes.storefront.d.ts.map +1 -0
  121. package/dist/backend/routes.storefront.js +79 -0
  122. package/dist/backend/routes.storefront.js.map +1 -0
  123. package/dist/backend/services/audience-resolver.d.ts +20 -0
  124. package/dist/backend/services/audience-resolver.d.ts.map +1 -0
  125. package/dist/backend/services/audience-resolver.js +41 -0
  126. package/dist/backend/services/audience-resolver.js.map +1 -0
  127. package/dist/backend/services/automation.service.d.ts +58 -0
  128. package/dist/backend/services/automation.service.d.ts.map +1 -0
  129. package/dist/backend/services/automation.service.js +301 -0
  130. package/dist/backend/services/automation.service.js.map +1 -0
  131. package/dist/backend/services/campaign-dispatch.service.d.ts +50 -0
  132. package/dist/backend/services/campaign-dispatch.service.d.ts.map +1 -0
  133. package/dist/backend/services/campaign-dispatch.service.js +130 -0
  134. package/dist/backend/services/campaign-dispatch.service.js.map +1 -0
  135. package/dist/backend/services/campaign.service.d.ts +40 -0
  136. package/dist/backend/services/campaign.service.d.ts.map +1 -0
  137. package/dist/backend/services/campaign.service.js +195 -0
  138. package/dist/backend/services/campaign.service.js.map +1 -0
  139. package/dist/backend/services/consent-block-seeder.d.ts +30 -0
  140. package/dist/backend/services/consent-block-seeder.d.ts.map +1 -0
  141. package/dist/backend/services/consent-block-seeder.js +52 -0
  142. package/dist/backend/services/consent-block-seeder.js.map +1 -0
  143. package/dist/backend/services/content.service.d.ts +52 -0
  144. package/dist/backend/services/content.service.d.ts.map +1 -0
  145. package/dist/backend/services/content.service.js +48 -0
  146. package/dist/backend/services/content.service.js.map +1 -0
  147. package/dist/backend/services/custom-field.service.d.ts +16 -0
  148. package/dist/backend/services/custom-field.service.d.ts.map +1 -0
  149. package/dist/backend/services/custom-field.service.js +67 -0
  150. package/dist/backend/services/custom-field.service.js.map +1 -0
  151. package/dist/backend/services/email-block.service.d.ts +26 -0
  152. package/dist/backend/services/email-block.service.d.ts.map +1 -0
  153. package/dist/backend/services/email-block.service.js +120 -0
  154. package/dist/backend/services/email-block.service.js.map +1 -0
  155. package/dist/backend/services/opt-in.service.d.ts +25 -0
  156. package/dist/backend/services/opt-in.service.d.ts.map +1 -0
  157. package/dist/backend/services/opt-in.service.js +46 -0
  158. package/dist/backend/services/opt-in.service.js.map +1 -0
  159. package/dist/backend/services/provider/console-provider.d.ts +32 -0
  160. package/dist/backend/services/provider/console-provider.d.ts.map +1 -0
  161. package/dist/backend/services/provider/console-provider.js +31 -0
  162. package/dist/backend/services/provider/console-provider.js.map +1 -0
  163. package/dist/backend/services/provider/provider-registry.d.ts +51 -0
  164. package/dist/backend/services/provider/provider-registry.d.ts.map +1 -0
  165. package/dist/backend/services/provider/provider-registry.js +84 -0
  166. package/dist/backend/services/provider/provider-registry.js.map +1 -0
  167. package/dist/backend/services/provider/smtp-provider.d.ts +30 -0
  168. package/dist/backend/services/provider/smtp-provider.d.ts.map +1 -0
  169. package/dist/backend/services/provider/smtp-provider.js +42 -0
  170. package/dist/backend/services/provider/smtp-provider.js.map +1 -0
  171. package/dist/backend/services/provider-admin.service.d.ts +39 -0
  172. package/dist/backend/services/provider-admin.service.d.ts.map +1 -0
  173. package/dist/backend/services/provider-admin.service.js +59 -0
  174. package/dist/backend/services/provider-admin.service.js.map +1 -0
  175. package/dist/backend/services/provider-admin.types.d.ts +6 -0
  176. package/dist/backend/services/provider-admin.types.d.ts.map +1 -0
  177. package/dist/backend/services/provider-admin.types.js +2 -0
  178. package/dist/backend/services/provider-admin.types.js.map +1 -0
  179. package/dist/backend/services/queues/newsletter-queues.d.ts +34 -0
  180. package/dist/backend/services/queues/newsletter-queues.d.ts.map +1 -0
  181. package/dist/backend/services/queues/newsletter-queues.js +58 -0
  182. package/dist/backend/services/queues/newsletter-queues.js.map +1 -0
  183. package/dist/backend/services/self.service.d.ts +17 -0
  184. package/dist/backend/services/self.service.d.ts.map +1 -0
  185. package/dist/backend/services/self.service.js +44 -0
  186. package/dist/backend/services/self.service.js.map +1 -0
  187. package/dist/backend/services/stats.service.d.ts +10 -0
  188. package/dist/backend/services/stats.service.d.ts.map +1 -0
  189. package/dist/backend/services/stats.service.js +45 -0
  190. package/dist/backend/services/stats.service.js.map +1 -0
  191. package/dist/backend/services/subscriber-admin.service.d.ts +26 -0
  192. package/dist/backend/services/subscriber-admin.service.d.ts.map +1 -0
  193. package/dist/backend/services/subscriber-admin.service.js +163 -0
  194. package/dist/backend/services/subscriber-admin.service.js.map +1 -0
  195. package/dist/backend/services/subscriber.service.d.ts +91 -0
  196. package/dist/backend/services/subscriber.service.d.ts.map +1 -0
  197. package/dist/backend/services/subscriber.service.js +280 -0
  198. package/dist/backend/services/subscriber.service.js.map +1 -0
  199. package/dist/backend/services/tag.service.d.ts +16 -0
  200. package/dist/backend/services/tag.service.d.ts.map +1 -0
  201. package/dist/backend/services/tag.service.js +79 -0
  202. package/dist/backend/services/tag.service.js.map +1 -0
  203. package/dist/backend/services/token.helper.d.ts +32 -0
  204. package/dist/backend/services/token.helper.d.ts.map +1 -0
  205. package/dist/backend/services/token.helper.js +76 -0
  206. package/dist/backend/services/token.helper.js.map +1 -0
  207. package/dist/backend/services/tracking.service.d.ts +14 -0
  208. package/dist/backend/services/tracking.service.d.ts.map +1 -0
  209. package/dist/backend/services/tracking.service.js +45 -0
  210. package/dist/backend/services/tracking.service.js.map +1 -0
  211. package/dist/manifest.d.ts +192 -0
  212. package/dist/manifest.d.ts.map +1 -0
  213. package/dist/manifest.js +211 -0
  214. package/dist/manifest.js.map +1 -0
  215. package/dist/migrations/20260629T200954_newsletter_init.d.ts +6 -0
  216. package/dist/migrations/20260629T200954_newsletter_init.d.ts.map +1 -0
  217. package/dist/migrations/20260629T200954_newsletter_init.js +261 -0
  218. package/dist/migrations/20260629T200954_newsletter_init.js.map +1 -0
  219. package/dist/migrations/20260830T212736_newsletter_organization_attribution.d.ts +116 -0
  220. package/dist/migrations/20260830T212736_newsletter_organization_attribution.d.ts.map +1 -0
  221. package/dist/migrations/20260830T212736_newsletter_organization_attribution.js +186 -0
  222. package/dist/migrations/20260830T212736_newsletter_organization_attribution.js.map +1 -0
  223. package/dist/migrations/20260903T101752_newsletter_namespace_block_names.d.ts +6 -0
  224. package/dist/migrations/20260903T101752_newsletter_namespace_block_names.d.ts.map +1 -0
  225. package/dist/migrations/20260903T101752_newsletter_namespace_block_names.js +58 -0
  226. package/dist/migrations/20260903T101752_newsletter_namespace_block_names.js.map +1 -0
  227. package/dist/migrations/20260912T125702_newsletter_subscriber_tenant_scope_index.d.ts +23 -0
  228. package/dist/migrations/20260912T125702_newsletter_subscriber_tenant_scope_index.d.ts.map +1 -0
  229. package/dist/migrations/20260912T125702_newsletter_subscriber_tenant_scope_index.js +27 -0
  230. package/dist/migrations/20260912T125702_newsletter_subscriber_tenant_scope_index.js.map +1 -0
  231. package/dist/migrations/index.d.ts +30 -0
  232. package/dist/migrations/index.d.ts.map +1 -0
  233. package/dist/migrations/index.js +35 -0
  234. package/dist/migrations/index.js.map +1 -0
  235. package/docs/newsletter.md +94 -0
  236. package/i18n/en.json +27 -0
  237. package/i18n/pl.json +27 -0
  238. package/package.json +119 -0
  239. package/tailwind.css +14 -0
@@ -0,0 +1,163 @@
1
+ import { ERROR_CODES, } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ import { NewsletterSubscriber } from '../entities/newsletter-subscriber.entity.js';
4
+ import { NewsletterTag } from '../entities/newsletter-tag.entity.js';
5
+ import { NewsletterSubscriberTag } from '../entities/newsletter-subscriber-tag.entity.js';
6
+ import { NewsletterSuppression } from '../entities/newsletter-suppression.entity.js';
7
+ const CSV_COLUMNS = ['email', 'status', 'tags', 'source', 'consentAt', 'unsubscribedAt', 'unsubscribeReason', 'customFields'];
8
+ function csvCell(value) {
9
+ return /[",\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
10
+ }
11
+ /** Admin subscriber list / detail / lifecycle / CSV export (feature 048, US3). */
12
+ export class NewsletterSubscriberAdminService {
13
+ emFactory;
14
+ auditLog;
15
+ constructor(emFactory, auditLog) {
16
+ this.emFactory = emFactory;
17
+ this.auditLog = auditLog;
18
+ }
19
+ async buildWhere(em, query) {
20
+ const where = {};
21
+ if (query.status)
22
+ where['status'] = query.status;
23
+ if (query.channel)
24
+ where['salesChannelId'] = query.channel;
25
+ if (query.q)
26
+ where['email'] = { $ilike: `%${query.q}%` };
27
+ if (query.tag) {
28
+ const tag = await em.findOne(NewsletterTag, { code: query.tag });
29
+ const links = tag ? await em.find(NewsletterSubscriberTag, { tagId: tag.id }) : [];
30
+ where['id'] = { $in: links.map((l) => l.subscriberId) };
31
+ }
32
+ return where;
33
+ }
34
+ async tagsFor(em, subscriberIds) {
35
+ const map = new Map();
36
+ if (subscriberIds.length === 0)
37
+ return map;
38
+ const links = await em.find(NewsletterSubscriberTag, { subscriberId: { $in: subscriberIds } });
39
+ const tagIds = [...new Set(links.map((l) => l.tagId))];
40
+ const tags = await em.find(NewsletterTag, { id: { $in: tagIds } });
41
+ const codeById = new Map(tags.map((t) => [t.id, t.code]));
42
+ for (const link of links) {
43
+ const code = codeById.get(link.tagId);
44
+ if (!code)
45
+ continue;
46
+ const arr = map.get(link.subscriberId) ?? [];
47
+ arr.push(code);
48
+ map.set(link.subscriberId, arr);
49
+ }
50
+ return map;
51
+ }
52
+ async list(query) {
53
+ const em = this.emFactory();
54
+ const where = await this.buildWhere(em, query);
55
+ const [rows, total] = await em.findAndCount(NewsletterSubscriber, where, {
56
+ orderBy: { createdAt: 'desc' },
57
+ limit: query.pageSize,
58
+ offset: (query.page - 1) * query.pageSize,
59
+ });
60
+ const tagMap = await this.tagsFor(em, rows.map((r) => r.id));
61
+ return {
62
+ items: rows.map((r) => this.toSummary(r, tagMap.get(r.id) ?? [])),
63
+ page: query.page,
64
+ pageSize: query.pageSize,
65
+ total,
66
+ };
67
+ }
68
+ async getDetail(id) {
69
+ const em = this.emFactory();
70
+ const sub = await em.findOne(NewsletterSubscriber, { id });
71
+ if (!sub)
72
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Subscriber not found.');
73
+ const tagMap = await this.tagsFor(em, [id]);
74
+ return this.toDetail(sub, tagMap.get(id) ?? []);
75
+ }
76
+ async deactivate(id, expectedVersion) {
77
+ const em = this.emFactory();
78
+ const sub = await em.findOne(NewsletterSubscriber, { id });
79
+ if (!sub)
80
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Subscriber not found.');
81
+ if (sub.version !== expectedVersion) {
82
+ throw new HttpError(409, ERROR_CODES.VERSION_CONFLICT, 'Subscriber was modified.');
83
+ }
84
+ sub.status = 'deactivated';
85
+ sub.deactivatedAt = new Date();
86
+ sub.version += 1;
87
+ await em.persistAndFlush(sub);
88
+ await this.auditLog?.record({
89
+ action: 'newsletter_deactivate',
90
+ objectType: 'newsletter_subscriber',
91
+ objectId: sub.id,
92
+ stateAfter: { status: 'deactivated' },
93
+ });
94
+ const tagMap = await this.tagsFor(em, [id]);
95
+ return this.toDetail(sub, tagMap.get(id) ?? []);
96
+ }
97
+ async remove(id) {
98
+ const em = this.emFactory();
99
+ const sub = await em.findOne(NewsletterSubscriber, { id });
100
+ if (!sub)
101
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Subscriber not found.');
102
+ // Retain a suppression keyed by email so the address stays unmailable (R6).
103
+ const existing = await em.findOne(NewsletterSuppression, { email: sub.email });
104
+ if (!existing) {
105
+ em.create(NewsletterSuppression, { email: sub.email, reason: 'unsubscribe', detail: 'deleted' });
106
+ }
107
+ await this.auditLog?.record({
108
+ action: 'newsletter_delete',
109
+ objectType: 'newsletter_subscriber',
110
+ objectId: sub.id,
111
+ stateBefore: { email: sub.email },
112
+ });
113
+ await em.removeAndFlush(sub); // bridges cascade via FK
114
+ }
115
+ async exportCsv(query) {
116
+ const em = this.emFactory();
117
+ const where = await this.buildWhere(em, query);
118
+ const rows = await em.find(NewsletterSubscriber, where, { orderBy: { createdAt: 'desc' } });
119
+ const tagMap = await this.tagsFor(em, rows.map((r) => r.id));
120
+ const lines = [CSV_COLUMNS.join(',')];
121
+ for (const r of rows) {
122
+ lines.push([
123
+ r.email,
124
+ r.status,
125
+ (tagMap.get(r.id) ?? []).join('|'),
126
+ r.source ?? '',
127
+ r.consentAt?.toISOString() ?? '',
128
+ r.unsubscribedAt?.toISOString() ?? '',
129
+ r.unsubscribeReason ?? '',
130
+ JSON.stringify(r.customFields),
131
+ ]
132
+ .map((v) => csvCell(String(v)))
133
+ .join(','));
134
+ }
135
+ return `${lines.join('\n')}\n`;
136
+ }
137
+ toSummary(s, tags) {
138
+ return {
139
+ id: s.id,
140
+ email: s.email,
141
+ status: s.status,
142
+ source: s.source,
143
+ salesChannelId: s.salesChannelId,
144
+ tags,
145
+ consentAt: s.consentAt?.toISOString() ?? null,
146
+ createdAt: s.createdAt.toISOString(),
147
+ };
148
+ }
149
+ toDetail(s, tags) {
150
+ return {
151
+ ...this.toSummary(s, tags),
152
+ customerAccountId: s.customerAccountId,
153
+ customFields: s.customFields,
154
+ confirmedAt: s.confirmedAt?.toISOString() ?? null,
155
+ unsubscribedAt: s.unsubscribedAt?.toISOString() ?? null,
156
+ unsubscribeReason: s.unsubscribeReason,
157
+ deactivatedAt: s.deactivatedAt?.toISOString() ?? null,
158
+ version: s.version,
159
+ updatedAt: s.updatedAt.toISOString(),
160
+ };
161
+ }
162
+ }
163
+ //# sourceMappingURL=subscriber-admin.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriber-admin.service.js","sourceRoot":"","sources":["../../../src/backend/services/subscriber-admin.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { CustomerAccountReadPort, EmailMailerPort } from '@endora-commerce/contracts';
3
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
4
+ import type { NewsletterOptInService } from './opt-in.service.js';
5
+ export interface NewsletterLinkBuilder {
6
+ confirm(token: string): string;
7
+ unsubscribe(token: string): string;
8
+ }
9
+ export interface SubscribeInput {
10
+ email: string;
11
+ salesChannelId: string | null;
12
+ tags?: string[];
13
+ customFields?: Record<string, string | number | boolean | null>;
14
+ source?: string;
15
+ customerAccountId?: string | null;
16
+ }
17
+ export interface SubscriberServiceDeps {
18
+ emFactory: () => EntityManager;
19
+ optIn: NewsletterOptInService;
20
+ /** Channel used for opt-in reads when a subscriber has no origin channel. */
21
+ /**
22
+ * The channel a subscriber with no origin channel belongs to, **resolved per
23
+ * call** since `specs/117-instance-bring-up/` Phase 6.
24
+ *
25
+ * It was a value the composition awaited and passed in, which is what made
26
+ * `newsletterBridge` a thing only a composition root could assemble. An
27
+ * accessor lets this module resolve the system-default channel itself,
28
+ * through the port the platform already contributes.
29
+ */
30
+ resolveDefaultChannelId: () => Promise<string | null>;
31
+ links: NewsletterLinkBuilder;
32
+ /**
33
+ * `customer_accounts`' read model, for the one field the owner stamp needs:
34
+ * the organisation the owning account belongs to (feature 087, D-187).
35
+ *
36
+ * **Required**, and it is the one dependency in this interface that is. The
37
+ * three optional slots beside it are collaborators a composition may
38
+ * genuinely lack — a transport, an audit sink, an event emitter — and each
39
+ * has a defined behaviour when absent. This one has none: there is no
40
+ * composition in which an owned subscriber written with no organisation is
41
+ * correct, because
42
+ * `newsletter_subscribers_organization_attribution_chk` refuses that row.
43
+ * Making it optional would move the failure from this constructor to a
44
+ * Postgres error naming a constraint the caller never heard of, on the first
45
+ * signed-in subscribe rather than at composition.
46
+ *
47
+ * Resolved through `lazyPort`, so a switched-off `customer_accounts` refuses
48
+ * at the call with the 503 `MODULE_DISABLED` envelope rather than at
49
+ * composition time.
50
+ */
51
+ customerAccounts: CustomerAccountReadPort;
52
+ /** `emailMailer`, owned by `email`, as its published contract (feature 075). */
53
+ mailer?: EmailMailerPort;
54
+ auditLog?: AuditPort;
55
+ /** Optional observability emitter (wraps the in-process EventBus). */
56
+ emitEvent?: (name: string, payload: Record<string, unknown>) => void;
57
+ }
58
+ /**
59
+ * Subscriber lifecycle (feature 048, US1/US3). Email is the global identity;
60
+ * re-submitting merges tags/custom fields. Suppression (table) overrides
61
+ * everything. Double opt-in creates a `pending` row + confirmation mail; single
62
+ * opt-in activates immediately.
63
+ */
64
+ export declare class NewsletterSubscriberService {
65
+ #private;
66
+ private readonly deps;
67
+ constructor(deps: SubscriberServiceDeps);
68
+ /** Subscribe (idempotent on email). Returns the resulting status. */
69
+ subscribe(input: SubscribeInput): Promise<{
70
+ status: 'pending' | 'active';
71
+ }>;
72
+ /** Confirm a double opt-in subscriber from a signed token. Idempotent. */
73
+ confirm(subscriberId: string): Promise<{
74
+ ok: boolean;
75
+ }>;
76
+ /** Unsubscribe with an optional reason; writes a suppression row. Idempotent. */
77
+ unsubscribe(subscriberId: string, reason?: string): Promise<{
78
+ ok: boolean;
79
+ }>;
80
+ /**
81
+ * Suppress an address on provider feedback (hard bounce / complaint — FR-035).
82
+ * Writes a suppression row (idempotent) so the address is excluded from all
83
+ * future sends regardless of targeting; a complaint/bounce suppression is NOT
84
+ * lifted by a later re-subscribe (only an explicit unsubscribe is).
85
+ */
86
+ suppress(email: string, reason: 'bounce' | 'complaint', detail?: string): Promise<void>;
87
+ private mergeCustomFields;
88
+ private mergeTags;
89
+ private sendConfirmation;
90
+ }
91
+ //# sourceMappingURL=subscriber.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriber.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/subscriber.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC3F,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAMlE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAElE,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CACpC;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAsDD,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,KAAK,EAAE,sBAAsB,CAAC;IAC9B,6EAA6E;IAC7E;;;;;;;;OAQG;IACH,uBAAuB,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtD,KAAK,EAAE,qBAAqB,CAAC;IAC7B;;;;;;;;;;;;;;;;;;OAkBG;IACH,gBAAgB,EAAE,uBAAuB,CAAC;IAC1C,gFAAgF;IAChF,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,sEAAsE;IACtE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACtE;AAED;;;;;GAKG;AACH,qBAAa,2BAA2B;;IAC1B,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,qBAAqB;IAsCxD,qEAAqE;IAC/D,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAA;KAAE,CAAC;IAoFjF,0EAA0E;IACpE,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC;IAmB7D,iFAAiF;IAC3E,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC;IAgClF;;;;;OAKG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,WAAW,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAwB/E,iBAAiB;YAejB,SAAS;YAUT,gBAAgB;CA0B/B"}
@@ -0,0 +1,280 @@
1
+ import { NewsletterSubscriber } from '../entities/newsletter-subscriber.entity.js';
2
+ import { NewsletterTag } from '../entities/newsletter-tag.entity.js';
3
+ import { NewsletterSubscriberTag } from '../entities/newsletter-subscriber-tag.entity.js';
4
+ import { NewsletterCustomField } from '../entities/newsletter-custom-field.entity.js';
5
+ import { NewsletterSuppression } from '../entities/newsletter-suppression.entity.js';
6
+ /**
7
+ * The two attribution columns, written together from one resolved owner.
8
+ *
9
+ * **Both** of this module's write sites go through it — the `em.create` on a
10
+ * first subscribe and the adoption of an already-ownerless row — which is what
11
+ * one function has to cover here. It needs no de-association arm, unlike
12
+ * `pwa`'s: nothing in `newsletter` ever clears `customer_account_id`, so the
13
+ * ownerless arm below is only ever the *initial* state of a row and never a
14
+ * transition into one.
15
+ */
16
+ function ownerColumns(owner) {
17
+ return owner.kind === 'customer'
18
+ ? { customerAccountId: owner.customerAccountId, organizationId: owner.organizationId }
19
+ : {
20
+ customerAccountId: null,
21
+ // FR-011 — a storefront sign-up belongs to no organisation, and the
22
+ // constraint says nothing about it. On this table that is the ordinary
23
+ // case: `POST /api/v1/newsletter/subscribe` passes no account. Who such
24
+ // a row *should* belong to is R-6's open question and is not answered
25
+ // here — in particular not by matching `email` against
26
+ // `customer_accounts.email`, which is that question guessed.
27
+ organizationId: null,
28
+ };
29
+ }
30
+ /**
31
+ * Subscriber lifecycle (feature 048, US1/US3). Email is the global identity;
32
+ * re-submitting merges tags/custom fields. Suppression (table) overrides
33
+ * everything. Double opt-in creates a `pending` row + confirmation mail; single
34
+ * opt-in activates immediately.
35
+ */
36
+ export class NewsletterSubscriberService {
37
+ deps;
38
+ constructor(deps) {
39
+ this.deps = deps;
40
+ }
41
+ /**
42
+ * The organisation that owns a subscriber belonging to this account — feature
43
+ * 087 Group B, D-187.
44
+ *
45
+ * Total, and refuses rather than returning `null`:
46
+ * `customer_accounts.organization_id` is `NOT NULL` (D-178), so an account
47
+ * that resolves always has one, and an account that does not resolve is a
48
+ * caller naming a row that is not there. Either way there is no organisation
49
+ * to stamp, and a subscriber written without one is a buyer hidden from the
50
+ * representative who serves them — and absent from that representative's CSV
51
+ * export, which is the surface R-6 is about. So the write stops here, where
52
+ * the message can name the account, instead of at the constraint.
53
+ */
54
+ async #organizationOf(customerAccountId) {
55
+ const account = await this.deps.customerAccounts.findById(customerAccountId);
56
+ if (!account) {
57
+ throw new Error(`NewsletterSubscriberService: customer account ${customerAccountId} does not resolve, ` +
58
+ 'so the subscriber it would own has no organisation to carry.');
59
+ }
60
+ return account.organizationId;
61
+ }
62
+ /** The caller's owner input, with the organisation a customer owner implies. */
63
+ async #resolveOwner(customerAccountId) {
64
+ if (customerAccountId === null || customerAccountId === undefined)
65
+ return { kind: 'anonymous' };
66
+ return {
67
+ kind: 'customer',
68
+ customerAccountId,
69
+ organizationId: await this.#organizationOf(customerAccountId),
70
+ };
71
+ }
72
+ /** Subscribe (idempotent on email). Returns the resulting status. */
73
+ async subscribe(input) {
74
+ const em = this.deps.emFactory();
75
+ const email = input.email.trim().toLowerCase();
76
+ const channelId = input.salesChannelId;
77
+ const settingsChannelId = channelId ?? (await this.deps.resolveDefaultChannelId());
78
+ const mode = await this.deps.optIn.resolveMode(settingsChannelId);
79
+ // Suppressed (complaint/bounce) addresses are not silently re-subscribed.
80
+ const suppressed = await em.findOne(NewsletterSuppression, { email });
81
+ // D-187 — resolved **before** the first managed entity is touched, not
82
+ // between the account assignment and the flush. Both `em.create` and the
83
+ // adoption below register the row with the unit of work, so a throw after
84
+ // either would leave an owned, unattributed subscriber there for whatever
85
+ // flushes this request next.
86
+ const owner = await this.#resolveOwner(input.customerAccountId);
87
+ let subscriber = await em.findOne(NewsletterSubscriber, { email });
88
+ const isNew = !subscriber;
89
+ if (!subscriber) {
90
+ subscriber = em.create(NewsletterSubscriber, {
91
+ email,
92
+ status: 'pending',
93
+ source: input.source ?? null,
94
+ salesChannelId: channelId,
95
+ // D-187 — the only place a subscriber is inserted, and the organisation
96
+ // goes in with the account. `ownerColumns` takes a *resolved* owner, so
97
+ // the customer branch has no shape in which the organisation could be
98
+ // omitted; the anonymous branch has none to carry (FR-011).
99
+ ...ownerColumns(owner),
100
+ });
101
+ }
102
+ if (owner.kind === 'customer' && !subscriber.customerAccountId) {
103
+ // The adoption: a subscriber who signed up anonymously and has now signed
104
+ // in. D-187 — the account and the organisation move together here too.
105
+ // `em.assign` rather than two statements, for the same reason
106
+ // `ownerColumns` returns both columns as one value: two statements are two
107
+ // things to remember, and the second one is the one that was never
108
+ // written.
109
+ em.assign(subscriber, ownerColumns(owner));
110
+ }
111
+ // Activation policy: single opt-in (or already confirmed) → active now;
112
+ // double opt-in → pending until the email link is followed.
113
+ const wasActive = subscriber.status === 'active';
114
+ if (mode === 'single' || subscriber.confirmedAt) {
115
+ subscriber.status = 'active';
116
+ subscriber.consentAt ??= new Date();
117
+ if (mode === 'single')
118
+ subscriber.confirmedAt ??= new Date();
119
+ }
120
+ else if (!wasActive) {
121
+ subscriber.status = 'pending';
122
+ subscriber.consentAt ??= new Date();
123
+ }
124
+ if (suppressed && subscriber.status === 'active') {
125
+ // An explicit re-subscribe lifts a prior self-unsubscribe suppression.
126
+ if (suppressed.reason === 'unsubscribe') {
127
+ await em.removeAndFlush(suppressed);
128
+ }
129
+ }
130
+ await this.mergeCustomFields(em, subscriber, input.customFields);
131
+ await em.persistAndFlush(subscriber);
132
+ await this.mergeTags(em, subscriber.id, input.tags);
133
+ if (subscriber.status === 'pending') {
134
+ await this.sendConfirmation(subscriber.id, email, settingsChannelId);
135
+ }
136
+ await this.deps.auditLog?.record({
137
+ action: isNew ? 'newsletter_subscribe' : 'newsletter_resubscribe',
138
+ objectType: 'newsletter_subscriber',
139
+ objectId: subscriber.id,
140
+ stateAfter: { email, status: subscriber.status, source: input.source ?? null },
141
+ });
142
+ this.deps.emitEvent?.('newsletter.subscribed.v1', {
143
+ subscriberId: subscriber.id,
144
+ email,
145
+ status: subscriber.status,
146
+ });
147
+ return { status: subscriber.status === 'active' ? 'active' : 'pending' };
148
+ }
149
+ /** Confirm a double opt-in subscriber from a signed token. Idempotent. */
150
+ async confirm(subscriberId) {
151
+ const em = this.deps.emFactory();
152
+ const subscriber = await em.findOne(NewsletterSubscriber, { id: subscriberId });
153
+ if (!subscriber)
154
+ return { ok: false };
155
+ if (subscriber.status === 'pending') {
156
+ subscriber.status = 'active';
157
+ subscriber.confirmedAt = new Date();
158
+ subscriber.consentAt ??= new Date();
159
+ await em.persistAndFlush(subscriber);
160
+ await this.deps.auditLog?.record({
161
+ action: 'newsletter_confirm',
162
+ objectType: 'newsletter_subscriber',
163
+ objectId: subscriber.id,
164
+ stateAfter: { status: 'active' },
165
+ });
166
+ }
167
+ return { ok: true };
168
+ }
169
+ /** Unsubscribe with an optional reason; writes a suppression row. Idempotent. */
170
+ async unsubscribe(subscriberId, reason) {
171
+ const em = this.deps.emFactory();
172
+ const subscriber = await em.findOne(NewsletterSubscriber, { id: subscriberId });
173
+ if (!subscriber)
174
+ return { ok: false };
175
+ if (subscriber.status !== 'unsubscribed') {
176
+ subscriber.status = 'unsubscribed';
177
+ subscriber.unsubscribedAt = new Date();
178
+ subscriber.unsubscribeReason = reason ?? null;
179
+ const existing = await em.findOne(NewsletterSuppression, { email: subscriber.email });
180
+ if (!existing) {
181
+ em.create(NewsletterSuppression, {
182
+ email: subscriber.email,
183
+ reason: 'unsubscribe',
184
+ detail: reason ?? null,
185
+ });
186
+ }
187
+ await em.persistAndFlush(subscriber);
188
+ await this.deps.auditLog?.record({
189
+ action: 'newsletter_unsubscribe',
190
+ objectType: 'newsletter_subscriber',
191
+ objectId: subscriber.id,
192
+ stateAfter: { status: 'unsubscribed', reason: reason ?? null },
193
+ });
194
+ this.deps.emitEvent?.('newsletter.unsubscribed.v1', {
195
+ subscriberId: subscriber.id,
196
+ email: subscriber.email,
197
+ reason: reason ?? null,
198
+ });
199
+ }
200
+ return { ok: true };
201
+ }
202
+ /**
203
+ * Suppress an address on provider feedback (hard bounce / complaint — FR-035).
204
+ * Writes a suppression row (idempotent) so the address is excluded from all
205
+ * future sends regardless of targeting; a complaint/bounce suppression is NOT
206
+ * lifted by a later re-subscribe (only an explicit unsubscribe is).
207
+ */
208
+ async suppress(email, reason, detail) {
209
+ const em = this.deps.emFactory();
210
+ const normalized = email.trim().toLowerCase();
211
+ const existing = await em.findOne(NewsletterSuppression, { email: normalized });
212
+ if (existing) {
213
+ existing.reason = reason;
214
+ if (detail)
215
+ existing.detail = detail;
216
+ }
217
+ else {
218
+ em.create(NewsletterSuppression, { email: normalized, reason, detail: detail ?? null });
219
+ }
220
+ const subscriber = await em.findOne(NewsletterSubscriber, { email: normalized });
221
+ if (subscriber && subscriber.status === 'active') {
222
+ subscriber.status = 'deactivated';
223
+ subscriber.deactivatedAt = new Date();
224
+ }
225
+ await em.flush();
226
+ await this.deps.auditLog?.record({
227
+ action: `newsletter_${reason}`,
228
+ objectType: 'newsletter_subscriber',
229
+ objectId: subscriber?.id ?? normalized,
230
+ stateAfter: { email: normalized, reason },
231
+ });
232
+ }
233
+ async mergeCustomFields(em, subscriber, incoming) {
234
+ if (!incoming || Object.keys(incoming).length === 0)
235
+ return;
236
+ const defined = await em.find(NewsletterCustomField, {});
237
+ const allowed = new Set(defined.map((f) => f.key));
238
+ const merged = { ...subscriber.customFields };
239
+ for (const [k, v] of Object.entries(incoming)) {
240
+ if (allowed.has(k))
241
+ merged[k] = v;
242
+ }
243
+ subscriber.customFields = merged;
244
+ }
245
+ async mergeTags(em, subscriberId, codes) {
246
+ if (!codes || codes.length === 0)
247
+ return;
248
+ const tags = await em.find(NewsletterTag, { code: { $in: codes } });
249
+ for (const tag of tags) {
250
+ const existing = await em.findOne(NewsletterSubscriberTag, { subscriberId, tagId: tag.id });
251
+ if (!existing)
252
+ em.create(NewsletterSubscriberTag, { subscriberId, tagId: tag.id });
253
+ }
254
+ await em.flush();
255
+ }
256
+ async sendConfirmation(subscriberId, email, salesChannelId) {
257
+ if (!this.deps.mailer)
258
+ return;
259
+ const token = await this.deps.optIn.mintConfirmToken(subscriberId, salesChannelId);
260
+ const url = this.deps.links.confirm(token);
261
+ const outcome = await this.deps.mailer.send({
262
+ messageId: `newsletter-confirm-${token}`,
263
+ to: email,
264
+ subject: 'Confirm your newsletter subscription',
265
+ text: `Please confirm your subscription by opening this link:\n${url}\n`,
266
+ html: `<p>Please confirm your subscription:</p><p><a href="${url}">Confirm subscription</a></p>`,
267
+ kind: 'newsletter_confirmation',
268
+ });
269
+ if (outcome.status !== 'sent') {
270
+ // The subscriber row exists in `pending` either way; without the message
271
+ // the double opt-in never completes, so it is named — and D-59's record
272
+ // is what an operator reads when the subscriber says it never arrived.
273
+ console.warn('[newsletter] the confirmation e-mail was not sent', {
274
+ subscriberId,
275
+ reason: outcome.reason,
276
+ });
277
+ }
278
+ }
279
+ }
280
+ //# sourceMappingURL=subscriber.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriber.service.js","sourceRoot":"","sources":["../../../src/backend/services/subscriber.service.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,oBAAoB,EAAE,MAAM,6CAA6C,CAAC;AACnF,OAAO,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,iDAAiD,CAAC;AAC1F,OAAO,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAC;AACtF,OAAO,EAAE,qBAAqB,EAAE,MAAM,8CAA8C,CAAC;AAyCrF;;;;;;;;;GASG;AACH,SAAS,YAAY,CAAC,KAA8B;IAIlD,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU;QAC9B,CAAC,CAAC,EAAE,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,EAAE;QACtF,CAAC,CAAC;YACE,iBAAiB,EAAE,IAAI;YACvB,oEAAoE;YACpE,uEAAuE;YACvE,wEAAwE;YACxE,sEAAsE;YACtE,uDAAuD;YACvD,6DAA6D;YAC7D,cAAc,EAAE,IAAI;SACrB,CAAC;AACR,CAAC;AA4CD;;;;;GAKG;AACH,MAAM,OAAO,2BAA2B;IACT;IAA7B,YAA6B,IAA2B;QAA3B,SAAI,GAAJ,IAAI,CAAuB;IAAG,CAAC;IAE5D;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,eAAe,CAAC,iBAAyB;QAC7C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC;QAC7E,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CACb,iDAAiD,iBAAiB,qBAAqB;gBACrF,8DAA8D,CACjE,CAAC;QACJ,CAAC;QACD,OAAO,OAAO,CAAC,cAAc,CAAC;IAChC,CAAC;IAED,gFAAgF;IAChF,KAAK,CAAC,aAAa,CACjB,iBAA4C;QAE5C,IAAI,iBAAiB,KAAK,IAAI,IAAI,iBAAiB,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;QAChG,OAAO;YACL,IAAI,EAAE,UAAU;YAChB,iBAAiB;YACjB,cAAc,EAAE,MAAM,IAAI,CAAC,eAAe,CAAC,iBAAiB,CAAC;SAC9D,CAAC;IACJ,CAAC;IAED,qEAAqE;IACrE,KAAK,CAAC,SAAS,CAAC,KAAqB;QACnC,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAE/C,MAAM,SAAS,GAAG,KAAK,CAAC,cAAc,CAAC;QACvC,MAAM,iBAAiB,GAAG,SAAS,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,uBAAuB,EAAE,CAAC,CAAC;QACnF,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,iBAAiB,CAAC,CAAC;QAElE,0EAA0E;QAC1E,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,qBAAqB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAEtE,uEAAuE;QACvE,yEAAyE;QACzE,0EAA0E;QAC1E,0EAA0E;QAC1E,6BAA6B;QAC7B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;QAEhE,IAAI,UAAU,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,oBAAoB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QACnE,MAAM,KAAK,GAAG,CAAC,UAAU,CAAC;QAC1B,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,UAAU,GAAG,EAAE,CAAC,MAAM,CAAC,oBAAoB,EAAE;gBAC3C,KAAK;gBACL,MAAM,EAAE,SAAS;gBACjB,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,IAAI;gBAC5B,cAAc,EAAE,SAAS;gBACzB,wEAAwE;gBACxE,wEAAwE;gBACxE,sEAAsE;gBACtE,4DAA4D;gBAC5D,GAAG,YAAY,CAAC,KAAK,CAAC;aACvB,CAAC,CAAC;QACL,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,UAAU,CAAC,iBAAiB,EAAE,CAAC;YAC/D,0EAA0E;YAC1E,uEAAuE;YACvE,8DAA8D;YAC9D,2EAA2E;YAC3E,mEAAmE;YACnE,WAAW;YACX,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;QAC7C,CAAC;QAED,wEAAwE;QACxE,4DAA4D;QAC5D,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,KAAK,QAAQ,CAAC;QACjD,IAAI,IAAI,KAAK,QAAQ,IAAI,UAAU,CAAC,WAAW,EAAE,CAAC;YAChD,UAAU,CAAC,MAAM,GAAG,QAAQ,CAAC;YAC7B,UAAU,CAAC,SAAS,KAAK,IAAI,IAAI,EAAE,CAAC;YACpC,IAAI,IAAI,KAAK,QAAQ;gBAAE,UAAU,CAAC,WAAW,KAAK,IAAI,IAAI,EAAE,CAAC;QAC/D,CAAC;aAAM,IAAI,CAAC,SAAS,EAAE,CAAC;YACtB,UAAU,CAAC,MAAM,GAAG,SAAS,CAAC;YAC9B,UAAU,CAAC,SAAS,KAAK,IAAI,IAAI,EAAE,CAAC;QACtC,CAAC;QACD,IAAI,UAAU,IAAI,UAAU,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YACjD,uEAAuE;YACvE,IAAI,UAAU,CAAC,MAAM,KAAK,aAAa,EAAE,CAAC;gBACxC,MAAM,EAAE,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;YACtC,CAAC;QACH,CAAC;QAED,MAAM,IAAI,CAAC,iBAAiB,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;QACjE,MAAM,EAAE,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;QACrC,MAAM,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,UAAU,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAEpD,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YACpC,MAAM,IAAI,CAAC,gBAAgB,CAAC,UAAU,CAAC,EAAE,EAAE,KAAK,EAAE,iBAAiB,CAAC,CAAC;QACvE,CAAC;QAED,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC;YAC/B,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,wBAAwB;YACjE,UAAU,EAAE,uBAAuB;YACnC,QAAQ,EAAE,UAAU,CAAC,EAAE;YACvB,UAAU,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,IAAI,EAAE;SAC/E,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,0BAA0B,EAAE;YAChD,YAAY,EAAE,UAAU,CAAC,EAAE;YAC3B,KAAK;YACL,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAC;QAEH,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;IAC3E,CAAC;IAED,0EAA0E;IAC1E,KAAK,CAAC,OAAO,CAAC,YAAoB;QAChC,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QACjC,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,oBAAoB,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,CAAC,CAAC;QAChF,IAAI,CAAC,UAAU;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC;QACtC,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YACpC,UAAU,CAAC,MAAM,GAAG,QAAQ,CAAC;YAC7B,UAAU,CAAC,WAAW,GAAG,IAAI,IAAI,EAAE,CAAC;YACpC,UAAU,CAAC,SAAS,KAAK,IAAI,IAAI,EAAE,CAAC;YACpC,MAAM,EAAE,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;YACrC,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAC/B,MAAM,EAAE,oBAAoB;gBAC5B,UAAU,EAAE,uBAAuB;gBACnC,QAAQ,EAAE,UAAU,CAAC,EAAE;gBACvB,UAAU,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE;aACjC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACtB,CAAC;IAED,iFAAiF;IACjF,KAAK,CAAC,WAAW,CAAC,YAAoB,EAAE,MAAe;QACrD,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QACjC,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,oBAAoB,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,CAAC,CAAC;QAChF,IAAI,CAAC,UAAU;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC;QACtC,IAAI,UAAU,CAAC,MAAM,KAAK,cAAc,EAAE,CAAC;YACzC,UAAU,CAAC,MAAM,GAAG,cAAc,CAAC;YACnC,UAAU,CAAC,cAAc,GAAG,IAAI,IAAI,EAAE,CAAC;YACvC,UAAU,CAAC,iBAAiB,GAAG,MAAM,IAAI,IAAI,CAAC;YAC9C,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,qBAAqB,EAAE,EAAE,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC;YACtF,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,EAAE,CAAC,MAAM,CAAC,qBAAqB,EAAE;oBAC/B,KAAK,EAAE,UAAU,CAAC,KAAK;oBACvB,MAAM,EAAE,aAAa;oBACrB,MAAM,EAAE,MAAM,IAAI,IAAI;iBACvB,CAAC,CAAC;YACL,CAAC;YACD,MAAM,EAAE,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;YACrC,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAC/B,MAAM,EAAE,wBAAwB;gBAChC,UAAU,EAAE,uBAAuB;gBACnC,QAAQ,EAAE,UAAU,CAAC,EAAE;gBACvB,UAAU,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,IAAI,IAAI,EAAE;aAC/D,CAAC,CAAC;YACH,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,4BAA4B,EAAE;gBAClD,YAAY,EAAE,UAAU,CAAC,EAAE;gBAC3B,KAAK,EAAE,UAAU,CAAC,KAAK;gBACvB,MAAM,EAAE,MAAM,IAAI,IAAI;aACvB,CAAC,CAAC;QACL,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACtB,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,QAAQ,CAAC,KAAa,EAAE,MAA8B,EAAE,MAAe;QAC3E,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QACjC,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC9C,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,qBAAqB,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;QAChF,IAAI,QAAQ,EAAE,CAAC;YACb,QAAQ,CAAC,MAAM,GAAG,MAAM,CAAC;YACzB,IAAI,MAAM;gBAAE,QAAQ,CAAC,MAAM,GAAG,MAAM,CAAC;QACvC,CAAC;aAAM,CAAC;YACN,EAAE,CAAC,MAAM,CAAC,qBAAqB,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC;QAC1F,CAAC;QACD,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,oBAAoB,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;QACjF,IAAI,UAAU,IAAI,UAAU,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YACjD,UAAU,CAAC,MAAM,GAAG,aAAa,CAAC;YAClC,UAAU,CAAC,aAAa,GAAG,IAAI,IAAI,EAAE,CAAC;QACxC,CAAC;QACD,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QACjB,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC;YAC/B,MAAM,EAAE,cAAc,MAAM,EAAE;YAC9B,UAAU,EAAE,uBAAuB;YACnC,QAAQ,EAAE,UAAU,EAAE,EAAE,IAAI,UAAU;YACtC,UAAU,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE;SAC1C,CAAC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,iBAAiB,CAC7B,EAAiB,EACjB,UAAgC,EAChC,QAA2D;QAE3D,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC5D,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,qBAAqB,EAAE,EAAE,CAAC,CAAC;QACzD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,EAAE,GAAG,UAAU,CAAC,YAAY,EAAE,CAAC;QAC9C,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9C,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;gBAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACpC,CAAC;QACD,UAAU,CAAC,YAAY,GAAG,MAAM,CAAC;IACnC,CAAC;IAEO,KAAK,CAAC,SAAS,CAAC,EAAiB,EAAE,YAAoB,EAAE,KAAgB;QAC/E,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACzC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QACpE,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,uBAAuB,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;YAC5F,IAAI,CAAC,QAAQ;gBAAE,EAAE,CAAC,MAAM,CAAC,uBAAuB,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;QACrF,CAAC;QACD,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;IACnB,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAC5B,YAAoB,EACpB,KAAa,EACb,cAA6B;QAE7B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO;QAC9B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;QACnF,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC3C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;YAC1C,SAAS,EAAE,sBAAsB,KAAK,EAAE;YACxC,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,sCAAsC;YAC/C,IAAI,EAAE,2DAA2D,GAAG,IAAI;YACxE,IAAI,EAAE,uDAAuD,GAAG,gCAAgC;YAChG,IAAI,EAAE,yBAAyB;SAChC,CAAC,CAAC;QACH,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC9B,yEAAyE;YACzE,wEAAwE;YACxE,uEAAuE;YACvE,OAAO,CAAC,IAAI,CAAC,mDAAmD,EAAE;gBAChE,YAAY;gBACZ,MAAM,EAAE,OAAO,CAAC,MAAM;aACvB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,16 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type CreateNewsletterTagRequest, type NewsletterTag as NewsletterTagDto, type UpdateNewsletterTagRequest } from '@endora-commerce/contracts';
3
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
4
+ /** Newsletter tag CRUD (feature 048, US3). */
5
+ export declare class NewsletterTagService {
6
+ #private;
7
+ private readonly emFactory;
8
+ private readonly auditLog?;
9
+ constructor(emFactory: () => EntityManager, auditLog?: AuditPort | undefined);
10
+ list(): Promise<NewsletterTagDto[]>;
11
+ create(input: CreateNewsletterTagRequest): Promise<NewsletterTagDto>;
12
+ update(id: string, input: UpdateNewsletterTagRequest): Promise<NewsletterTagDto>;
13
+ remove(id: string): Promise<void>;
14
+ private toDto;
15
+ }
16
+ //# sourceMappingURL=tag.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tag.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/tag.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAe,KAAK,0BAA0B,EAAE,KAAK,aAAa,IAAI,gBAAgB,EAAE,KAAK,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAInK,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE,8CAA8C;AAC9C,qBAAa,oBAAoB;;IAE7B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBADT,SAAS,EAAE,MAAM,aAAa,EAC9B,QAAQ,CAAC,EAAE,SAAS,YAAA;IAejC,IAAI,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAMnC,MAAM,CAAC,KAAK,EAAE,0BAA0B,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAepE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,0BAA0B,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAWhF,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBvC,OAAO,CAAC,KAAK;CAGd"}