@openlup/platform 0.16.0 → 0.17.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 (296) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +285 -5
  3. package/dist/communications/accountingDocumentDelivery.d.ts +35 -0
  4. package/dist/communications/accountingDocumentDelivery.js +33 -0
  5. package/dist/communications/authEmailError.d.ts +5 -0
  6. package/dist/communications/authEmailError.js +8 -0
  7. package/dist/communications/authEmailPorts.d.ts +2 -0
  8. package/dist/communications/authEmailPorts.js +1 -0
  9. package/dist/communications/authSendEmailHook.d.ts +35 -0
  10. package/dist/communications/authSendEmailHook.js +250 -0
  11. package/dist/communications/capturedTransactionalDelivery.d.ts +13 -0
  12. package/dist/communications/capturedTransactionalDelivery.js +14 -0
  13. package/dist/communications/communicationSync.d.ts +11 -0
  14. package/dist/communications/communicationSync.js +79 -0
  15. package/dist/communications/communicationSyncWorker.d.ts +68 -0
  16. package/dist/communications/communicationSyncWorker.js +218 -0
  17. package/dist/communications/consentPolicy.d.ts +19 -0
  18. package/dist/communications/consentPolicy.js +43 -0
  19. package/dist/communications/consentPurposeRegistry.d.ts +4 -0
  20. package/dist/communications/consentPurposeRegistry.js +24 -0
  21. package/dist/communications/contracts/abandonedCart.d.ts +27 -0
  22. package/dist/communications/contracts/abandonedCart.js +25 -0
  23. package/dist/communications/contracts/authEmail.d.ts +62 -0
  24. package/dist/communications/contracts/authEmail.js +1 -0
  25. package/dist/communications/contracts/backInStock.d.ts +28 -0
  26. package/dist/communications/contracts/backInStock.js +28 -0
  27. package/dist/communications/contracts/blocks.d.ts +24 -0
  28. package/dist/communications/contracts/blocks.js +23 -0
  29. package/dist/communications/contracts/checkoutExpired.d.ts +22 -0
  30. package/dist/communications/contracts/checkoutExpired.js +25 -0
  31. package/dist/communications/contracts/checkoutRecovery.d.ts +26 -0
  32. package/dist/communications/contracts/checkoutRecovery.js +37 -0
  33. package/dist/communications/contracts/commerceEmailContent.d.ts +176 -0
  34. package/dist/communications/contracts/commerceEmailContent.js +1 -0
  35. package/dist/communications/contracts/commerceEngagementEmailContent.d.ts +95 -0
  36. package/dist/communications/contracts/commerceEngagementEmailContent.js +1 -0
  37. package/dist/communications/contracts/consent.d.ts +12 -0
  38. package/dist/communications/contracts/consent.js +1 -0
  39. package/dist/communications/contracts/email.d.ts +182 -0
  40. package/dist/communications/contracts/email.js +1 -0
  41. package/dist/communications/contracts/guideCard.d.ts +3 -0
  42. package/dist/communications/contracts/guideCard.js +26 -0
  43. package/dist/communications/contracts/index.d.ts +51 -0
  44. package/dist/communications/contracts/index.js +42 -0
  45. package/dist/communications/contracts/inquiryContent.d.ts +32 -0
  46. package/dist/communications/contracts/inquiryContent.js +33 -0
  47. package/dist/communications/contracts/invoiceDocumentContent.d.ts +26 -0
  48. package/dist/communications/contracts/invoiceDocumentContent.js +36 -0
  49. package/dist/communications/contracts/marketingAutomation.d.ts +52 -0
  50. package/dist/communications/contracts/marketingAutomation.js +1 -0
  51. package/dist/communications/contracts/newsletter.d.ts +63 -0
  52. package/dist/communications/contracts/newsletter.js +1 -0
  53. package/dist/communications/contracts/newsletterWebhook.d.ts +26 -0
  54. package/dist/communications/contracts/newsletterWebhook.js +1 -0
  55. package/dist/communications/contracts/optOutContent.d.ts +11 -0
  56. package/dist/communications/contracts/optOutContent.js +17 -0
  57. package/dist/communications/contracts/orderCanceled.d.ts +25 -0
  58. package/dist/communications/contracts/orderCanceled.js +27 -0
  59. package/dist/communications/contracts/orderDraft.d.ts +46 -0
  60. package/dist/communications/contracts/orderDraft.js +46 -0
  61. package/dist/communications/contracts/orderPaid.d.ts +53 -0
  62. package/dist/communications/contracts/orderPaid.js +71 -0
  63. package/dist/communications/contracts/orderRefunded.d.ts +24 -0
  64. package/dist/communications/contracts/orderRefunded.js +27 -0
  65. package/dist/communications/contracts/paymentFailed.d.ts +24 -0
  66. package/dist/communications/contracts/paymentFailed.js +24 -0
  67. package/dist/communications/contracts/publicIntake.d.ts +49 -0
  68. package/dist/communications/contracts/publicIntake.js +1 -0
  69. package/dist/communications/contracts/render.d.ts +2 -0
  70. package/dist/communications/contracts/render.js +229 -0
  71. package/dist/communications/contracts/reorderReminder.d.ts +24 -0
  72. package/dist/communications/contracts/reorderReminder.js +24 -0
  73. package/dist/communications/contracts/returnEmails.d.ts +25 -0
  74. package/dist/communications/contracts/returnEmails.js +62 -0
  75. package/dist/communications/contracts/reviewEffects.d.ts +26 -0
  76. package/dist/communications/contracts/reviewEffects.js +25 -0
  77. package/dist/communications/contracts/reviewRequest.d.ts +26 -0
  78. package/dist/communications/contracts/reviewRequest.js +24 -0
  79. package/dist/communications/contracts/roleNotificationContent.d.ts +25 -0
  80. package/dist/communications/contracts/roleNotificationContent.js +46 -0
  81. package/dist/communications/contracts/shipmentDelivered.d.ts +23 -0
  82. package/dist/communications/contracts/shipmentDelivered.js +24 -0
  83. package/dist/communications/contracts/shipmentDispatched.d.ts +27 -0
  84. package/dist/communications/contracts/shipmentDispatched.js +35 -0
  85. package/dist/communications/contracts/shipmentException.d.ts +21 -0
  86. package/dist/communications/contracts/shipmentException.js +26 -0
  87. package/dist/communications/contracts/subscriptionActivationActionRequired.d.ts +17 -0
  88. package/dist/communications/contracts/subscriptionActivationActionRequired.js +22 -0
  89. package/dist/communications/contracts/subscriptionAddressChanged.d.ts +19 -0
  90. package/dist/communications/contracts/subscriptionAddressChanged.js +25 -0
  91. package/dist/communications/contracts/subscriptionCancelled.d.ts +19 -0
  92. package/dist/communications/contracts/subscriptionCancelled.js +25 -0
  93. package/dist/communications/contracts/subscriptionCycleSkipped.d.ts +25 -0
  94. package/dist/communications/contracts/subscriptionCycleSkipped.js +40 -0
  95. package/dist/communications/contracts/subscriptionDeliveryRescheduled.d.ts +25 -0
  96. package/dist/communications/contracts/subscriptionDeliveryRescheduled.js +39 -0
  97. package/dist/communications/contracts/subscriptionEmailContent.d.ts +145 -0
  98. package/dist/communications/contracts/subscriptionEmailContent.js +1 -0
  99. package/dist/communications/contracts/subscriptionPackageChanged.d.ts +20 -0
  100. package/dist/communications/contracts/subscriptionPackageChanged.js +26 -0
  101. package/dist/communications/contracts/subscriptionPauseReminder.d.ts +23 -0
  102. package/dist/communications/contracts/subscriptionPauseReminder.js +29 -0
  103. package/dist/communications/contracts/subscriptionPaused.d.ts +21 -0
  104. package/dist/communications/contracts/subscriptionPaused.js +25 -0
  105. package/dist/communications/contracts/subscriptionPaymentExpired.d.ts +34 -0
  106. package/dist/communications/contracts/subscriptionPaymentExpired.js +37 -0
  107. package/dist/communications/contracts/subscriptionPaymentFailed.d.ts +49 -0
  108. package/dist/communications/contracts/subscriptionPaymentFailed.js +56 -0
  109. package/dist/communications/contracts/subscriptionPaymentRecovered.d.ts +19 -0
  110. package/dist/communications/contracts/subscriptionPaymentRecovered.js +22 -0
  111. package/dist/communications/contracts/subscriptionRenewalAtRisk.d.ts +36 -0
  112. package/dist/communications/contracts/subscriptionRenewalAtRisk.js +36 -0
  113. package/dist/communications/contracts/subscriptionRenewalUpcoming.d.ts +49 -0
  114. package/dist/communications/contracts/subscriptionRenewalUpcoming.js +43 -0
  115. package/dist/communications/contracts/subscriptionResumed.d.ts +23 -0
  116. package/dist/communications/contracts/subscriptionResumed.js +25 -0
  117. package/dist/communications/contracts/subscriptionWelcome.d.ts +44 -0
  118. package/dist/communications/contracts/subscriptionWelcome.js +45 -0
  119. package/dist/communications/contracts/subscriptionWinback.d.ts +21 -0
  120. package/dist/communications/contracts/subscriptionWinback.js +28 -0
  121. package/dist/communications/contracts/transactionalDelivery.d.ts +42 -0
  122. package/dist/communications/contracts/transactionalDelivery.js +1 -0
  123. package/dist/communications/contracts/types.d.ts +46 -0
  124. package/dist/communications/contracts/types.js +1 -0
  125. package/dist/communications/contribution.d.ts +10 -0
  126. package/dist/communications/contribution.js +24 -0
  127. package/dist/communications/deliveryReconciliation.d.ts +64 -0
  128. package/dist/communications/deliveryReconciliation.js +60 -0
  129. package/dist/communications/emailEgress.d.ts +38 -0
  130. package/dist/communications/emailEgress.js +100 -0
  131. package/dist/communications/emailHealthAssessment.d.ts +39 -0
  132. package/dist/communications/emailHealthAssessment.js +59 -0
  133. package/dist/communications/emailHealthMetricsPort.d.ts +17 -0
  134. package/dist/communications/emailHealthMetricsPort.js +1 -0
  135. package/dist/communications/emailNotificationControl.d.ts +27 -0
  136. package/dist/communications/emailNotificationControl.js +10 -0
  137. package/dist/communications/emailObservation.d.ts +28 -0
  138. package/dist/communications/emailObservation.js +23 -0
  139. package/dist/communications/emailTransport.d.ts +82 -0
  140. package/dist/communications/emailTransport.js +95 -0
  141. package/dist/communications/errors.d.ts +15 -0
  142. package/dist/communications/errors.js +30 -0
  143. package/dist/communications/index.d.ts +46 -0
  144. package/dist/communications/index.js +38 -0
  145. package/dist/communications/integrationEventSignature.d.ts +15 -0
  146. package/dist/communications/integrationEventSignature.js +37 -0
  147. package/dist/communications/managedTransactionalDeliveryStore.d.ts +12 -0
  148. package/dist/communications/managedTransactionalDeliveryStore.js +49 -0
  149. package/dist/communications/nativeOperations.d.ts +79 -0
  150. package/dist/communications/nativeOperations.js +3 -0
  151. package/dist/communications/newsletterIntegrationContracts.d.ts +130 -0
  152. package/dist/communications/newsletterIntegrationContracts.js +128 -0
  153. package/dist/communications/newsletterWebhookPort.d.ts +11 -0
  154. package/dist/communications/newsletterWebhookPort.js +84 -0
  155. package/dist/communications/newsletterWebhookProcessing.d.ts +17 -0
  156. package/dist/communications/newsletterWebhookProcessing.js +71 -0
  157. package/dist/communications/originPolicy.d.ts +40 -0
  158. package/dist/communications/originPolicy.js +100 -0
  159. package/dist/communications/postgresCommunicationSync.d.ts +79 -0
  160. package/dist/communications/postgresCommunicationSync.js +111 -0
  161. package/dist/communications/postgresConsentEvaluator.d.ts +19 -0
  162. package/dist/communications/postgresConsentEvaluator.js +43 -0
  163. package/dist/communications/postgresDeliveryReconciliation.d.ts +3 -0
  164. package/dist/communications/postgresDeliveryReconciliation.js +113 -0
  165. package/dist/communications/postgresNativeOperations.d.ts +13 -0
  166. package/dist/communications/postgresNativeOperations.js +63 -0
  167. package/dist/communications/postgresNotificationRecipients.d.ts +37 -0
  168. package/dist/communications/postgresNotificationRecipients.js +60 -0
  169. package/dist/communications/postgresTransactionalDeliveryStore.d.ts +7 -0
  170. package/dist/communications/postgresTransactionalDeliveryStore.js +55 -0
  171. package/dist/communications/preferencesToken.d.ts +10 -0
  172. package/dist/communications/preferencesToken.js +40 -0
  173. package/dist/communications/publicPreferenceToken.d.ts +24 -0
  174. package/dist/communications/publicPreferenceToken.js +53 -0
  175. package/dist/communications/shipmentNotifications.d.ts +34 -0
  176. package/dist/communications/shipmentNotifications.js +61 -0
  177. package/dist/communications/sql.d.ts +26 -0
  178. package/dist/communications/sql.js +7 -0
  179. package/dist/communications/standardWebhooks.d.ts +14 -0
  180. package/dist/communications/standardWebhooks.js +73 -0
  181. package/dist/communications/status.d.ts +11 -0
  182. package/dist/communications/status.js +36 -0
  183. package/dist/communications/transactionalDelivery.d.ts +19 -0
  184. package/dist/communications/transactionalDelivery.js +96 -0
  185. package/dist/communications/transactionalDeliveryIdentifier.d.ts +3 -0
  186. package/dist/communications/transactionalDeliveryIdentifier.js +6 -0
  187. package/dist/communications/unsubscribeToken.d.ts +77 -0
  188. package/dist/communications/unsubscribeToken.js +150 -0
  189. package/dist/communications/validation.d.ts +2 -0
  190. package/dist/communications/validation.js +3 -0
  191. package/dist/http/index.d.ts +1 -0
  192. package/dist/http/index.js +1 -0
  193. package/dist/http/webhookSignature.d.ts +42 -0
  194. package/dist/http/webhookSignature.js +86 -0
  195. package/package.json +12 -2
  196. package/release-gates.json +41 -8
  197. package/sql/20260807190000_comms_outbox_rail.sql +329 -0
  198. package/sql/20260810120001_transactional_delivery_receipts.sql +209 -0
  199. package/sql/20260813120000_communications_control_plane.sql +568 -0
  200. package/sql/20261019200000_communications_native_operations.sql +68 -0
  201. package/src/communications/accountingDocumentDelivery.ts +63 -0
  202. package/src/communications/authEmailError.ts +10 -0
  203. package/src/communications/authEmailPorts.ts +2 -0
  204. package/src/communications/authSendEmailHook.ts +310 -0
  205. package/src/communications/capturedTransactionalDelivery.ts +29 -0
  206. package/src/communications/communicationSync.ts +96 -0
  207. package/src/communications/communicationSyncWorker.ts +302 -0
  208. package/src/communications/consentPolicy.ts +82 -0
  209. package/src/communications/consentPurposeRegistry.ts +31 -0
  210. package/src/communications/contracts/abandonedCart.ts +64 -0
  211. package/src/communications/contracts/authEmail.ts +73 -0
  212. package/src/communications/contracts/backInStock.ts +69 -0
  213. package/src/communications/contracts/blocks.ts +47 -0
  214. package/src/communications/contracts/checkoutExpired.ts +62 -0
  215. package/src/communications/contracts/checkoutRecovery.ts +68 -0
  216. package/src/communications/contracts/commerceEmailContent.ts +189 -0
  217. package/src/communications/contracts/commerceEngagementEmailContent.ts +106 -0
  218. package/src/communications/contracts/consent.ts +13 -0
  219. package/src/communications/contracts/email.ts +210 -0
  220. package/src/communications/contracts/guideCard.ts +34 -0
  221. package/src/communications/contracts/index.ts +51 -0
  222. package/src/communications/contracts/inquiryContent.ts +53 -0
  223. package/src/communications/contracts/invoiceDocumentContent.ts +57 -0
  224. package/src/communications/contracts/marketingAutomation.ts +45 -0
  225. package/src/communications/contracts/newsletter.ts +78 -0
  226. package/src/communications/contracts/newsletterWebhook.ts +23 -0
  227. package/src/communications/contracts/optOutContent.ts +23 -0
  228. package/src/communications/contracts/orderCanceled.ts +64 -0
  229. package/src/communications/contracts/orderDraft.ts +112 -0
  230. package/src/communications/contracts/orderPaid.ts +147 -0
  231. package/src/communications/contracts/orderRefunded.ts +63 -0
  232. package/src/communications/contracts/paymentFailed.ts +59 -0
  233. package/src/communications/contracts/publicIntake.ts +53 -0
  234. package/src/communications/contracts/render.ts +271 -0
  235. package/src/communications/contracts/reorderReminder.ts +59 -0
  236. package/src/communications/contracts/returnEmails.ts +113 -0
  237. package/src/communications/contracts/reviewEffects.ts +60 -0
  238. package/src/communications/contracts/reviewRequest.ts +59 -0
  239. package/src/communications/contracts/roleNotificationContent.ts +65 -0
  240. package/src/communications/contracts/shipmentDelivered.ts +59 -0
  241. package/src/communications/contracts/shipmentDispatched.ts +77 -0
  242. package/src/communications/contracts/shipmentException.ts +62 -0
  243. package/src/communications/contracts/subscriptionActivationActionRequired.ts +37 -0
  244. package/src/communications/contracts/subscriptionAddressChanged.ts +56 -0
  245. package/src/communications/contracts/subscriptionCancelled.ts +57 -0
  246. package/src/communications/contracts/subscriptionCycleSkipped.ts +80 -0
  247. package/src/communications/contracts/subscriptionDeliveryRescheduled.ts +79 -0
  248. package/src/communications/contracts/subscriptionEmailContent.ts +163 -0
  249. package/src/communications/contracts/subscriptionPackageChanged.ts +59 -0
  250. package/src/communications/contracts/subscriptionPauseReminder.ts +63 -0
  251. package/src/communications/contracts/subscriptionPaused.ts +58 -0
  252. package/src/communications/contracts/subscriptionPaymentExpired.ts +83 -0
  253. package/src/communications/contracts/subscriptionPaymentFailed.ts +117 -0
  254. package/src/communications/contracts/subscriptionPaymentRecovered.ts +50 -0
  255. package/src/communications/contracts/subscriptionRenewalAtRisk.ts +85 -0
  256. package/src/communications/contracts/subscriptionRenewalUpcoming.ts +103 -0
  257. package/src/communications/contracts/subscriptionResumed.ts +60 -0
  258. package/src/communications/contracts/subscriptionWelcome.ts +102 -0
  259. package/src/communications/contracts/subscriptionWinback.ts +62 -0
  260. package/src/communications/contracts/transactionalDelivery.ts +43 -0
  261. package/src/communications/contracts/types.ts +58 -0
  262. package/src/communications/contribution.ts +33 -0
  263. package/src/communications/deliveryReconciliation.ts +108 -0
  264. package/src/communications/emailEgress.ts +111 -0
  265. package/src/communications/emailHealthAssessment.ts +106 -0
  266. package/src/communications/emailHealthMetricsPort.ts +17 -0
  267. package/src/communications/emailNotificationControl.ts +28 -0
  268. package/src/communications/emailObservation.ts +43 -0
  269. package/src/communications/emailTransport.ts +167 -0
  270. package/src/communications/errors.ts +34 -0
  271. package/src/communications/index.ts +46 -0
  272. package/src/communications/integrationEventSignature.ts +49 -0
  273. package/src/communications/managedTransactionalDeliveryStore.ts +59 -0
  274. package/src/communications/nativeOperations.ts +40 -0
  275. package/src/communications/newsletterIntegrationContracts.ts +154 -0
  276. package/src/communications/newsletterWebhookPort.ts +104 -0
  277. package/src/communications/newsletterWebhookProcessing.ts +102 -0
  278. package/src/communications/originPolicy.ts +151 -0
  279. package/src/communications/postgresCommunicationSync.ts +127 -0
  280. package/src/communications/postgresConsentEvaluator.ts +49 -0
  281. package/src/communications/postgresDeliveryReconciliation.ts +101 -0
  282. package/src/communications/postgresNativeOperations.ts +55 -0
  283. package/src/communications/postgresNotificationRecipients.ts +57 -0
  284. package/src/communications/postgresTransactionalDeliveryStore.ts +79 -0
  285. package/src/communications/preferencesToken.ts +62 -0
  286. package/src/communications/publicPreferenceToken.ts +68 -0
  287. package/src/communications/shipmentNotifications.ts +78 -0
  288. package/src/communications/sql.ts +7 -0
  289. package/src/communications/standardWebhooks.ts +84 -0
  290. package/src/communications/status.ts +51 -0
  291. package/src/communications/transactionalDelivery.ts +114 -0
  292. package/src/communications/transactionalDeliveryIdentifier.ts +9 -0
  293. package/src/communications/unsubscribeToken.ts +186 -0
  294. package/src/communications/validation.ts +3 -0
  295. package/src/http/index.ts +1 -0
  296. package/src/http/webhookSignature.ts +96 -0
