@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.
- package/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/admin/api/newsletter-client.d.ts +64 -0
- package/dist/admin/api/newsletter-client.d.ts.map +1 -0
- package/dist/admin/api/newsletter-client.js +71 -0
- package/dist/admin/api/newsletter-client.js.map +1 -0
- package/dist/admin/email-variables.d.ts +21 -0
- package/dist/admin/email-variables.d.ts.map +1 -0
- package/dist/admin/email-variables.js +48 -0
- package/dist/admin/email-variables.js.map +1 -0
- package/dist/admin/index.d.ts +44 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +149 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/AutomationBuilder.d.ts +8 -0
- package/dist/admin/pages/AutomationBuilder.d.ts.map +1 -0
- package/dist/admin/pages/AutomationBuilder.js +131 -0
- package/dist/admin/pages/AutomationBuilder.js.map +1 -0
- package/dist/admin/pages/AutomationsPage.d.ts +8 -0
- package/dist/admin/pages/AutomationsPage.d.ts.map +1 -0
- package/dist/admin/pages/AutomationsPage.js +43 -0
- package/dist/admin/pages/AutomationsPage.js.map +1 -0
- package/dist/admin/pages/BlocksPage.d.ts +8 -0
- package/dist/admin/pages/BlocksPage.d.ts.map +1 -0
- package/dist/admin/pages/BlocksPage.js +110 -0
- package/dist/admin/pages/BlocksPage.js.map +1 -0
- package/dist/admin/pages/CampaignEditor.d.ts +8 -0
- package/dist/admin/pages/CampaignEditor.d.ts.map +1 -0
- package/dist/admin/pages/CampaignEditor.js +144 -0
- package/dist/admin/pages/CampaignEditor.js.map +1 -0
- package/dist/admin/pages/CampaignStats.d.ts +8 -0
- package/dist/admin/pages/CampaignStats.d.ts.map +1 -0
- package/dist/admin/pages/CampaignStats.js +45 -0
- package/dist/admin/pages/CampaignStats.js.map +1 -0
- package/dist/admin/pages/CampaignsPage.d.ts +8 -0
- package/dist/admin/pages/CampaignsPage.d.ts.map +1 -0
- package/dist/admin/pages/CampaignsPage.js +28 -0
- package/dist/admin/pages/CampaignsPage.js.map +1 -0
- package/dist/admin/pages/ProviderSettingsPage.d.ts +8 -0
- package/dist/admin/pages/ProviderSettingsPage.d.ts.map +1 -0
- package/dist/admin/pages/ProviderSettingsPage.js +64 -0
- package/dist/admin/pages/ProviderSettingsPage.js.map +1 -0
- package/dist/admin/pages/SubscribersPage.d.ts +8 -0
- package/dist/admin/pages/SubscribersPage.d.ts.map +1 -0
- package/dist/admin/pages/SubscribersPage.js +55 -0
- package/dist/admin/pages/SubscribersPage.js.map +1 -0
- package/dist/admin/pages/TagsPage.d.ts +8 -0
- package/dist/admin/pages/TagsPage.d.ts.map +1 -0
- package/dist/admin/pages/TagsPage.js +114 -0
- package/dist/admin/pages/TagsPage.js.map +1 -0
- package/dist/backend/entities/newsletter-automation-run.entity.d.ts +19 -0
- package/dist/backend/entities/newsletter-automation-run.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-automation-run.entity.js +72 -0
- package/dist/backend/entities/newsletter-automation-run.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-automation.entity.d.ts +22 -0
- package/dist/backend/entities/newsletter-automation.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-automation.entity.js +86 -0
- package/dist/backend/entities/newsletter-automation.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-campaign-subscriber.entity.d.ts +6 -0
- package/dist/backend/entities/newsletter-campaign-subscriber.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-campaign-subscriber.entity.js +30 -0
- package/dist/backend/entities/newsletter-campaign-subscriber.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-campaign.entity.d.ts +22 -0
- package/dist/backend/entities/newsletter-campaign.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-campaign.entity.js +102 -0
- package/dist/backend/entities/newsletter-campaign.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-custom-field.entity.d.ts +12 -0
- package/dist/backend/entities/newsletter-custom-field.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-custom-field.entity.js +53 -0
- package/dist/backend/entities/newsletter-custom-field.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-email-block-sales-channel.entity.d.ts +7 -0
- package/dist/backend/entities/newsletter-email-block-sales-channel.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-email-block-sales-channel.entity.js +35 -0
- package/dist/backend/entities/newsletter-email-block-sales-channel.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-email-block.entity.d.ts +21 -0
- package/dist/backend/entities/newsletter-email-block.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-email-block.entity.js +78 -0
- package/dist/backend/entities/newsletter-email-block.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-engagement-event.entity.d.ts +12 -0
- package/dist/backend/entities/newsletter-engagement-event.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-engagement-event.entity.js +54 -0
- package/dist/backend/entities/newsletter-engagement-event.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-send-record.entity.d.ts +23 -0
- package/dist/backend/entities/newsletter-send-record.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-send-record.entity.js +89 -0
- package/dist/backend/entities/newsletter-send-record.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-subscriber-tag.entity.d.ts +6 -0
- package/dist/backend/entities/newsletter-subscriber-tag.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-subscriber-tag.entity.js +31 -0
- package/dist/backend/entities/newsletter-subscriber-tag.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-subscriber.entity.d.ts +63 -0
- package/dist/backend/entities/newsletter-subscriber.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-subscriber.entity.js +147 -0
- package/dist/backend/entities/newsletter-subscriber.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-suppression.entity.d.ts +14 -0
- package/dist/backend/entities/newsletter-suppression.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-suppression.entity.js +51 -0
- package/dist/backend/entities/newsletter-suppression.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-tag.entity.d.ts +12 -0
- package/dist/backend/entities/newsletter-tag.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-tag.entity.js +53 -0
- package/dist/backend/entities/newsletter-tag.entity.js.map +1 -0
- package/dist/backend/index.d.ts +120 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +139 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +114 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +171 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +27 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +118 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.self.d.ts +15 -0
- package/dist/backend/routes.self.d.ts.map +1 -0
- package/dist/backend/routes.self.js +28 -0
- package/dist/backend/routes.self.js.map +1 -0
- package/dist/backend/routes.storefront.d.ts +24 -0
- package/dist/backend/routes.storefront.d.ts.map +1 -0
- package/dist/backend/routes.storefront.js +79 -0
- package/dist/backend/routes.storefront.js.map +1 -0
- package/dist/backend/services/audience-resolver.d.ts +20 -0
- package/dist/backend/services/audience-resolver.d.ts.map +1 -0
- package/dist/backend/services/audience-resolver.js +41 -0
- package/dist/backend/services/audience-resolver.js.map +1 -0
- package/dist/backend/services/automation.service.d.ts +58 -0
- package/dist/backend/services/automation.service.d.ts.map +1 -0
- package/dist/backend/services/automation.service.js +301 -0
- package/dist/backend/services/automation.service.js.map +1 -0
- package/dist/backend/services/campaign-dispatch.service.d.ts +50 -0
- package/dist/backend/services/campaign-dispatch.service.d.ts.map +1 -0
- package/dist/backend/services/campaign-dispatch.service.js +130 -0
- package/dist/backend/services/campaign-dispatch.service.js.map +1 -0
- package/dist/backend/services/campaign.service.d.ts +40 -0
- package/dist/backend/services/campaign.service.d.ts.map +1 -0
- package/dist/backend/services/campaign.service.js +195 -0
- package/dist/backend/services/campaign.service.js.map +1 -0
- package/dist/backend/services/consent-block-seeder.d.ts +30 -0
- package/dist/backend/services/consent-block-seeder.d.ts.map +1 -0
- package/dist/backend/services/consent-block-seeder.js +52 -0
- package/dist/backend/services/consent-block-seeder.js.map +1 -0
- package/dist/backend/services/content.service.d.ts +52 -0
- package/dist/backend/services/content.service.d.ts.map +1 -0
- package/dist/backend/services/content.service.js +48 -0
- package/dist/backend/services/content.service.js.map +1 -0
- package/dist/backend/services/custom-field.service.d.ts +16 -0
- package/dist/backend/services/custom-field.service.d.ts.map +1 -0
- package/dist/backend/services/custom-field.service.js +67 -0
- package/dist/backend/services/custom-field.service.js.map +1 -0
- package/dist/backend/services/email-block.service.d.ts +26 -0
- package/dist/backend/services/email-block.service.d.ts.map +1 -0
- package/dist/backend/services/email-block.service.js +120 -0
- package/dist/backend/services/email-block.service.js.map +1 -0
- package/dist/backend/services/opt-in.service.d.ts +25 -0
- package/dist/backend/services/opt-in.service.d.ts.map +1 -0
- package/dist/backend/services/opt-in.service.js +46 -0
- package/dist/backend/services/opt-in.service.js.map +1 -0
- package/dist/backend/services/provider/console-provider.d.ts +32 -0
- package/dist/backend/services/provider/console-provider.d.ts.map +1 -0
- package/dist/backend/services/provider/console-provider.js +31 -0
- package/dist/backend/services/provider/console-provider.js.map +1 -0
- package/dist/backend/services/provider/provider-registry.d.ts +51 -0
- package/dist/backend/services/provider/provider-registry.d.ts.map +1 -0
- package/dist/backend/services/provider/provider-registry.js +84 -0
- package/dist/backend/services/provider/provider-registry.js.map +1 -0
- package/dist/backend/services/provider/smtp-provider.d.ts +30 -0
- package/dist/backend/services/provider/smtp-provider.d.ts.map +1 -0
- package/dist/backend/services/provider/smtp-provider.js +42 -0
- package/dist/backend/services/provider/smtp-provider.js.map +1 -0
- package/dist/backend/services/provider-admin.service.d.ts +39 -0
- package/dist/backend/services/provider-admin.service.d.ts.map +1 -0
- package/dist/backend/services/provider-admin.service.js +59 -0
- package/dist/backend/services/provider-admin.service.js.map +1 -0
- package/dist/backend/services/provider-admin.types.d.ts +6 -0
- package/dist/backend/services/provider-admin.types.d.ts.map +1 -0
- package/dist/backend/services/provider-admin.types.js +2 -0
- package/dist/backend/services/provider-admin.types.js.map +1 -0
- package/dist/backend/services/queues/newsletter-queues.d.ts +34 -0
- package/dist/backend/services/queues/newsletter-queues.d.ts.map +1 -0
- package/dist/backend/services/queues/newsletter-queues.js +58 -0
- package/dist/backend/services/queues/newsletter-queues.js.map +1 -0
- package/dist/backend/services/self.service.d.ts +17 -0
- package/dist/backend/services/self.service.d.ts.map +1 -0
- package/dist/backend/services/self.service.js +44 -0
- package/dist/backend/services/self.service.js.map +1 -0
- package/dist/backend/services/stats.service.d.ts +10 -0
- package/dist/backend/services/stats.service.d.ts.map +1 -0
- package/dist/backend/services/stats.service.js +45 -0
- package/dist/backend/services/stats.service.js.map +1 -0
- package/dist/backend/services/subscriber-admin.service.d.ts +26 -0
- package/dist/backend/services/subscriber-admin.service.d.ts.map +1 -0
- package/dist/backend/services/subscriber-admin.service.js +163 -0
- package/dist/backend/services/subscriber-admin.service.js.map +1 -0
- package/dist/backend/services/subscriber.service.d.ts +91 -0
- package/dist/backend/services/subscriber.service.d.ts.map +1 -0
- package/dist/backend/services/subscriber.service.js +280 -0
- package/dist/backend/services/subscriber.service.js.map +1 -0
- package/dist/backend/services/tag.service.d.ts +16 -0
- package/dist/backend/services/tag.service.d.ts.map +1 -0
- package/dist/backend/services/tag.service.js +79 -0
- package/dist/backend/services/tag.service.js.map +1 -0
- package/dist/backend/services/token.helper.d.ts +32 -0
- package/dist/backend/services/token.helper.d.ts.map +1 -0
- package/dist/backend/services/token.helper.js +76 -0
- package/dist/backend/services/token.helper.js.map +1 -0
- package/dist/backend/services/tracking.service.d.ts +14 -0
- package/dist/backend/services/tracking.service.d.ts.map +1 -0
- package/dist/backend/services/tracking.service.js +45 -0
- package/dist/backend/services/tracking.service.js.map +1 -0
- package/dist/manifest.d.ts +192 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +211 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260629T200954_newsletter_init.d.ts +6 -0
- package/dist/migrations/20260629T200954_newsletter_init.d.ts.map +1 -0
- package/dist/migrations/20260629T200954_newsletter_init.js +261 -0
- package/dist/migrations/20260629T200954_newsletter_init.js.map +1 -0
- package/dist/migrations/20260830T212736_newsletter_organization_attribution.d.ts +116 -0
- package/dist/migrations/20260830T212736_newsletter_organization_attribution.d.ts.map +1 -0
- package/dist/migrations/20260830T212736_newsletter_organization_attribution.js +186 -0
- package/dist/migrations/20260830T212736_newsletter_organization_attribution.js.map +1 -0
- package/dist/migrations/20260903T101752_newsletter_namespace_block_names.d.ts +6 -0
- package/dist/migrations/20260903T101752_newsletter_namespace_block_names.d.ts.map +1 -0
- package/dist/migrations/20260903T101752_newsletter_namespace_block_names.js +58 -0
- package/dist/migrations/20260903T101752_newsletter_namespace_block_names.js.map +1 -0
- package/dist/migrations/20260912T125702_newsletter_subscriber_tenant_scope_index.d.ts +23 -0
- package/dist/migrations/20260912T125702_newsletter_subscriber_tenant_scope_index.d.ts.map +1 -0
- package/dist/migrations/20260912T125702_newsletter_subscriber_tenant_scope_index.js +27 -0
- package/dist/migrations/20260912T125702_newsletter_subscriber_tenant_scope_index.js.map +1 -0
- package/dist/migrations/index.d.ts +30 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +35 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/newsletter.md +94 -0
- package/i18n/en.json +27 -0
- package/i18n/pl.json +27 -0
- package/package.json +119 -0
- 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"}
|