@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,211 @@
1
+ import { defineModuleManifest, defineModuleSettingsManifest } from '@endora-commerce/contracts';
2
+ import { NEWSLETTER_SETTING_CODES } from '@endora-commerce/contracts';
3
+ /**
4
+ * Newsletter module (feature 048). Owns the subscriber list, tags, custom
5
+ * fields, campaigns, linear automations, reusable email-safe blocks, the
6
+ * bulk-sending provider (SMTP / Amazon SES via nodemailer), and engagement
7
+ * tracking. Bulk delivery is independent of the transactional-email transport.
8
+ * The SMTP connection + credentials come from a reusable `email_adapter`
9
+ * credential configuration referenced by `newsletter.email_credentials`
10
+ * (feature 058); when unset, dispatch uses the console (dev) sink.
11
+ */
12
+ export const newsletterSettingsManifest = defineModuleSettingsManifest({
13
+ moduleCode: 'newsletter',
14
+ groups: [{ code: 'newsletter', name: 'Newsletter' }],
15
+ settings: [
16
+ {
17
+ // Feature 073 — the operator's activation control. Platform-wide.
18
+ code: 'newsletter.enabled',
19
+ name: 'Newsletter enabled',
20
+ description: 'Switches the newsletter on or off: the storefront signup and confirmation links, the admin subscriber, campaign and automation screens, and bulk dispatch. Nothing is dropped — subscribers, their consent history, campaigns and stats stay in the database, and a campaign left mid-send resumes where it stopped.',
21
+ groupCode: 'newsletter',
22
+ valueType: 'boolean',
23
+ defaultValue: true,
24
+ },
25
+ {
26
+ code: NEWSLETTER_SETTING_CODES.OPT_IN_MODE,
27
+ name: 'Opt-in mode',
28
+ description: 'Single (immediate) or double (confirmation email) opt-in. Scopable per sales channel.',
29
+ groupCode: 'newsletter',
30
+ valueType: 'string',
31
+ enumOptions: ['single', 'double'],
32
+ defaultValue: 'double',
33
+ },
34
+ {
35
+ code: NEWSLETTER_SETTING_CODES.CONFIRM_TTL_HOURS,
36
+ name: 'Confirmation link TTL (hours)',
37
+ description: 'How long a double opt-in confirmation link stays valid before the pending subscriber expires.',
38
+ groupCode: 'newsletter',
39
+ valueType: 'number',
40
+ defaultValue: 168,
41
+ },
42
+ {
43
+ code: NEWSLETTER_SETTING_CODES.EMAIL_CREDENTIALS,
44
+ name: 'Email credentials',
45
+ description: 'Reference a reusable Email adapter credential configuration (Credentials screen) providing the SMTP host/port/security/username/password. Required to send real mail; when empty, newsletter dispatch uses the console (dev) sink.',
46
+ groupCode: 'newsletter',
47
+ valueType: 'credential_ref',
48
+ configurationType: 'email_adapter',
49
+ defaultValue: '',
50
+ },
51
+ {
52
+ code: NEWSLETTER_SETTING_CODES.SENDER_FROM_EMAIL,
53
+ name: 'Sender email',
54
+ description: 'Verified From address for newsletter mail.',
55
+ groupCode: 'newsletter',
56
+ valueType: 'string',
57
+ defaultValue: '',
58
+ hidden: true,
59
+ },
60
+ {
61
+ code: NEWSLETTER_SETTING_CODES.SENDER_FROM_NAME,
62
+ name: 'Sender name',
63
+ description: 'Display name shown in the From header.',
64
+ groupCode: 'newsletter',
65
+ valueType: 'string',
66
+ defaultValue: '',
67
+ hidden: true,
68
+ },
69
+ {
70
+ code: NEWSLETTER_SETTING_CODES.RATE_LIMIT_PER_SECOND,
71
+ name: 'Send rate limit (per second)',
72
+ description: 'Maximum messages dispatched per second (provider throttle).',
73
+ groupCode: 'newsletter',
74
+ valueType: 'number',
75
+ defaultValue: 14,
76
+ hidden: true,
77
+ },
78
+ ],
79
+ });
80
+ export const manifest = defineModuleManifest({
81
+ id: 'newsletter',
82
+ name: 'Newsletter',
83
+ description: 'Own-infrastructure newsletter: subscribers with tags + custom fields, campaigns, linear automations, email-safe templates and variables, engagement stats, and a configurable bulk-sending provider.',
84
+ version: '1.0.0',
85
+ // `cms` owns `cmsBlockSeedPort`, the seam this module's registration-consent
86
+ // block is kept in place through (feature 075 / D-87). Declared here rather
87
+ // than withheld as non-binding because the edge is binding in the direction
88
+ // that matters to an operator: the consent label the storefront renders on
89
+ // the registration form *is* that block, and a `newsletter` with no consent
90
+ // text to show is not a reduced newsletter but a form that cannot lawfully
91
+ // collect the consent.
92
+ dependencies: [
93
+ 'audit_logs',
94
+ 'auth',
95
+ 'cms',
96
+ 'credentials',
97
+ 'customers',
98
+ 'email',
99
+ 'sales_channels',
100
+ 'settings',
101
+ ],
102
+ settings: newsletterSettingsManifest,
103
+ activation: { settingCode: 'newsletter.enabled', default: true },
104
+ /**
105
+ * `ALREADY_SUBSCRIBED` — D-129's remaining sweep, MR 4
106
+ * (`specs/090-module-owned-error-codes/d129-sweep.md` §5.2, Appendix A;
107
+ * D-186 in `specs/080-f4-real-scope/rulings.md`). The first error code this
108
+ * module declares.
109
+ *
110
+ * It was `_i18n`'s until now, not because anybody judged it the platform's
111
+ * but because the deleted prefix chain had no rule for it and its last line
112
+ * was `return 'core'`. **D-121 T1 puts it here**: the noun is a subscription,
113
+ * and `NewsletterSubscriber` — with the double opt-in that decides when a
114
+ * second sign-up is a duplicate rather than a re-confirmation — is this
115
+ * module's own entity.
116
+ *
117
+ * **The attribution is the sweep's own word "weakly", and the reason to
118
+ * record it here is that the tree holds one counter-signal.** Nothing raises
119
+ * this code anywhere (class D). The only place it is named outside a
120
+ * declaration is a doc comment in `inventory`'s
121
+ * `availability-notification-service.ts`, which says the restock subscribe
122
+ * refuses `409 ALREADY_SUBSCRIBED` for an idempotent re-subscribe — and that
123
+ * method does not refuse at all: it returns the existing row
124
+ * (`if (existing) return existing;`). So the comment describes a refusal that
125
+ * does not exist, and the one competing claim on the noun is a claim nothing
126
+ * implements. If a back-in-stock subscription ever does start refusing, the
127
+ * question is open again and `REHOMED_ERROR_CODES` is where this decision is
128
+ * written down.
129
+ *
130
+ * **It arrives without a sentence, deliberately.** It carried a placeholder
131
+ * in `_i18n`'s bundle, the code rewritten twice —
132
+ * `"Already Subscribed."` and `"Błąd: already subscribed."` — which D-186 §2
133
+ * deletes rather than moves: in this module's own bundle it would read as
134
+ * this module's
135
+ * answer and every instrument would count the code as translated for good.
136
+ * `d129-sweep.md` §5.4 keeps writing real prose available and calls it the
137
+ * better outcome, and it is available whenever a raise site says what the
138
+ * refusal means. There is no raise site here, so a sentence could only be
139
+ * invented from the code's own name — the placeholder again in longer words,
140
+ * rendered for nobody. It is a `check-error-translations.ts`
141
+ * `UNTRANSLATED_ERROR_CODES` entry under `newsletter` instead, where the debt
142
+ * is findable and attached to whoever implements the refusal.
143
+ *
144
+ * No `tokens`, for the same reason and from the same direction: there is no
145
+ * raise site to put a `details.code` on the wire.
146
+ */
147
+ errorCodes: [{ code: 'ALREADY_SUBSCRIBED' }],
148
+ i18n: { bundlesDir: 'i18n' },
149
+ docs: { dir: 'docs' },
150
+ permissions: [
151
+ { code: 'newsletter:read', label: 'View newsletter' },
152
+ {
153
+ code: 'newsletter:write',
154
+ label: 'Manage newsletter subscribers, campaigns, automations, and provider',
155
+ },
156
+ ],
157
+ actions: [
158
+ {
159
+ id: 'open-newsletter',
160
+ labelKey: 'actions.openNewsletter.label',
161
+ descriptionKey: 'actions.openNewsletter.description',
162
+ icon: 'Inbox',
163
+ targetRoute: '/newsletter/subscribers',
164
+ requiredPermission: 'newsletter:read',
165
+ keywords: ['newsletter', 'campaign', 'subscribers', 'marketing', 'automation'],
166
+ weight: 230,
167
+ },
168
+ {
169
+ id: 'new-newsletter-campaign',
170
+ labelKey: 'actions.newCampaign.label',
171
+ descriptionKey: 'actions.newCampaign.description',
172
+ icon: 'Plus',
173
+ targetRoute: '/newsletter/campaigns/new',
174
+ requiredPermission: 'newsletter:write',
175
+ keywords: ['newsletter', 'campaign', 'new', 'create', 'send'],
176
+ weight: 231,
177
+ },
178
+ /**
179
+ * The two `AppShell.tsx` `PALETTE_ITEMS` rows feature 091's batch 11
180
+ * deletes, arriving as declarations (Principle XVI).
181
+ *
182
+ * Same destinations, same codes and the same keywords the hand-written
183
+ * rows carried, English and Polish alike — the palette an operator sees is
184
+ * unchanged. What changes is who answers for it: a hand-written row is a
185
+ * copy the server was never asked about, so it went on advertising these
186
+ * two screens after an operator switched `newsletter` off, and this module
187
+ * is one that really can be switched off.
188
+ */
189
+ {
190
+ id: 'open-newsletter-campaigns',
191
+ labelKey: 'actions.openCampaigns.label',
192
+ descriptionKey: 'actions.openCampaigns.description',
193
+ icon: 'Inbox',
194
+ targetRoute: '/newsletter/campaigns',
195
+ requiredPermission: 'newsletter:read',
196
+ keywords: ['newsletter', 'campaigns', 'email', 'marketing', 'kampanie'],
197
+ weight: 232,
198
+ },
199
+ {
200
+ id: 'open-newsletter-automations',
201
+ labelKey: 'actions.openAutomations.label',
202
+ descriptionKey: 'actions.openAutomations.description',
203
+ icon: 'Inbox',
204
+ targetRoute: '/newsletter/automations',
205
+ requiredPermission: 'newsletter:read',
206
+ keywords: ['newsletter', 'automations', 'workflow', 'automatyzacje'],
207
+ weight: 233,
208
+ },
209
+ ],
210
+ });
211
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ export declare class Migration20260629T200954NewsletterInit extends Migration {
3
+ up(): Promise<void>;
4
+ down(): Promise<void>;
5
+ }
6
+ //# sourceMappingURL=20260629T200954_newsletter_init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260629T200954_newsletter_init.d.ts","sourceRoot":"","sources":["../../src/migrations/20260629T200954_newsletter_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAuBlD,qBAAa,sCAAuC,SAAQ,SAAS;IACpD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAiRnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAerC"}
@@ -0,0 +1,261 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ import { randomUUID } from 'crypto';
3
+ import { defaultHeaderTree } from '@endora-commerce/email-components/defaults/default-header';
4
+ import { defaultFooterTree, envelopeFromTree } from '@endora-commerce/email-components/defaults/default-footer';
5
+ /**
6
+ * Feature 048 — Newsletter.
7
+ *
8
+ * Creates the full module schema: subscribers + tags + custom fields +
9
+ * suppressions, reusable email blocks (+ channel bridge), campaigns (+ group
10
+ * bridge), per-recipient send records (with the atomic-claim partial unique
11
+ * indexes that make dispatch idempotent under N≥2 workers — Principle X),
12
+ * engagement events, and automations (+ runs, with the `once` re-entry partial
13
+ * unique index). Seeds the system default header/footer blocks (FR-020/026).
14
+ */
15
+ const DEFAULT_LANGUAGES = ['en-US', 'pl-PL'];
16
+ const NEWSLETTER_DEFAULT_HEADER_CODE = 'newsletter_default_header';
17
+ const NEWSLETTER_DEFAULT_FOOTER_CODE = 'newsletter_default_footer';
18
+ function jsonbLiteral(value) {
19
+ return JSON.stringify(value).replace(/'/g, "''");
20
+ }
21
+ export class Migration20260629T200954NewsletterInit extends Migration {
22
+ async up() {
23
+ // --- Subscribers -------------------------------------------------------
24
+ this.addSql(`
25
+ create table "newsletter_subscribers" (
26
+ "id" uuid not null,
27
+ "email" varchar(320) not null,
28
+ "status" varchar(16) not null default 'pending',
29
+ "source" varchar(64) null,
30
+ "sales_channel_id" uuid null,
31
+ "customer_account_id" uuid null,
32
+ "custom_fields" jsonb not null default '{}',
33
+ "consent_at" timestamptz null,
34
+ "confirmed_at" timestamptz null,
35
+ "unsubscribed_at" timestamptz null,
36
+ "unsubscribe_reason" text null,
37
+ "deactivated_at" timestamptz null,
38
+ "version" int not null default 1,
39
+ "created_at" timestamptz not null,
40
+ "updated_at" timestamptz not null,
41
+ constraint "newsletter_subscribers_pkey" primary key ("id")
42
+ );
43
+ `);
44
+ this.addSql(`alter table "newsletter_subscribers" add constraint "newsletter_subscribers_email_unique" unique ("email");`);
45
+ this.addSql(`create index "newsletter_subscribers_status_idx" on "newsletter_subscribers" ("status");`);
46
+ this.addSql(`create index "newsletter_subscribers_channel_idx" on "newsletter_subscribers" ("sales_channel_id");`);
47
+ this.addSql(`create index "newsletter_subscribers_custom_fields_idx" on "newsletter_subscribers" using gin ("custom_fields");`);
48
+ // --- Tags + bridge -----------------------------------------------------
49
+ this.addSql(`
50
+ create table "newsletter_tags" (
51
+ "id" uuid not null,
52
+ "code" varchar(64) not null,
53
+ "name" varchar(160) not null,
54
+ "description" text null,
55
+ "created_at" timestamptz not null,
56
+ "updated_at" timestamptz not null,
57
+ constraint "newsletter_tags_pkey" primary key ("id")
58
+ );
59
+ `);
60
+ this.addSql(`alter table "newsletter_tags" add constraint "newsletter_tags_code_unique" unique ("code");`);
61
+ this.addSql(`
62
+ create table "newsletter_subscriber_tags" (
63
+ "subscriber_id" uuid not null,
64
+ "tag_id" uuid not null,
65
+ constraint "newsletter_subscriber_tags_pkey" primary key ("subscriber_id", "tag_id")
66
+ );
67
+ `);
68
+ this.addSql(`alter table "newsletter_subscriber_tags" add constraint "nst_subscriber_fk" ` +
69
+ `foreign key ("subscriber_id") references "newsletter_subscribers" ("id") on delete cascade;`);
70
+ this.addSql(`alter table "newsletter_subscriber_tags" add constraint "nst_tag_fk" ` +
71
+ `foreign key ("tag_id") references "newsletter_tags" ("id") on delete cascade;`);
72
+ this.addSql(`create index "nst_tag_idx" on "newsletter_subscriber_tags" ("tag_id");`);
73
+ // --- Custom fields -----------------------------------------------------
74
+ this.addSql(`
75
+ create table "newsletter_custom_fields" (
76
+ "id" uuid not null,
77
+ "key" varchar(64) not null,
78
+ "label" varchar(160) not null,
79
+ "type" varchar(16) not null,
80
+ "created_at" timestamptz not null,
81
+ "updated_at" timestamptz not null,
82
+ constraint "newsletter_custom_fields_pkey" primary key ("id")
83
+ );
84
+ `);
85
+ this.addSql(`alter table "newsletter_custom_fields" add constraint "newsletter_custom_fields_key_unique" unique ("key");`);
86
+ // --- Suppressions ------------------------------------------------------
87
+ this.addSql(`
88
+ create table "newsletter_suppressions" (
89
+ "id" uuid not null,
90
+ "email" varchar(320) not null,
91
+ "reason" varchar(16) not null,
92
+ "detail" text null,
93
+ "created_at" timestamptz not null,
94
+ constraint "newsletter_suppressions_pkey" primary key ("id")
95
+ );
96
+ `);
97
+ this.addSql(`alter table "newsletter_suppressions" add constraint "newsletter_suppressions_email_unique" unique ("email");`);
98
+ // --- Email blocks + bridge ---------------------------------------------
99
+ this.addSql(`
100
+ create table "newsletter_email_blocks" (
101
+ "id" uuid not null,
102
+ "code" varchar(180) not null,
103
+ "name" varchar(200) not null,
104
+ "description" text null,
105
+ "active" boolean not null default true,
106
+ "content" jsonb not null default '{}',
107
+ "is_system" boolean not null default false,
108
+ "version" int not null default 1,
109
+ "created_at" timestamptz not null,
110
+ "updated_at" timestamptz not null,
111
+ constraint "newsletter_email_blocks_pkey" primary key ("id")
112
+ );
113
+ `);
114
+ this.addSql(`create index "newsletter_email_blocks_code_idx" on "newsletter_email_blocks" ("code");`);
115
+ this.addSql(`
116
+ create table "newsletter_email_block_sales_channels" (
117
+ "block_id" uuid not null,
118
+ "sales_channel_id" uuid not null,
119
+ "code" varchar(180) not null,
120
+ constraint "newsletter_ebsc_pkey" primary key ("block_id", "sales_channel_id")
121
+ );
122
+ `);
123
+ this.addSql(`alter table "newsletter_email_block_sales_channels" add constraint "newsletter_ebsc_block_fk" ` +
124
+ `foreign key ("block_id") references "newsletter_email_blocks" ("id") on delete cascade;`);
125
+ this.addSql(`create unique index "newsletter_ebsc_channel_code_unique" on "newsletter_email_block_sales_channels" ("sales_channel_id", "code");`);
126
+ // --- Campaigns + group bridge ------------------------------------------
127
+ this.addSql(`
128
+ create table "newsletter_campaigns" (
129
+ "id" uuid not null,
130
+ "name" varchar(200) not null,
131
+ "subject" text not null,
132
+ "content" jsonb not null default '{}',
133
+ "sales_channel_id" uuid null,
134
+ "language" varchar(12) not null,
135
+ "target_type" varchar(16) not null default 'all',
136
+ "target_tag_ids" jsonb not null default '[]',
137
+ "tracking_enabled" boolean not null default true,
138
+ "status" varchar(16) not null default 'draft',
139
+ "scheduled_at" timestamptz null,
140
+ "dispatch_job_id" varchar(128) null,
141
+ "stats" jsonb not null default '{}',
142
+ "version" int not null default 1,
143
+ "created_at" timestamptz not null,
144
+ "updated_at" timestamptz not null,
145
+ constraint "newsletter_campaigns_pkey" primary key ("id")
146
+ );
147
+ `);
148
+ this.addSql(`create index "newsletter_campaigns_status_idx" on "newsletter_campaigns" ("status");`);
149
+ this.addSql(`
150
+ create table "newsletter_campaign_subscribers" (
151
+ "campaign_id" uuid not null,
152
+ "subscriber_id" uuid not null,
153
+ constraint "newsletter_campaign_subscribers_pkey" primary key ("campaign_id", "subscriber_id")
154
+ );
155
+ `);
156
+ this.addSql(`alter table "newsletter_campaign_subscribers" add constraint "ncs_campaign_fk" ` +
157
+ `foreign key ("campaign_id") references "newsletter_campaigns" ("id") on delete cascade;`);
158
+ this.addSql(`alter table "newsletter_campaign_subscribers" add constraint "ncs_subscriber_fk" ` +
159
+ `foreign key ("subscriber_id") references "newsletter_subscribers" ("id") on delete cascade;`);
160
+ // --- Automations + runs ------------------------------------------------
161
+ this.addSql(`
162
+ create table "newsletter_automations" (
163
+ "id" uuid not null,
164
+ "name" varchar(200) not null,
165
+ "status" varchar(16) not null default 'draft',
166
+ "trigger_type" varchar(16) not null default 'all',
167
+ "trigger_tag_ids" jsonb not null default '[]',
168
+ "sales_channel_id" uuid null,
169
+ "language" varchar(12) not null,
170
+ "reentry_policy" varchar(16) not null default 'once',
171
+ "steps" jsonb not null default '[]',
172
+ "version" int not null default 1,
173
+ "created_at" timestamptz not null,
174
+ "updated_at" timestamptz not null,
175
+ constraint "newsletter_automations_pkey" primary key ("id")
176
+ );
177
+ `);
178
+ this.addSql(`
179
+ create table "newsletter_automation_runs" (
180
+ "id" uuid not null,
181
+ "automation_id" uuid not null,
182
+ "subscriber_id" uuid not null,
183
+ "current_step" int not null default 0,
184
+ "status" varchar(16) not null default 'active',
185
+ "next_step_at" timestamptz null,
186
+ "next_step_job_id" varchar(128) null,
187
+ "created_at" timestamptz not null,
188
+ "updated_at" timestamptz not null,
189
+ constraint "newsletter_automation_runs_pkey" primary key ("id")
190
+ );
191
+ `);
192
+ this.addSql(`alter table "newsletter_automation_runs" add constraint "nar_automation_fk" ` +
193
+ `foreign key ("automation_id") references "newsletter_automations" ("id") on delete cascade;`);
194
+ this.addSql(`alter table "newsletter_automation_runs" add constraint "nar_subscriber_fk" ` +
195
+ `foreign key ("subscriber_id") references "newsletter_subscribers" ("id") on delete cascade;`);
196
+ this.addSql(`create index "nar_automation_idx" on "newsletter_automation_runs" ("automation_id");`);
197
+ this.addSql(`create unique index "nar_once_unique" on "newsletter_automation_runs" ("automation_id", "subscriber_id") where "status" <> 'cancelled';`);
198
+ // --- Send records (atomic claim) + engagement events -------------------
199
+ this.addSql(`
200
+ create table "newsletter_send_records" (
201
+ "id" uuid not null,
202
+ "campaign_id" uuid null,
203
+ "automation_run_id" uuid null,
204
+ "step_index" int null,
205
+ "subscriber_id" uuid not null,
206
+ "status" varchar(16) not null default 'queued',
207
+ "provider_message_id" varchar(255) null,
208
+ "error" text null,
209
+ "opened_at" timestamptz null,
210
+ "click_count" int not null default 0,
211
+ "sent_at" timestamptz null,
212
+ "created_at" timestamptz not null,
213
+ constraint "newsletter_send_records_pkey" primary key ("id")
214
+ );
215
+ `);
216
+ this.addSql(`alter table "newsletter_send_records" add constraint "nsr_subscriber_fk" ` +
217
+ `foreign key ("subscriber_id") references "newsletter_subscribers" ("id") on delete cascade;`);
218
+ this.addSql(`create index "nsr_subscriber_idx" on "newsletter_send_records" ("subscriber_id");`);
219
+ this.addSql(`create index "nsr_status_idx" on "newsletter_send_records" ("status");`);
220
+ this.addSql(`create unique index "nsr_campaign_claim_unique" on "newsletter_send_records" ("campaign_id", "subscriber_id") where "campaign_id" is not null;`);
221
+ this.addSql(`create unique index "nsr_automation_claim_unique" on "newsletter_send_records" ("automation_run_id", "step_index", "subscriber_id") where "automation_run_id" is not null;`);
222
+ this.addSql(`
223
+ create table "newsletter_engagement_events" (
224
+ "id" uuid not null,
225
+ "send_record_id" uuid not null,
226
+ "type" varchar(8) not null,
227
+ "link_id" varchar(64) null,
228
+ "url" text null,
229
+ "occurred_at" timestamptz not null,
230
+ constraint "newsletter_engagement_events_pkey" primary key ("id")
231
+ );
232
+ `);
233
+ this.addSql(`alter table "newsletter_engagement_events" add constraint "nee_send_record_fk" ` +
234
+ `foreign key ("send_record_id") references "newsletter_send_records" ("id") on delete cascade;`);
235
+ this.addSql(`create index "nee_send_record_idx" on "newsletter_engagement_events" ("send_record_id");`);
236
+ this.addSql(`create index "nee_type_idx" on "newsletter_engagement_events" ("type");`);
237
+ // --- Seed system default header + footer blocks (FR-020/026) -----------
238
+ const header = envelopeFromTree(defaultHeaderTree(), DEFAULT_LANGUAGES);
239
+ const footer = envelopeFromTree(defaultFooterTree(), DEFAULT_LANGUAGES);
240
+ this.addSql(`insert into "newsletter_email_blocks" ("id", "code", "name", "active", "content", "is_system", "version", "created_at", "updated_at") ` +
241
+ `values ('${randomUUID()}', '${NEWSLETTER_DEFAULT_HEADER_CODE}', 'Newsletter default header', true, '${jsonbLiteral(header)}'::jsonb, true, 1, now(), now());`);
242
+ this.addSql(`insert into "newsletter_email_blocks" ("id", "code", "name", "active", "content", "is_system", "version", "created_at", "updated_at") ` +
243
+ `values ('${randomUUID()}', '${NEWSLETTER_DEFAULT_FOOTER_CODE}', 'Newsletter default footer', true, '${jsonbLiteral(footer)}'::jsonb, true, 1, now(), now());`);
244
+ }
245
+ async down() {
246
+ this.addSql('drop table if exists "newsletter_engagement_events" cascade;');
247
+ this.addSql('drop table if exists "newsletter_send_records" cascade;');
248
+ this.addSql('drop table if exists "newsletter_automation_runs" cascade;');
249
+ this.addSql('drop table if exists "newsletter_automations" cascade;');
250
+ this.addSql('drop table if exists "newsletter_campaign_subscribers" cascade;');
251
+ this.addSql('drop table if exists "newsletter_campaigns" cascade;');
252
+ this.addSql('drop table if exists "newsletter_email_block_sales_channels" cascade;');
253
+ this.addSql('drop table if exists "newsletter_email_blocks" cascade;');
254
+ this.addSql('drop table if exists "newsletter_suppressions" cascade;');
255
+ this.addSql('drop table if exists "newsletter_custom_fields" cascade;');
256
+ this.addSql('drop table if exists "newsletter_subscriber_tags" cascade;');
257
+ this.addSql('drop table if exists "newsletter_tags" cascade;');
258
+ this.addSql('drop table if exists "newsletter_subscribers" cascade;');
259
+ }
260
+ }
261
+ //# sourceMappingURL=20260629T200954_newsletter_init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260629T200954_newsletter_init.js","sourceRoot":"","sources":["../../src/migrations/20260629T200954_newsletter_init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,116 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * D-187 — `newsletter_subscribers` gains its organisation, and a row that names
4
+ * a customer account carries one.
5
+ *
6
+ * Feature 087 Group B, class 4 of 4 and the last of the five
7
+ * (`specs/087-tenant-scope-enforcement/`). `NewsletterSubscriber` is
8
+ * `@CustomerScoped`, and until this migration the `allowed-set` arm of
9
+ * `customerFilterCond` had no column to grant on: a sales representative
10
+ * assigned to the buyer's own organisation was shown **none** of their
11
+ * subscribers. The column is what turns that into an answer.
12
+ *
13
+ * ## The column and the read arrive together, on purpose
14
+ *
15
+ * `customerOrganizationColumn` asks the ORM's own metadata whether the filtered
16
+ * entity carries `organizationId`, **per query**, so the entity property in
17
+ * this same merge request is what switches the grant on — there is no third
18
+ * artefact, no flag and no staging. At the same instant the
19
+ * `ORGANIZATION_ATTRIBUTION_PENDING` disclosure retires itself: it is keyed on
20
+ * the column's *absence*.
21
+ *
22
+ * That pairing is why this migration is not "column and backfill". From the
23
+ * moment the grant is live, a row inserted without an organisation is invisible
24
+ * to the representative who serves that organisation, on a screen that has just
25
+ * stopped explaining itself — and MikroORM applies no filter to `INSERT`
26
+ * (`r1-spike.md` §4, measured), so the filter cannot refuse it. The `CHECK`
27
+ * below is the only refusal an `INSERT` has.
28
+ *
29
+ * ## An implication, not an equivalence
30
+ *
31
+ * `customer_account_id is null or organization_id is not null` is FR-010 and
32
+ * FR-011 together and nothing wider: an **owned** row has an organisation, an
33
+ * **ownerless** row need not. This table is ownerless by construction for the
34
+ * storefront's sign-up form — `POST /api/v1/newsletter/subscribe` passes no
35
+ * account at all, so every subscriber who has never signed in is such a row,
36
+ * and on this table they are the majority rather than the exception. Who they
37
+ * belong to is R-6's open question. The equivalence would answer it in the
38
+ * schema, which D-187 declines to do.
39
+ *
40
+ * The shape the implication leaves open — an **ownerless** row still carrying
41
+ * an organisation — is unreachable here, for `inventory`'s reason rather than
42
+ * `pwa`'s. `subscriber.service.ts` writes `customer_account_id` in exactly two
43
+ * places and both of them *acquire* an owner: the `em.create` on first
44
+ * subscribe, and the adoption line that fills in the account when a subscriber
45
+ * who signed up anonymously later signs in. Nothing in this module ever clears
46
+ * it (grep of `packages/modules/newsletter/src` for `customerAccountId`: two
47
+ * writes, both assignments of a non-null id, the rest reads). So there is no
48
+ * de-association direction for the constraint to be silent about.
49
+ *
50
+ * ## The e-mail address is not a derivation
51
+ *
52
+ * `newsletter_subscribers.email` is globally unique and `customer_accounts`
53
+ * has an `email` too, so the backfill below could have matched them and
54
+ * attributed far more rows. It deliberately does not. That match would claim
55
+ * rows the schema does not link, silently and irreversibly, for whoever
56
+ * happened to sign up with the address their employer later registered — it is
57
+ * R-6 answer (2) guessed rather than decided. The only derivation is
58
+ * `customer_account_id`.
59
+ *
60
+ * ## No foreign key
61
+ *
62
+ * Matching `Cart`, `Comparison`, `PushSubscription` and
63
+ * `AvailabilityNotification`, none of which carries one. D-187 withdraws
64
+ * `r1-spike.md` §8's `on delete set null` recommendation as contradicting
65
+ * FR-011: it produces a row that names an account and no organisation, which is
66
+ * precisely the state this migration makes unreachable, and under the
67
+ * constraint below it would abort an unrelated organisation delete with a
68
+ * message about a table the operator was not touching. If one is ever taken
69
+ * here it must be `restrict`, and it is not this migration's decision.
70
+ *
71
+ * ## Derive, then count, then refuse — never delete
72
+ *
73
+ * The derivation is the owning account's own organisation. **On this table the
74
+ * refusal is a real branch, not a formality**: like `push_subscriptions` and
75
+ * `availability_notifications`, and unlike `comparisons`,
76
+ * `newsletter_subscribers` has **no foreign key** on `customer_account_id` —
77
+ * `20260629T200954_newsletter_init.ts` declares it `uuid null` and constrains
78
+ * nothing — so a row naming an account that is gone is representable here.
79
+ *
80
+ * It is nevertheless empty today, and the reason is worth knowing rather than
81
+ * hoping: a customer account is never hard-deleted in this tree.
82
+ * `customer-account-lifecycle-ports.ts` soft-deletes, restores and
83
+ * **anonymises** — the last one rewrites the e-mail and the name and keeps the
84
+ * row — and every one of those still satisfies `organization_id NOT NULL`
85
+ * (D-178), so it still derives. Change any of that and the count below is the
86
+ * only thing that would notice. The anonymise path is worth a second look on
87
+ * *this* table in particular, because it rewrites the very column an e-mail
88
+ * match would have keyed on; deriving from `customer_account_id` is unaffected
89
+ * by it.
90
+ *
91
+ * It raises with the count and up to twenty ids and deletes nothing (D-184), in
92
+ * the shape `20260825T141659_customer_accounts_organization_required.ts`
93
+ * established and the three Group B migrations before this one repeated.
94
+ * Deleting would be particularly wrong here: a subscriber row is a consent
95
+ * record, and dropping one destroys the evidence that the address may be
96
+ * mailed at all.
97
+ *
98
+ * ## Nothing to declare in the manifest
99
+ *
100
+ * `newsletter` declares `customers` in its manifest `dependencies`, whose
101
+ * transitive closure reaches `customer_accounts` and `organizations`, so the
102
+ * table this reads is created before this runs. No foreign key is added, so
103
+ * `fk-dependency-drift` has nothing to say either, and a migration naming
104
+ * another module's table is outside `check:module-boundary`'s population by
105
+ * that check's own rule.
106
+ *
107
+ * The standing guard is
108
+ * `backend/test/integration/tenancy/customer-scoped-organization-completeness.test.ts`,
109
+ * which derives its population from the ORM's metadata rather than from a list
110
+ * — so this class is covered by gaining the column, with no edit to that file.
111
+ */
112
+ export declare class Migration20260830T212736NewsletterOrganizationAttribution extends Migration {
113
+ up(): Promise<void>;
114
+ down(): Promise<void>;
115
+ }
116
+ //# sourceMappingURL=20260830T212736_newsletter_organization_attribution.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260830T212736_newsletter_organization_attribution.d.ts","sourceRoot":"","sources":["../../src/migrations/20260830T212736_newsletter_organization_attribution.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6GG;AACH,qBAAa,yDAA0D,SAAQ,SAAS;IACvE,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAoEnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAarC"}