@@ -0,0 +1,568 @@
1
+ -- Public provider-neutral communications control plane.
2
+ --
3
+ -- This forward adds only opaque operator, control, template and command
4
+ -- identifiers around the existing transactional-delivery receipt. It stores no
5
+ -- recipient address, message content, provider identifier or transport payload.
6
+ -- Every routine is invoker-rights with a fixed search path; browser principals
7
+ -- receive no table or function grant and the catalog contains no branded seed.
8
+
9
+ CREATE TABLE public.platform_communication_operators (
10
+ principal_id uuid PRIMARY KEY,
11
+ active boolean NOT NULL DEFAULT true,
12
+ created_at timestamptz NOT NULL DEFAULT now(),
13
+ updated_at timestamptz NOT NULL DEFAULT now(),
14
+ CONSTRAINT platform_communication_operators_timestamp_check
15
+ CHECK (updated_at >= created_at)
16
+ );
17
+
18
+ CREATE TABLE public.communication_delivery_controls (
19
+ control_key text PRIMARY KEY,
20
+ enabled boolean NOT NULL DEFAULT false,
21
+ revision integer NOT NULL DEFAULT 1,
22
+ updated_at timestamptz NOT NULL DEFAULT now(),
23
+ CONSTRAINT communication_delivery_controls_key_check
24
+ CHECK (control_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
25
+ CONSTRAINT communication_delivery_controls_revision_check CHECK (revision >= 1)
26
+ );
27
+
28
+ CREATE TABLE public.communication_delivery_templates (
29
+ template_reference text PRIMARY KEY,
30
+ control_key text NOT NULL REFERENCES public.communication_delivery_controls(control_key),
31
+ active boolean NOT NULL DEFAULT false,
32
+ revision integer NOT NULL DEFAULT 1,
33
+ updated_at timestamptz NOT NULL DEFAULT now(),
34
+ CONSTRAINT communication_delivery_templates_reference_check
35
+ CHECK (template_reference ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$'),
36
+ CONSTRAINT communication_delivery_templates_revision_check CHECK (revision >= 1)
37
+ );
38
+
39
+ CREATE TABLE public.communication_delivery_preparations (
40
+ idempotency_key text PRIMARY KEY,
41
+ command_fingerprint text NOT NULL,
42
+ template_reference text NOT NULL REFERENCES public.communication_delivery_templates(template_reference),
43
+ control_key text NOT NULL REFERENCES public.communication_delivery_controls(control_key),
44
+ recipient_fingerprint text NOT NULL,
45
+ operator_id uuid NOT NULL REFERENCES public.platform_communication_operators(principal_id),
46
+ created_at timestamptz NOT NULL DEFAULT now(),
47
+ updated_at timestamptz NOT NULL DEFAULT now(),
48
+ CONSTRAINT communication_delivery_preparations_key_check
49
+ CHECK (idempotency_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$'),
50
+ CONSTRAINT communication_delivery_preparations_fingerprint_check
51
+ CHECK (command_fingerprint ~ '^[a-f0-9]{64}$'),
52
+ CONSTRAINT communication_delivery_preparations_recipient_fingerprint_check
53
+ CHECK (recipient_fingerprint ~ '^[a-f0-9]{64}$'),
54
+ CONSTRAINT communication_delivery_preparations_timestamp_check
55
+ CHECK (updated_at >= created_at)
56
+ );
57
+
58
+ CREATE TABLE public.communication_delivery_commands (
59
+ idempotency_key text PRIMARY KEY,
60
+ command_fingerprint text NOT NULL,
61
+ template_reference text NOT NULL REFERENCES public.communication_delivery_templates(template_reference),
62
+ control_key text NOT NULL REFERENCES public.communication_delivery_controls(control_key),
63
+ recipient_fingerprint text NOT NULL,
64
+ created_at timestamptz NOT NULL DEFAULT now(),
65
+ updated_at timestamptz NOT NULL DEFAULT now(),
66
+ CONSTRAINT communication_delivery_commands_receipt_fk
67
+ FOREIGN KEY (idempotency_key)
68
+ REFERENCES public.transactional_delivery_receipts(idempotency_key)
69
+ DEFERRABLE INITIALLY DEFERRED,
70
+ CONSTRAINT communication_delivery_commands_key_check
71
+ CHECK (idempotency_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$'),
72
+ CONSTRAINT communication_delivery_commands_fingerprint_check
73
+ CHECK (command_fingerprint ~ '^[a-f0-9]{64}$'),
74
+ CONSTRAINT communication_delivery_commands_recipient_fingerprint_check
75
+ CHECK (recipient_fingerprint ~ '^[a-f0-9]{64}$'),
76
+ CONSTRAINT communication_delivery_commands_timestamp_check
77
+ CHECK (updated_at >= created_at)
78
+ );
79
+
80
+ CREATE TABLE public.communication_delivery_events (
81
+ event_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
82
+ idempotency_key text NOT NULL
83
+ REFERENCES public.communication_delivery_commands(idempotency_key),
84
+ transition text NOT NULL,
85
+ attempt_count integer NOT NULL,
86
+ operator_id uuid NOT NULL REFERENCES public.platform_communication_operators(principal_id),
87
+ occurred_at timestamptz NOT NULL DEFAULT now(),
88
+ CONSTRAINT communication_delivery_events_transition_check
89
+ CHECK (transition IN ('attempted', 'accepted', 'failed', 'replayed')),
90
+ CONSTRAINT communication_delivery_events_attempt_check CHECK (attempt_count >= 1)
91
+ );
92
+
93
+ CREATE INDEX communication_delivery_commands_updated_idx
94
+ ON public.communication_delivery_commands(updated_at DESC, idempotency_key);
95
+ CREATE INDEX communication_delivery_events_command_idx
96
+ ON public.communication_delivery_events(idempotency_key, event_id);
97
+
98
+ CREATE FUNCTION public.communications_refuse_delivery_event_mutation()
99
+ RETURNS trigger
100
+ LANGUAGE plpgsql
101
+ SET search_path = pg_catalog
102
+ AS $$
103
+ BEGIN
104
+ RAISE LOG 'communications_delivery_events_append_only_rejected';
105
+ RAISE EXCEPTION 'communications_delivery_events_append_only' USING ERRCODE = '55000';
106
+ END;
107
+ $$;
108
+
109
+ CREATE TRIGGER communications_delivery_events_append_only
110
+ BEFORE UPDATE OR DELETE ON public.communication_delivery_events
111
+ FOR EACH ROW EXECUTE FUNCTION public.communications_refuse_delivery_event_mutation();
112
+
113
+ REVOKE ALL ON TABLE
114
+ public.platform_communication_operators,
115
+ public.communication_delivery_controls,
116
+ public.communication_delivery_templates,
117
+ public.communication_delivery_preparations,
118
+ public.communication_delivery_commands,
119
+ public.communication_delivery_events
120
+ FROM PUBLIC, anon, authenticated;
121
+ REVOKE ALL ON SEQUENCE public.communication_delivery_events_event_id_seq
122
+ FROM PUBLIC, anon, authenticated;
123
+
124
+ CREATE FUNCTION public.communications_operator_is_active(p_principal_id uuid)
125
+ RETURNS boolean
126
+ LANGUAGE sql
127
+ STABLE
128
+ SET search_path = pg_catalog
129
+ AS $$
130
+ SELECT EXISTS (
131
+ SELECT 1
132
+ FROM public.platform_communication_operators AS operator_row
133
+ WHERE operator_row.principal_id = p_principal_id
134
+ AND operator_row.active IS TRUE
135
+ );
136
+ $$;
137
+
138
+ CREATE FUNCTION public.communications_require_active_operator(p_operator_id uuid)
139
+ RETURNS void
140
+ LANGUAGE plpgsql
141
+ STABLE
142
+ SET search_path = pg_catalog
143
+ AS $$
144
+ BEGIN
145
+ IF p_operator_id IS NULL OR NOT public.communications_operator_is_active(p_operator_id) THEN
146
+ RAISE EXCEPTION 'communications_operator_inactive' USING ERRCODE = '42501';
147
+ END IF;
148
+ END;
149
+ $$;
150
+
151
+ CREATE FUNCTION public.communications_set_delivery_control(
152
+ p_operator_id uuid,
153
+ p_control_key text,
154
+ p_enabled boolean
155
+ )
156
+ RETURNS integer
157
+ LANGUAGE plpgsql
158
+ SET search_path = pg_catalog
159
+ AS $$
160
+ DECLARE
161
+ v_revision integer;
162
+ BEGIN
163
+ PERFORM public.communications_require_active_operator(p_operator_id);
164
+ IF p_control_key IS NULL
165
+ OR p_control_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
166
+ OR p_enabled IS NULL THEN
167
+ RAISE EXCEPTION 'communications_control_invalid' USING ERRCODE = '22023';
168
+ END IF;
169
+
170
+ INSERT INTO public.communication_delivery_controls(control_key, enabled)
171
+ VALUES (p_control_key, p_enabled)
172
+ ON CONFLICT (control_key) DO UPDATE
173
+ SET enabled = EXCLUDED.enabled,
174
+ revision = public.communication_delivery_controls.revision + 1,
175
+ updated_at = now()
176
+ RETURNING revision INTO v_revision;
177
+ RETURN v_revision;
178
+ END;
179
+ $$;
180
+
181
+ CREATE FUNCTION public.communications_set_delivery_template(
182
+ p_operator_id uuid,
183
+ p_template_reference text,
184
+ p_control_key text,
185
+ p_active boolean
186
+ )
187
+ RETURNS integer
188
+ LANGUAGE plpgsql
189
+ SET search_path = pg_catalog
190
+ AS $$
191
+ DECLARE
192
+ v_revision integer;
193
+ BEGIN
194
+ PERFORM public.communications_require_active_operator(p_operator_id);
195
+ IF p_template_reference IS NULL
196
+ OR p_template_reference !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$'
197
+ OR p_control_key IS NULL
198
+ OR p_control_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
199
+ OR p_active IS NULL THEN
200
+ RAISE EXCEPTION 'communications_template_invalid' USING ERRCODE = '22023';
201
+ END IF;
202
+ IF NOT EXISTS (
203
+ SELECT 1 FROM public.communication_delivery_controls
204
+ WHERE control_key = p_control_key
205
+ ) THEN
206
+ RAISE EXCEPTION 'communications_control_not_found' USING ERRCODE = '23505';
207
+ END IF;
208
+
209
+ INSERT INTO public.communication_delivery_templates(
210
+ template_reference, control_key, active
211
+ ) VALUES (p_template_reference, p_control_key, p_active)
212
+ ON CONFLICT (template_reference) DO UPDATE
213
+ SET control_key = EXCLUDED.control_key,
214
+ active = EXCLUDED.active,
215
+ revision = public.communication_delivery_templates.revision + 1,
216
+ updated_at = now()
217
+ RETURNING revision INTO v_revision;
218
+ RETURN v_revision;
219
+ END;
220
+ $$;
221
+
222
+ CREATE FUNCTION public.communications_prepare_delivery_command(
223
+ p_operator_id uuid,
224
+ p_idempotency_key text,
225
+ p_command_fingerprint text,
226
+ p_template_reference text,
227
+ p_recipient_fingerprint text
228
+ )
229
+ RETURNS TABLE(action text, attempt_count integer)
230
+ LANGUAGE plpgsql
231
+ SET search_path = pg_catalog
232
+ AS $$
233
+ DECLARE
234
+ v_command public.communication_delivery_commands%ROWTYPE;
235
+ v_preparation public.communication_delivery_preparations%ROWTYPE;
236
+ v_receipt public.transactional_delivery_receipts%ROWTYPE;
237
+ v_control_key text;
238
+ BEGIN
239
+ PERFORM public.communications_require_active_operator(p_operator_id);
240
+ IF p_idempotency_key IS NULL
241
+ OR p_idempotency_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$'
242
+ OR p_command_fingerprint IS NULL
243
+ OR p_command_fingerprint !~ '^[a-f0-9]{64}$'
244
+ OR p_template_reference IS NULL
245
+ OR p_template_reference !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$'
246
+ OR p_recipient_fingerprint IS NULL
247
+ OR p_recipient_fingerprint !~ '^[a-f0-9]{64}$' THEN
248
+ RAISE EXCEPTION 'communications_command_invalid' USING ERRCODE = '22023';
249
+ END IF;
250
+
251
+ PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(p_idempotency_key, 0));
252
+ SELECT * INTO v_command
253
+ FROM public.communication_delivery_commands
254
+ WHERE idempotency_key = p_idempotency_key
255
+ FOR UPDATE;
256
+
257
+ IF FOUND THEN
258
+ IF v_command.command_fingerprint <> p_command_fingerprint
259
+ OR v_command.template_reference <> p_template_reference
260
+ OR v_command.recipient_fingerprint <> p_recipient_fingerprint THEN
261
+ RAISE EXCEPTION 'communications_command_conflict' USING ERRCODE = '23505';
262
+ END IF;
263
+ SELECT * INTO STRICT v_receipt
264
+ FROM public.transactional_delivery_receipts
265
+ WHERE idempotency_key = p_idempotency_key
266
+ FOR UPDATE;
267
+ IF v_receipt.state = 'accepted' THEN
268
+ INSERT INTO public.communication_delivery_events(
269
+ idempotency_key, transition, attempt_count, operator_id
270
+ ) VALUES (p_idempotency_key, 'replayed', v_receipt.attempt_count, p_operator_id);
271
+ RETURN QUERY SELECT 'replayed'::text, v_receipt.attempt_count;
272
+ RETURN;
273
+ END IF;
274
+
275
+ -- A durable accepted receipt always replays, even if policy changes later.
276
+ -- A failed command is a new delivery attempt and must re-check current
277
+ -- eligibility before invoking the captured delivery adapter again.
278
+ IF NOT EXISTS (
279
+ SELECT 1
280
+ FROM public.communication_delivery_templates AS template_row
281
+ JOIN public.communication_delivery_controls AS control_row
282
+ ON control_row.control_key = template_row.control_key
283
+ WHERE template_row.template_reference = v_command.template_reference
284
+ AND template_row.active IS TRUE
285
+ AND control_row.enabled IS TRUE
286
+ ) THEN
287
+ RAISE EXCEPTION 'communications_delivery_disabled' USING ERRCODE = '23505';
288
+ END IF;
289
+
290
+ INSERT INTO public.communication_delivery_events(
291
+ idempotency_key, transition, attempt_count, operator_id
292
+ ) VALUES (p_idempotency_key, 'attempted', v_receipt.attempt_count + 1, p_operator_id);
293
+ RETURN QUERY SELECT 'proceed'::text, v_receipt.attempt_count + 1;
294
+ RETURN;
295
+ END IF;
296
+
297
+ SELECT * INTO v_preparation
298
+ FROM public.communication_delivery_preparations
299
+ WHERE idempotency_key = p_idempotency_key
300
+ FOR UPDATE;
301
+ IF FOUND THEN
302
+ IF v_preparation.command_fingerprint <> p_command_fingerprint
303
+ OR v_preparation.template_reference <> p_template_reference
304
+ OR v_preparation.recipient_fingerprint <> p_recipient_fingerprint
305
+ OR v_preparation.operator_id <> p_operator_id THEN
306
+ RAISE EXCEPTION 'communications_command_conflict' USING ERRCODE = '23505';
307
+ END IF;
308
+ IF NOT EXISTS (
309
+ SELECT 1
310
+ FROM public.communication_delivery_templates AS template_row
311
+ JOIN public.communication_delivery_controls AS control_row
312
+ ON control_row.control_key = template_row.control_key
313
+ WHERE template_row.template_reference = v_preparation.template_reference
314
+ AND template_row.active IS TRUE
315
+ AND control_row.enabled IS TRUE
316
+ ) THEN
317
+ RAISE EXCEPTION 'communications_delivery_disabled' USING ERRCODE = '23505';
318
+ END IF;
319
+ UPDATE public.communication_delivery_preparations
320
+ SET updated_at = now() WHERE idempotency_key = p_idempotency_key;
321
+ RETURN QUERY SELECT 'proceed'::text, 1;
322
+ RETURN;
323
+ END IF;
324
+
325
+ SELECT template_row.control_key INTO v_control_key
326
+ FROM public.communication_delivery_templates AS template_row
327
+ JOIN public.communication_delivery_controls AS control_row
328
+ ON control_row.control_key = template_row.control_key
329
+ WHERE template_row.template_reference = p_template_reference
330
+ AND template_row.active IS TRUE
331
+ AND control_row.enabled IS TRUE
332
+ FOR UPDATE OF template_row, control_row;
333
+ IF NOT FOUND THEN
334
+ RAISE EXCEPTION 'communications_delivery_disabled' USING ERRCODE = '23505';
335
+ END IF;
336
+
337
+ INSERT INTO public.communication_delivery_preparations(
338
+ idempotency_key, command_fingerprint, template_reference,
339
+ control_key, recipient_fingerprint, operator_id
340
+ ) VALUES (
341
+ p_idempotency_key, p_command_fingerprint, p_template_reference,
342
+ v_control_key, p_recipient_fingerprint, p_operator_id
343
+ );
344
+ RETURN QUERY SELECT 'proceed'::text, 1;
345
+ END;
346
+ $$;
347
+
348
+ CREATE FUNCTION public.communications_record_delivery_accepted(
349
+ p_operator_id uuid,
350
+ p_idempotency_key text,
351
+ p_command_fingerprint text,
352
+ p_delivery_reference text
353
+ )
354
+ RETURNS SETOF public.transactional_delivery_receipts
355
+ LANGUAGE plpgsql
356
+ SET search_path = pg_catalog
357
+ AS $$
358
+ DECLARE
359
+ v_command public.communication_delivery_commands%ROWTYPE;
360
+ v_preparation public.communication_delivery_preparations%ROWTYPE;
361
+ v_receipt public.transactional_delivery_receipts%ROWTYPE;
362
+ BEGIN
363
+ PERFORM public.communications_require_active_operator(p_operator_id);
364
+ PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(p_idempotency_key, 0));
365
+ SELECT * INTO v_command FROM public.communication_delivery_commands
366
+ WHERE idempotency_key = p_idempotency_key FOR UPDATE;
367
+ IF FOUND AND v_command.command_fingerprint <> p_command_fingerprint THEN
368
+ RAISE EXCEPTION 'communications_command_conflict' USING ERRCODE = '23505';
369
+ END IF;
370
+ IF NOT FOUND THEN
371
+ SELECT * INTO v_preparation FROM public.communication_delivery_preparations
372
+ WHERE idempotency_key = p_idempotency_key FOR UPDATE;
373
+ IF NOT FOUND OR v_preparation.command_fingerprint <> p_command_fingerprint
374
+ OR v_preparation.operator_id <> p_operator_id THEN
375
+ RAISE EXCEPTION 'communications_command_conflict' USING ERRCODE = '23505';
376
+ END IF;
377
+ END IF;
378
+ SELECT * INTO v_receipt FROM public.transactional_delivery_record_accepted(
379
+ p_idempotency_key, p_command_fingerprint, p_delivery_reference
380
+ );
381
+ IF v_command.idempotency_key IS NULL THEN
382
+ INSERT INTO public.communication_delivery_commands(
383
+ idempotency_key, command_fingerprint, template_reference,
384
+ control_key, recipient_fingerprint
385
+ ) VALUES (
386
+ v_preparation.idempotency_key, v_preparation.command_fingerprint,
387
+ v_preparation.template_reference, v_preparation.control_key,
388
+ v_preparation.recipient_fingerprint
389
+ );
390
+ INSERT INTO public.communication_delivery_events(
391
+ idempotency_key, transition, attempt_count, operator_id
392
+ ) VALUES (p_idempotency_key, 'attempted', v_receipt.attempt_count, p_operator_id);
393
+ DELETE FROM public.communication_delivery_preparations
394
+ WHERE idempotency_key = p_idempotency_key;
395
+ END IF;
396
+ UPDATE public.communication_delivery_commands
397
+ SET updated_at = now() WHERE idempotency_key = p_idempotency_key;
398
+ INSERT INTO public.communication_delivery_events(
399
+ idempotency_key, transition, attempt_count, operator_id
400
+ ) VALUES (p_idempotency_key, 'accepted', v_receipt.attempt_count, p_operator_id);
401
+ RETURN NEXT v_receipt;
402
+ END;
403
+ $$;
404
+
405
+ CREATE FUNCTION public.communications_record_delivery_failed(
406
+ p_operator_id uuid,
407
+ p_idempotency_key text,
408
+ p_command_fingerprint text,
409
+ p_error_code text
410
+ )
411
+ RETURNS SETOF public.transactional_delivery_receipts
412
+ LANGUAGE plpgsql
413
+ SET search_path = pg_catalog
414
+ AS $$
415
+ DECLARE
416
+ v_command public.communication_delivery_commands%ROWTYPE;
417
+ v_preparation public.communication_delivery_preparations%ROWTYPE;
418
+ v_receipt public.transactional_delivery_receipts%ROWTYPE;
419
+ BEGIN
420
+ PERFORM public.communications_require_active_operator(p_operator_id);
421
+ PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(p_idempotency_key, 0));
422
+ SELECT * INTO v_command FROM public.communication_delivery_commands
423
+ WHERE idempotency_key = p_idempotency_key FOR UPDATE;
424
+ IF FOUND AND v_command.command_fingerprint <> p_command_fingerprint THEN
425
+ RAISE EXCEPTION 'communications_command_conflict' USING ERRCODE = '23505';
426
+ END IF;
427
+ IF NOT FOUND THEN
428
+ SELECT * INTO v_preparation FROM public.communication_delivery_preparations
429
+ WHERE idempotency_key = p_idempotency_key FOR UPDATE;
430
+ IF NOT FOUND OR v_preparation.command_fingerprint <> p_command_fingerprint
431
+ OR v_preparation.operator_id <> p_operator_id THEN
432
+ RAISE EXCEPTION 'communications_command_conflict' USING ERRCODE = '23505';
433
+ END IF;
434
+ END IF;
435
+ SELECT * INTO v_receipt FROM public.transactional_delivery_record_failed(
436
+ p_idempotency_key, p_command_fingerprint, p_error_code
437
+ );
438
+ IF v_command.idempotency_key IS NULL THEN
439
+ INSERT INTO public.communication_delivery_commands(
440
+ idempotency_key, command_fingerprint, template_reference,
441
+ control_key, recipient_fingerprint
442
+ ) VALUES (
443
+ v_preparation.idempotency_key, v_preparation.command_fingerprint,
444
+ v_preparation.template_reference, v_preparation.control_key,
445
+ v_preparation.recipient_fingerprint
446
+ );
447
+ INSERT INTO public.communication_delivery_events(
448
+ idempotency_key, transition, attempt_count, operator_id
449
+ ) VALUES (p_idempotency_key, 'attempted', v_receipt.attempt_count, p_operator_id);
450
+ DELETE FROM public.communication_delivery_preparations
451
+ WHERE idempotency_key = p_idempotency_key;
452
+ END IF;
453
+ UPDATE public.communication_delivery_commands
454
+ SET updated_at = now() WHERE idempotency_key = p_idempotency_key;
455
+ INSERT INTO public.communication_delivery_events(
456
+ idempotency_key, transition, attempt_count, operator_id
457
+ ) VALUES (p_idempotency_key, 'failed', v_receipt.attempt_count, p_operator_id);
458
+ RETURN NEXT v_receipt;
459
+ END;
460
+ $$;
461
+
462
+ CREATE FUNCTION public.communications_list_delivery_operations(
463
+ p_operator_id uuid,
464
+ p_page integer,
465
+ p_page_size integer
466
+ )
467
+ RETURNS TABLE(
468
+ idempotency_key text, template_reference text, recipient_fingerprint text,
469
+ state text, delivery_reference text, error_code text, attempt_count integer,
470
+ created_at timestamptz, updated_at timestamptz, total_count bigint
471
+ )
472
+ LANGUAGE plpgsql
473
+ STABLE
474
+ SET search_path = pg_catalog
475
+ AS $$
476
+ BEGIN
477
+ PERFORM public.communications_require_active_operator(p_operator_id);
478
+ IF p_page IS NULL OR p_page < 0 OR p_page_size IS NULL OR p_page_size < 1 OR p_page_size > 100 THEN
479
+ RAISE EXCEPTION 'communications_page_invalid' USING ERRCODE = '22023';
480
+ END IF;
481
+ RETURN QUERY
482
+ SELECT command_row.idempotency_key, command_row.template_reference,
483
+ command_row.recipient_fingerprint, receipt.state,
484
+ receipt.delivery_reference, receipt.error_code, receipt.attempt_count,
485
+ command_row.created_at, command_row.updated_at, count(*) OVER ()
486
+ FROM public.communication_delivery_commands AS command_row
487
+ JOIN public.transactional_delivery_receipts AS receipt
488
+ ON receipt.idempotency_key = command_row.idempotency_key
489
+ ORDER BY command_row.updated_at DESC, command_row.idempotency_key
490
+ LIMIT p_page_size OFFSET p_page * p_page_size;
491
+ END;
492
+ $$;
493
+
494
+ CREATE FUNCTION public.communications_list_delivery_events(
495
+ p_operator_id uuid,
496
+ p_idempotency_key text
497
+ )
498
+ RETURNS TABLE(
499
+ event_id bigint, idempotency_key text, transition text, attempt_count integer,
500
+ operator_id uuid, occurred_at timestamptz
501
+ )
502
+ LANGUAGE plpgsql
503
+ STABLE
504
+ SET search_path = pg_catalog
505
+ AS $$
506
+ BEGIN
507
+ PERFORM public.communications_require_active_operator(p_operator_id);
508
+ RETURN QUERY
509
+ SELECT event_row.event_id, event_row.idempotency_key, event_row.transition,
510
+ event_row.attempt_count, event_row.operator_id, event_row.occurred_at
511
+ FROM public.communication_delivery_events AS event_row
512
+ WHERE event_row.idempotency_key = p_idempotency_key
513
+ ORDER BY event_row.event_id;
514
+ END;
515
+ $$;
516
+
517
+ CREATE FUNCTION public.communications_list_delivery_controls(p_operator_id uuid)
518
+ RETURNS SETOF public.communication_delivery_controls
519
+ LANGUAGE plpgsql STABLE SET search_path = pg_catalog
520
+ AS $$ BEGIN
521
+ PERFORM public.communications_require_active_operator(p_operator_id);
522
+ RETURN QUERY SELECT * FROM public.communication_delivery_controls ORDER BY control_key;
523
+ END; $$;
524
+
525
+ CREATE FUNCTION public.communications_list_delivery_templates(p_operator_id uuid)
526
+ RETURNS SETOF public.communication_delivery_templates
527
+ LANGUAGE plpgsql STABLE SET search_path = pg_catalog
528
+ AS $$ BEGIN
529
+ PERFORM public.communications_require_active_operator(p_operator_id);
530
+ RETURN QUERY SELECT * FROM public.communication_delivery_templates ORDER BY template_reference;
531
+ END; $$;
532
+
533
+ CREATE FUNCTION public.communications_delivery_health(p_operator_id uuid)
534
+ RETURNS TABLE(attempted bigint, accepted bigint, failed bigint)
535
+ LANGUAGE plpgsql STABLE SET search_path = pg_catalog
536
+ AS $$ BEGIN
537
+ PERFORM public.communications_require_active_operator(p_operator_id);
538
+ RETURN QUERY SELECT
539
+ count(*) FILTER (WHERE transition = 'attempted'),
540
+ count(*) FILTER (WHERE transition = 'accepted'),
541
+ count(*) FILTER (WHERE transition = 'failed')
542
+ FROM public.communication_delivery_events;
543
+ END; $$;
544
+
545
+ CREATE FUNCTION public.communications_delivery_readiness(p_operator_id uuid)
546
+ RETURNS TABLE(required_control_count bigint, disabled_control_keys text[])
547
+ LANGUAGE plpgsql STABLE SET search_path = pg_catalog
548
+ AS $$ BEGIN
549
+ PERFORM public.communications_require_active_operator(p_operator_id);
550
+ RETURN QUERY SELECT count(*),
551
+ COALESCE(array_agg(control_key ORDER BY control_key) FILTER (WHERE enabled IS FALSE), ARRAY[]::text[])
552
+ FROM public.communication_delivery_controls;
553
+ END; $$;
554
+
555
+ REVOKE ALL ON FUNCTION public.communications_operator_is_active(uuid) FROM PUBLIC, anon, authenticated;
556
+ REVOKE ALL ON FUNCTION public.communications_refuse_delivery_event_mutation() FROM PUBLIC, anon, authenticated;
557
+ REVOKE ALL ON FUNCTION public.communications_require_active_operator(uuid) FROM PUBLIC, anon, authenticated;
558
+ REVOKE ALL ON FUNCTION public.communications_set_delivery_control(uuid,text,boolean) FROM PUBLIC, anon, authenticated;
559
+ REVOKE ALL ON FUNCTION public.communications_set_delivery_template(uuid,text,text,boolean) FROM PUBLIC, anon, authenticated;
560
+ REVOKE ALL ON FUNCTION public.communications_prepare_delivery_command(uuid,text,text,text,text) FROM PUBLIC, anon, authenticated;
561
+ REVOKE ALL ON FUNCTION public.communications_record_delivery_accepted(uuid,text,text,text) FROM PUBLIC, anon, authenticated;
562
+ REVOKE ALL ON FUNCTION public.communications_record_delivery_failed(uuid,text,text,text) FROM PUBLIC, anon, authenticated;
563
+ REVOKE ALL ON FUNCTION public.communications_list_delivery_operations(uuid,integer,integer) FROM PUBLIC, anon, authenticated;
564
+ REVOKE ALL ON FUNCTION public.communications_list_delivery_events(uuid,text) FROM PUBLIC, anon, authenticated;
565
+ REVOKE ALL ON FUNCTION public.communications_list_delivery_controls(uuid) FROM PUBLIC, anon, authenticated;
566
+ REVOKE ALL ON FUNCTION public.communications_list_delivery_templates(uuid) FROM PUBLIC, anon, authenticated;
567
+ REVOKE ALL ON FUNCTION public.communications_delivery_health(uuid) FROM PUBLIC, anon, authenticated;
568
+ REVOKE ALL ON FUNCTION public.communications_delivery_readiness(uuid) FROM PUBLIC, anon, authenticated;
@@ -0,0 +1,68 @@
1
+ -- Native communications facts only. The existing outbox owns all sync claims.
2
+ -- CREATE-only: runtime capabilities refuse unsafe effective browser/PUBLIC ACLs.
3
+ CREATE TABLE public.communication_native_sends (
4
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
5
+ binding text NOT NULL CHECK (length(binding) BETWEEN 1 AND 256),
6
+ source_key text NOT NULL CHECK (length(source_key) BETWEEN 1 AND 256),
7
+ message_id text NOT NULL CHECK (length(message_id) BETWEEN 1 AND 256),
8
+ recipient_fingerprint text NOT NULL CHECK (length(recipient_fingerprint) = 64),
9
+ template_reference text NOT NULL CHECK (length(template_reference) BETWEEN 1 AND 256),
10
+ accepted_at timestamptz NOT NULL,
11
+ observed_state text CHECK (observed_state IN ('sent','delayed','delivered','bounced','complained','failed')),
12
+ observed_at timestamptz,
13
+ last_poll_at timestamptz,
14
+ not_found_count integer NOT NULL DEFAULT 0 CHECK (not_found_count BETWEEN 0 AND 2147483647),
15
+ abandoned_at timestamptz,
16
+ CONSTRAINT communication_native_sends_source_key UNIQUE (binding, source_key),
17
+ CONSTRAINT communication_native_sends_message_key UNIQUE (binding, message_id)
18
+ );
19
+ CREATE INDEX communication_native_sends_poll ON public.communication_native_sends (binding, last_poll_at, accepted_at, id);
20
+ CREATE TABLE public.communication_native_observations (
21
+ binding text NOT NULL,
22
+ observation_key text NOT NULL CHECK (length(observation_key) BETWEEN 1 AND 256),
23
+ send_id uuid NOT NULL REFERENCES public.communication_native_sends(id),
24
+ kind text NOT NULL CHECK (kind IN ('sent','delayed','delivered','bounced','complained','failed')),
25
+ occurred_at timestamptz NOT NULL,
26
+ source text NOT NULL CHECK (source IN ('poll','callback')),
27
+ fact_fingerprint text NOT NULL CHECK (length(fact_fingerprint) = 64),
28
+ CONSTRAINT communication_native_observations_pkey PRIMARY KEY (binding, observation_key)
29
+ );
30
+ CREATE INDEX communication_native_observations_send ON public.communication_native_observations (send_id, occurred_at, observation_key);
31
+ CREATE TABLE public.communication_native_sync_state (
32
+ work_id uuid PRIMARY KEY REFERENCES public.outbox_events(id),
33
+ binding text NOT NULL CHECK (length(binding) BETWEEN 1 AND 256),
34
+ provider_kind text NOT NULL CHECK (length(provider_kind) BETWEEN 1 AND 256),
35
+ contact_id text NOT NULL CHECK (length(contact_id) BETWEEN 1 AND 256),
36
+ normalized_email text NOT NULL CHECK (length(normalized_email) BETWEEN 3 AND 320),
37
+ purpose text NOT NULL CHECK (purpose IN ('marketing_launch_offer','marketing_newsletter')),
38
+ event_kind text NOT NULL CHECK (event_kind IN ('subscribe','update','unsubscribe','suppress')),
39
+ origin_provider_kind text,
40
+ origin_provider_event_id text,
41
+ remote_profile_id text,
42
+ remote_list_id text,
43
+ response_summary jsonb NOT NULL DEFAULT '{}' CHECK (jsonb_typeof(response_summary) = 'object'),
44
+ command_fingerprint text NOT NULL CHECK (length(command_fingerprint) = 64),
45
+ max_attempts integer NOT NULL DEFAULT 8 CHECK (max_attempts BETWEEN 1 AND 20)
46
+ );
47
+ CREATE TABLE public.notification_recipients (
48
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
49
+ email text NOT NULL CHECK (email = lower(btrim(email))),
50
+ name text,
51
+ active boolean NOT NULL DEFAULT true,
52
+ notification_type text NOT NULL CHECK (notification_type IN ('packaging_digest','new_signup','commerce_payment_critical')),
53
+ created_at timestamptz NOT NULL DEFAULT now(),
54
+ updated_at timestamptz NOT NULL DEFAULT now(),
55
+ CONSTRAINT notification_recipients_type_email_key UNIQUE (notification_type, email)
56
+ );
57
+ CREATE INDEX notification_recipients_order ON public.notification_recipients (notification_type, created_at, id);
58
+ CREATE TABLE public.communication_native_policy_decisions (
59
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
60
+ recipient_fingerprint text NOT NULL CHECK (length(recipient_fingerprint) = 64),
61
+ purpose text NOT NULL CHECK (length(purpose) BETWEEN 1 AND 256),
62
+ allowed boolean NOT NULL,
63
+ reason text NOT NULL CHECK (length(reason) BETWEEN 1 AND 256),
64
+ permission_snapshot jsonb NOT NULL CHECK (jsonb_typeof(permission_snapshot) = 'object'),
65
+ source_table text,
66
+ source_id text,
67
+ evaluated_at timestamptz NOT NULL DEFAULT now()
68
+ );