@openlup/platform 0.20.0 → 0.21.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 (210) hide show
  1. package/CHANGELOG.md +62 -231
  2. package/README.md +172 -6
  3. package/dist/composition/composeBundle.d.ts +6 -0
  4. package/dist/composition/composeBundle.js +2 -0
  5. package/dist/composition/index.js +1 -0
  6. package/dist/payments/adapterConformance.d.ts +8 -0
  7. package/dist/payments/adapterConformance.js +109 -0
  8. package/dist/payments/bindings.d.ts +3 -0
  9. package/dist/payments/bindings.js +17 -0
  10. package/dist/payments/checkoutActivePaymentActionResolver.d.ts +7 -0
  11. package/dist/payments/checkoutActivePaymentActionResolver.js +77 -0
  12. package/dist/payments/checkoutRecoveryPaymentResolver.d.ts +8 -0
  13. package/dist/payments/checkoutRecoveryPaymentResolver.js +87 -0
  14. package/dist/payments/contracts/adapter.d.ts +113 -0
  15. package/dist/payments/contracts/adapter.js +1 -0
  16. package/dist/payments/contracts/conformance.d.ts +155 -0
  17. package/dist/payments/contracts/conformance.js +1 -0
  18. package/dist/payments/contracts/evidence.d.ts +157 -0
  19. package/dist/payments/contracts/evidence.js +1 -0
  20. package/dist/payments/contracts/index.d.ts +10 -0
  21. package/dist/payments/contracts/index.js +1 -0
  22. package/dist/payments/contracts/preparedAttempt.d.ts +69 -0
  23. package/dist/payments/contracts/preparedAttempt.js +1 -0
  24. package/dist/payments/contracts/reconciliation.d.ts +303 -0
  25. package/dist/payments/contracts/reconciliation.js +1 -0
  26. package/dist/payments/contracts/repair.d.ts +92 -0
  27. package/dist/payments/contracts/repair.js +1 -0
  28. package/dist/payments/contracts/truth.d.ts +92 -0
  29. package/dist/payments/contracts/truth.js +1 -0
  30. package/dist/payments/eventConformanceRunner.d.ts +12 -0
  31. package/dist/payments/eventConformanceRunner.js +125 -0
  32. package/dist/payments/index.d.ts +16 -0
  33. package/dist/payments/index.js +16 -0
  34. package/dist/payments/paymentAttemptSilenceWindow.d.ts +10 -0
  35. package/dist/payments/paymentAttemptSilenceWindow.js +19 -0
  36. package/dist/payments/paymentMethodLifecycle.d.ts +11 -0
  37. package/dist/payments/paymentMethodLifecycle.js +37 -0
  38. package/dist/payments/paymentProviderReconciliationAbsenceReopen.d.ts +7 -0
  39. package/dist/payments/paymentProviderReconciliationAbsenceReopen.js +100 -0
  40. package/dist/payments/paymentProviderReconciliationEvidence.d.ts +36 -0
  41. package/dist/payments/paymentProviderReconciliationEvidence.js +91 -0
  42. package/dist/payments/paymentProviderReconciliationWorker.d.ts +4 -0
  43. package/dist/payments/paymentProviderReconciliationWorker.js +245 -0
  44. package/dist/payments/paymentTruth.d.ts +8 -0
  45. package/dist/payments/paymentTruth.js +125 -0
  46. package/dist/payments/paymentVerifyNowHandler.d.ts +27 -0
  47. package/dist/payments/paymentVerifyNowHandler.js +70 -0
  48. package/dist/payments/paymentVerifyNowService.d.ts +23 -0
  49. package/dist/payments/paymentVerifyNowService.js +93 -0
  50. package/dist/payments/paymentWebhookPostProcessing.d.ts +22 -0
  51. package/dist/payments/paymentWebhookPostProcessing.js +15 -0
  52. package/dist/payments/postgresPaymentTruth.d.ts +10 -0
  53. package/dist/payments/postgresPaymentTruth.js +126 -0
  54. package/dist/payments/preparedAttempt.d.ts +6 -0
  55. package/dist/payments/preparedAttempt.js +70 -0
  56. package/dist/payments/preparedAttemptFinalization.d.ts +5 -0
  57. package/dist/payments/preparedAttemptFinalization.js +12 -0
  58. package/dist/payments/schemaCapabilities.d.ts +10 -0
  59. package/dist/payments/schemaCapabilities.js +10 -0
  60. package/dist/payments/sql.d.ts +20 -0
  61. package/dist/payments/sql.js +6 -0
  62. package/dist/payments/stripe/classifyDecline.d.ts +46 -0
  63. package/dist/payments/stripe/classifyDecline.js +58 -0
  64. package/dist/payments/stripe/declineFailureHints.d.ts +46 -0
  65. package/dist/payments/stripe/declineFailureHints.js +87 -0
  66. package/dist/payments/stripe/donorContracts.d.ts +120 -0
  67. package/dist/payments/stripe/donorContracts.js +1 -0
  68. package/dist/payments/stripe/executionDecline.d.ts +53 -0
  69. package/dist/payments/stripe/executionDecline.js +107 -0
  70. package/dist/payments/stripe/index.d.ts +16 -0
  71. package/dist/payments/stripe/index.js +239 -0
  72. package/dist/payments/stripe/intentMapper.d.ts +26 -0
  73. package/dist/payments/stripe/intentMapper.js +51 -0
  74. package/dist/payments/stripe/methodFacts.d.ts +10 -0
  75. package/dist/payments/stripe/methodFacts.js +12 -0
  76. package/dist/payments/stripe/paymentFailureEvidence.d.ts +26 -0
  77. package/dist/payments/stripe/paymentFailureEvidence.js +33 -0
  78. package/dist/payments/stripe/stripeFailureEvidence.d.ts +41 -0
  79. package/dist/payments/stripe/stripeFailureEvidence.js +82 -0
  80. package/dist/payments/stripe/stripeIntentConfirmationEvidence.d.ts +28 -0
  81. package/dist/payments/stripe/stripeIntentConfirmationEvidence.js +32 -0
  82. package/dist/payments/stripe/stripePaymentReconciliationProvider.d.ts +35 -0
  83. package/dist/payments/stripe/stripePaymentReconciliationProvider.js +222 -0
  84. package/dist/payments/stripe/stripeRefundEvidence.d.ts +8 -0
  85. package/dist/payments/stripe/stripeRefundEvidence.js +17 -0
  86. package/dist/payments/stripe/stripeSandboxPaymentExecutionAdapter.d.ts +45 -0
  87. package/dist/payments/stripe/stripeSandboxPaymentExecutionAdapter.js +220 -0
  88. package/dist/payments/stripe/stripeWebhookNormalizer.d.ts +14 -0
  89. package/dist/payments/stripe/stripeWebhookNormalizer.js +243 -0
  90. package/dist/payments/stripe/webhookSignature.d.ts +14 -0
  91. package/dist/payments/stripe/webhookSignature.js +43 -0
  92. package/dist/payments/tpay/blikRecurringMapper.d.ts +37 -0
  93. package/dist/payments/tpay/blikRecurringMapper.js +24 -0
  94. package/dist/payments/tpay/capabilities.d.ts +3 -0
  95. package/dist/payments/tpay/capabilities.js +18 -0
  96. package/dist/payments/tpay/classifyDecline.d.ts +46 -0
  97. package/dist/payments/tpay/classifyDecline.js +58 -0
  98. package/dist/payments/tpay/declineFailureHints.d.ts +72 -0
  99. package/dist/payments/tpay/declineFailureHints.js +37 -0
  100. package/dist/payments/tpay/donorContracts.d.ts +140 -0
  101. package/dist/payments/tpay/donorContracts.js +1 -0
  102. package/dist/payments/tpay/index.d.ts +43 -0
  103. package/dist/payments/tpay/index.js +264 -0
  104. package/dist/payments/tpay/paymentFailureEvidence.d.ts +26 -0
  105. package/dist/payments/tpay/paymentFailureEvidence.js +33 -0
  106. package/dist/payments/tpay/preparedProviderAttempt.d.ts +22 -0
  107. package/dist/payments/tpay/preparedProviderAttempt.js +24 -0
  108. package/dist/payments/tpay/providerAttemptFailureDiagnostic.d.ts +3 -0
  109. package/dist/payments/tpay/providerAttemptFailureDiagnostic.js +95 -0
  110. package/dist/payments/tpay/tpayCreateTransactionInput.d.ts +25 -0
  111. package/dist/payments/tpay/tpayCreateTransactionInput.js +128 -0
  112. package/dist/payments/tpay/tpayDeclineEvidence.d.ts +126 -0
  113. package/dist/payments/tpay/tpayDeclineEvidence.js +177 -0
  114. package/dist/payments/tpay/tpayDispatchFailure.d.ts +60 -0
  115. package/dist/payments/tpay/tpayDispatchFailure.js +121 -0
  116. package/dist/payments/tpay/tpayFailureEvidence.d.ts +20 -0
  117. package/dist/payments/tpay/tpayFailureEvidence.js +61 -0
  118. package/dist/payments/tpay/tpayHttpClient.d.ts +52 -0
  119. package/dist/payments/tpay/tpayHttpClient.js +185 -0
  120. package/dist/payments/tpay/tpayHttpFailure.d.ts +57 -0
  121. package/dist/payments/tpay/tpayHttpFailure.js +143 -0
  122. package/dist/payments/tpay/tpayPaymentExecutionAdapter.d.ts +20 -0
  123. package/dist/payments/tpay/tpayPaymentExecutionAdapter.js +249 -0
  124. package/dist/payments/tpay/tpayPaymentReconciliationProvider.d.ts +5 -0
  125. package/dist/payments/tpay/tpayPaymentReconciliationProvider.js +244 -0
  126. package/dist/payments/tpay/tpayTransactionPayload.d.ts +120 -0
  127. package/dist/payments/tpay/tpayTransactionPayload.js +96 -0
  128. package/dist/payments/tpay/wallClockTime.d.ts +31 -0
  129. package/dist/payments/tpay/wallClockTime.js +57 -0
  130. package/dist/payments/tpay/webhookJwsVerifier.d.ts +17 -0
  131. package/dist/payments/tpay/webhookJwsVerifier.js +51 -0
  132. package/dist/payments/tpay/webhookNormalizer.d.ts +54 -0
  133. package/dist/payments/tpay/webhookNormalizer.js +200 -0
  134. package/dist/payments/verifiedEvidenceProjection.d.ts +12 -0
  135. package/dist/payments/verifiedEvidenceProjection.js +166 -0
  136. package/dist/payments/verifiedIngress.d.ts +119 -0
  137. package/dist/payments/verifiedIngress.js +220 -0
  138. package/package.json +22 -2
  139. package/release-gates.json +64 -3
  140. package/sql/20260810180000_payment_settlement_rail.sql +574 -0
  141. package/sql/20260815150000_payment_truth_reconciliation.sql +479 -0
  142. package/sql/20260826100000_late_payment_shifts_the_subscription_cycle.sql +215 -0
  143. package/src/composition/composeBundle.ts +9 -0
  144. package/src/composition/index.ts +1 -0
  145. package/src/payments/adapterConformance.ts +118 -0
  146. package/src/payments/bindings.ts +17 -0
  147. package/src/payments/checkoutActivePaymentActionResolver.ts +97 -0
  148. package/src/payments/checkoutRecoveryPaymentResolver.ts +108 -0
  149. package/src/payments/contracts/adapter.ts +96 -0
  150. package/src/payments/contracts/conformance.ts +171 -0
  151. package/src/payments/contracts/evidence.ts +142 -0
  152. package/src/payments/contracts/index.ts +54 -0
  153. package/src/payments/contracts/preparedAttempt.ts +86 -0
  154. package/src/payments/contracts/reconciliation.ts +296 -0
  155. package/src/payments/contracts/repair.ts +95 -0
  156. package/src/payments/contracts/truth.ts +108 -0
  157. package/src/payments/eventConformanceRunner.ts +149 -0
  158. package/src/payments/index.ts +21 -0
  159. package/src/payments/paymentAttemptSilenceWindow.ts +26 -0
  160. package/src/payments/paymentMethodLifecycle.ts +50 -0
  161. package/src/payments/paymentProviderReconciliationAbsenceReopen.ts +130 -0
  162. package/src/payments/paymentProviderReconciliationEvidence.ts +117 -0
  163. package/src/payments/paymentProviderReconciliationWorker.ts +291 -0
  164. package/src/payments/paymentTruth.ts +131 -0
  165. package/src/payments/paymentVerifyNowHandler.ts +94 -0
  166. package/src/payments/paymentVerifyNowService.ts +125 -0
  167. package/src/payments/paymentWebhookPostProcessing.ts +36 -0
  168. package/src/payments/postgresPaymentTruth.ts +149 -0
  169. package/src/payments/preparedAttempt.ts +90 -0
  170. package/src/payments/preparedAttemptFinalization.ts +13 -0
  171. package/src/payments/schemaCapabilities.ts +10 -0
  172. package/src/payments/sql.ts +6 -0
  173. package/src/payments/stripe/classifyDecline.ts +62 -0
  174. package/src/payments/stripe/declineFailureHints.ts +89 -0
  175. package/src/payments/stripe/donorContracts.ts +87 -0
  176. package/src/payments/stripe/executionDecline.ts +126 -0
  177. package/src/payments/stripe/index.ts +235 -0
  178. package/src/payments/stripe/intentMapper.ts +58 -0
  179. package/src/payments/stripe/methodFacts.ts +21 -0
  180. package/src/payments/stripe/paymentFailureEvidence.ts +58 -0
  181. package/src/payments/stripe/stripeFailureEvidence.ts +115 -0
  182. package/src/payments/stripe/stripeIntentConfirmationEvidence.ts +34 -0
  183. package/src/payments/stripe/stripePaymentReconciliationProvider.ts +263 -0
  184. package/src/payments/stripe/stripeRefundEvidence.ts +19 -0
  185. package/src/payments/stripe/stripeSandboxPaymentExecutionAdapter.ts +283 -0
  186. package/src/payments/stripe/stripeWebhookNormalizer.ts +277 -0
  187. package/src/payments/stripe/webhookSignature.ts +55 -0
  188. package/src/payments/tpay/blikRecurringMapper.ts +42 -0
  189. package/src/payments/tpay/capabilities.ts +21 -0
  190. package/src/payments/tpay/classifyDecline.ts +62 -0
  191. package/src/payments/tpay/declineFailureHints.ts +90 -0
  192. package/src/payments/tpay/donorContracts.ts +104 -0
  193. package/src/payments/tpay/index.ts +257 -0
  194. package/src/payments/tpay/paymentFailureEvidence.ts +58 -0
  195. package/src/payments/tpay/preparedProviderAttempt.ts +38 -0
  196. package/src/payments/tpay/providerAttemptFailureDiagnostic.ts +99 -0
  197. package/src/payments/tpay/tpayCreateTransactionInput.ts +158 -0
  198. package/src/payments/tpay/tpayDeclineEvidence.ts +233 -0
  199. package/src/payments/tpay/tpayDispatchFailure.ts +186 -0
  200. package/src/payments/tpay/tpayFailureEvidence.ts +75 -0
  201. package/src/payments/tpay/tpayHttpClient.ts +290 -0
  202. package/src/payments/tpay/tpayHttpFailure.ts +186 -0
  203. package/src/payments/tpay/tpayPaymentExecutionAdapter.ts +297 -0
  204. package/src/payments/tpay/tpayPaymentReconciliationProvider.ts +285 -0
  205. package/src/payments/tpay/tpayTransactionPayload.ts +207 -0
  206. package/src/payments/tpay/wallClockTime.ts +57 -0
  207. package/src/payments/tpay/webhookJwsVerifier.ts +82 -0
  208. package/src/payments/tpay/webhookNormalizer.ts +279 -0
  209. package/src/payments/verifiedEvidenceProjection.ts +179 -0
  210. package/src/payments/verifiedIngress.ts +317 -0
@@ -0,0 +1,200 @@
1
+ const PROVIDER_KIND = "tpay";
2
+ /**
3
+ * Alias-rail event names, mapped to the neutral lifecycle vocabulary.
4
+ *
5
+ * ⛔ `ALIAS_UPDATE` is a ROTATION, not a death: the payer's standing consent is
6
+ * unchanged and only the alias behind it moves. Classifying it with the two
7
+ * terminal names would strip a healthy subscription of the mandate it renews
8
+ * on. That is why the `active` flag below already treats register and update
9
+ * alike, and why this table must keep them apart from unregister and expiry.
10
+ */
11
+ const ALIAS_LIFECYCLE_KINDS = {
12
+ ALIAS_REGISTER: "method_registered",
13
+ ALIAS_UPDATE: "method_updated",
14
+ ALIAS_UNREGISTER: "method_revoked",
15
+ ALIAS_EXPIRED: "method_expired",
16
+ };
17
+ export function normalizeTpayWebhook(rawBody) {
18
+ const payload = parsePayload(rawBody);
19
+ const aliasEvent = payload.event;
20
+ if (aliasEvent) {
21
+ return normalizeAliasWebhook(payload);
22
+ }
23
+ return normalizeTransactionWebhook(payload);
24
+ }
25
+ export function toNormalizedProviderTpayWebhook(webhook, occurredAt) {
26
+ return {
27
+ provider: PROVIDER_KIND,
28
+ providerEventId: webhook.event.provider_event_id,
29
+ eventType: webhook.event.event_type,
30
+ providerPaymentId: webhook.event.payment_provider_id,
31
+ paymentIntentId: webhook.event.payment_intent_id ?? null,
32
+ amountMinor: webhook.event.amount_minor ?? null,
33
+ currency: readCurrency(webhook.event.raw_payload),
34
+ occurredAt,
35
+ rawPayload: webhook.event.raw_payload,
36
+ reusableMethod: toReusableMethod(webhook.methodRef),
37
+ // The alias value IS the stored method reference on this rail. The rail
38
+ // publishes no scheme label or digits with an alias transition, and its
39
+ // expiry field is deliberately not read here (see the wave plan): a
40
+ // transition on this rail is a state change, not a facts refresh.
41
+ methodLifecycle: webhook.methodLifecycleKind
42
+ ? {
43
+ kind: webhook.methodLifecycleKind,
44
+ providerKind: PROVIDER_KIND,
45
+ providerMethodRef: webhook.event.payment_provider_id,
46
+ providerEventId: webhook.event.provider_event_id,
47
+ occurredAt,
48
+ replacement: null,
49
+ }
50
+ : null,
51
+ };
52
+ }
53
+ function normalizeTransactionWebhook(payload) {
54
+ const transactionTitle = required(payload, "tr_id");
55
+ const rawStatus = required(payload, "tr_status");
56
+ // Tpay production sends `tr_status` UPPERCASE ("TRUE"/"CHARGEBACK"); a
57
+ // case-sensitive compare mislabels every real success as payment.failed
58
+ // (caught live 2026-07-13 via a production payment-link). Classify
59
+ // case-insensitively; keep the raw value in raw_payload for observability.
60
+ const status = rawStatus.toLowerCase();
61
+ const amountMinor = amountToMinor(payload.tr_paid || payload.tr_amount);
62
+ const paymentIntentId = parseUuid(payload.tr_crc);
63
+ return {
64
+ event: {
65
+ // Only chargebacks split identity; every other status keeps its legacy keys.
66
+ provider_event_id: status === "chargeback" ? `${transactionTitle}:chargeback` : transactionTitle,
67
+ event_type: status === "chargeback" ? "payment.refunded" : status === "true" ? "payment.succeeded" : "payment.failed",
68
+ payment_provider_id: transactionTitle,
69
+ payment_intent_id: paymentIntentId,
70
+ amount_minor: amountMinor,
71
+ raw_payload: {
72
+ provider: PROVIDER_KIND,
73
+ eventKind: "transaction",
74
+ transactionTitle,
75
+ hiddenDescription: payload.tr_crc ?? null,
76
+ transactionAmount: payload.tr_amount ?? null,
77
+ paidAmount: payload.tr_paid ?? null,
78
+ // Null, not the settlement default: this records what the provider SAID,
79
+ // and a currency it did not send is unknown. The whole path was already
80
+ // built for that - `NormalizedTpayProviderWebhook.currency` is typed
81
+ // `string | null` and `readCurrency` below returns null for an absent or
82
+ // blank field - so the old `?? "PLN"` was manufacturing an operand rather
83
+ // than filling a gap.
84
+ //
85
+ // It does not weaken the SQL cross-check either. `commerce_payment_control_
86
+ // apply_result` guards `v_event.currency IS NOT NULL AND v_event.currency <>
87
+ // v_intent.currency`; a fabricated code cannot detect a real mismatch, it can
88
+ // only compare equal by luck in a PLN deployment or raise a *false*
89
+ // `payment_control_result_currency_mismatch` in one that settles elsewhere and
90
+ // refuse a legitimate payment. Skipping a comparison nobody can make beats
91
+ // making one against an invented value.
92
+ currency: payload.tr_currency ?? null,
93
+ status: rawStatus,
94
+ testMode: payload.test_mode ?? null,
95
+ },
96
+ },
97
+ methodRef: null,
98
+ methodLifecycleKind: null,
99
+ responseBody: "TRUE",
100
+ };
101
+ }
102
+ function normalizeAliasWebhook(payload) {
103
+ const aliasValue = required(payload, "msg_value[value]");
104
+ // Tpay sends alias event names UPPERCASE; normalize defensively so the
105
+ // classification can never be defeated by casing (see tr_status above).
106
+ const event = required(payload, "event").toUpperCase();
107
+ const active = event === "ALIAS_REGISTER" || event === "ALIAS_UPDATE";
108
+ const paymentIntentId = paymentIntentIdFromAlias(aliasValue);
109
+ return {
110
+ event: {
111
+ provider_event_id: `${event}:${aliasValue}`,
112
+ event_type: active ? "setup.succeeded" : "setup.failed",
113
+ payment_provider_id: aliasValue,
114
+ payment_intent_id: paymentIntentId,
115
+ raw_payload: {
116
+ provider: PROVIDER_KIND,
117
+ eventKind: "blik_alias",
118
+ event,
119
+ paymentIntentId,
120
+ aliasValuePresent: true,
121
+ aliasType: payload["msg_value[type]"] ?? null,
122
+ aliasExpirationDatePresent: Boolean(payload["msg_value[expirationDate]"]),
123
+ },
124
+ },
125
+ methodRef: {
126
+ clientId: payload.client_id ?? null,
127
+ subscriptionId: payload.subscription_id ?? null,
128
+ providerMethodRef: aliasValue,
129
+ methodKind: "blik_payid",
130
+ status: active ? "active" : "inactive",
131
+ consentSnapshot: { source: "tpay.alias.webhook", event },
132
+ },
133
+ methodLifecycleKind: ALIAS_LIFECYCLE_KINDS[event] ?? null,
134
+ responseBody: "TRUE",
135
+ };
136
+ }
137
+ function toReusableMethod(methodRef) {
138
+ if (!methodRef?.clientId)
139
+ return null;
140
+ return {
141
+ clientId: methodRef.clientId,
142
+ subscriptionId: methodRef.subscriptionId ?? null,
143
+ providerCustomerRef: null,
144
+ providerMethodRef: methodRef.providerMethodRef,
145
+ providerMandateRef: null,
146
+ methodKind: methodRef.methodKind,
147
+ status: methodRef.status,
148
+ consentSnapshot: methodRef.consentSnapshot,
149
+ };
150
+ }
151
+ function readCurrency(payload) {
152
+ const currency = payload.currency;
153
+ return typeof currency === "string" && currency.trim() ? currency : null;
154
+ }
155
+ function parsePayload(rawBody) {
156
+ const trimmed = rawBody.trim();
157
+ if (trimmed.startsWith("{")) {
158
+ const parsed = JSON.parse(trimmed);
159
+ return flattenJson(parsed);
160
+ }
161
+ const params = new URLSearchParams(rawBody);
162
+ return Object.fromEntries(Array.from(params.entries()));
163
+ }
164
+ function flattenJson(value, prefix = "") {
165
+ const out = {};
166
+ for (const [key, raw] of Object.entries(value)) {
167
+ const name = prefix ? `${prefix}[${key}]` : key;
168
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
169
+ Object.assign(out, flattenJson(raw, name));
170
+ }
171
+ else if (raw !== undefined && raw !== null) {
172
+ out[name] = String(raw);
173
+ }
174
+ }
175
+ return out;
176
+ }
177
+ function required(payload, key) {
178
+ const value = payload[key];
179
+ if (!value)
180
+ throw new Error(`tpay_webhook_missing_${key}`);
181
+ return value;
182
+ }
183
+ function amountToMinor(value) {
184
+ if (!value)
185
+ return undefined;
186
+ const numeric = Number(value.replace(",", "."));
187
+ return Number.isFinite(numeric) ? Math.round(numeric * 100) : undefined;
188
+ }
189
+ function paymentIntentIdFromAlias(value) {
190
+ const match = /^openlup_([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/i.exec(value);
191
+ return match ? match[1].toLowerCase() : null;
192
+ }
193
+ function parseUuid(value) {
194
+ if (!value)
195
+ return null;
196
+ const trimmed = value.trim();
197
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(trimmed)
198
+ ? trimmed.toLowerCase()
199
+ : null;
200
+ }
@@ -0,0 +1,12 @@
1
+ import type { PaymentVerifyNormalizePort, RefundEvidencePolicy, VerifiedMethodEvidence, VerifiedPaymentEvidence } from "./contracts/evidence.js";
2
+ import type { ProviderWebhookWithMethodLifecycle } from "./paymentMethodLifecycle.js";
3
+ /** New boundary module: verified adapter envelopes into the unchanged donor carrier.
4
+ * Original bytes go only to verification. Store adapter-redacted audit facts plus
5
+ * nested normalized evidence; typed facts override overlapping reader keys.
6
+ * Neither audit shape nor receipt time changes adapter identity/fingerprint.
7
+ * Incomplete refunds are ingested and acknowledged, but require-evidence prevents apply.
8
+ */
9
+ export declare function projectVerifiedEvidence(evidence: VerifiedPaymentEvidence | VerifiedMethodEvidence, input: Parameters<PaymentVerifyNormalizePort["verifyAndNormalize"]>[0], policy: RefundEvidencePolicy, methodOwner?: {
10
+ clientId: string;
11
+ subscriptionId?: string | null;
12
+ }): ProviderWebhookWithMethodLifecycle;
@@ -0,0 +1,166 @@
1
+ /** New boundary module: verified adapter envelopes into the unchanged donor carrier.
2
+ * Original bytes go only to verification. Store adapter-redacted audit facts plus
3
+ * nested normalized evidence; typed facts override overlapping reader keys.
4
+ * Neither audit shape nor receipt time changes adapter identity/fingerprint.
5
+ * Incomplete refunds are ingested and acknowledged, but require-evidence prevents apply.
6
+ */
7
+ export function projectVerifiedEvidence(evidence, input, policy, methodOwner) {
8
+ const payload = auditPayload(evidence);
9
+ const base = {
10
+ provider: evidence.provider, providerEventId: evidence.providerEventId,
11
+ occurredAt: input.receivedAt, rawPayload: payload,
12
+ paymentIntentId: evidence.intentRef,
13
+ };
14
+ switch (evidence.kind) {
15
+ case "money": {
16
+ // The donor treats absent money as unasserted and skips parity checks.
17
+ // Settling adapter evidence must instead use its existing mismatch refusal.
18
+ if ((evidence.outcome === "succeeded" || evidence.outcome === "refunded" || evidence.outcome === "disputed")
19
+ && (evidence.money === null || !Number.isSafeInteger(evidence.money.amountMinor)
20
+ || evidence.money.amountMinor <= 0 || !evidence.money.currency.trim())) {
21
+ throw new Error("payment_control_result_amount_mismatch");
22
+ }
23
+ const reversal = evidence.refund;
24
+ const refundKind = evidence.outcome === "refunded"
25
+ ? (reversal ? projectRefund(reversal, evidence) : null) ?? (policy === "legacy-assume-full" ? "full" : null)
26
+ : undefined;
27
+ return {
28
+ ...base, eventType: moneyEvent(evidence.outcome, refundKind ? reversal : undefined),
29
+ providerPaymentId: evidence.providerPaymentRef, paymentAttemptId: evidence.attemptRef,
30
+ amountMinor: evidence.money?.amountMinor ?? null, currency: evidence.money?.currency ?? null,
31
+ refundKind,
32
+ };
33
+ }
34
+ case "method":
35
+ return {
36
+ ...base, eventType: methodEvent(evidence.outcome),
37
+ // Pinned donor normalizer :59 calls resolveLinkagePaymentId :146–154:
38
+ // setup/method objects use their own object id as the carrier reference.
39
+ providerPaymentId: evidence.setupRef ?? evidence.methodRef,
40
+ methodLifecycle: lifecycle(evidence.methodLifecycle),
41
+ ...(evidence.reusableMethod && methodOwner ? { reusableMethod: {
42
+ clientId: methodOwner.clientId, subscriptionId: methodOwner.subscriptionId,
43
+ providerCustomerRef: evidence.reusableMethod.providerCustomerRef,
44
+ providerMethodRef: evidence.reusableMethod.providerMethodRef,
45
+ providerMandateRef: evidence.reusableMethod.providerMandateRef,
46
+ methodKind: evidence.reusableMethod.methodKind, status: evidence.reusableMethod.status,
47
+ } } : {}),
48
+ };
49
+ }
50
+ return assertNever(evidence);
51
+ }
52
+ function moneyEvent(outcome, refund) {
53
+ switch (outcome) {
54
+ case "succeeded": return "payment.succeeded";
55
+ case "failed": return "payment.failed";
56
+ case "pending": return "payment.requires_action";
57
+ case "refunded": return refund?.kind === "full-reversal" && refund.reason === "chargeback" ? "payment.disputed" : "payment.refunded";
58
+ case "disputed": return "payment.disputed";
59
+ }
60
+ return assertNever(outcome);
61
+ }
62
+ function methodEvent(outcome) {
63
+ switch (outcome) {
64
+ case "registered":
65
+ case "updated": return "setup.succeeded";
66
+ case "revoked":
67
+ case "expired":
68
+ case "suspended":
69
+ case "failed": return "setup.failed";
70
+ case "requires-action": return "setup.requires_action";
71
+ }
72
+ return assertNever(outcome);
73
+ }
74
+ function projectRefund(refund, evidence) {
75
+ const original = refund.originalPaymentMoney;
76
+ if (!refund.operationId?.trim() || !evidence.providerPaymentRef.trim()
77
+ || !Number.isSafeInteger(original?.amountMinor) || original.amountMinor <= 0
78
+ || !original.currency?.trim() || !evidence.money
79
+ || evidence.money.amountMinor !== original.amountMinor || evidence.money.currency !== original.currency) {
80
+ return null;
81
+ }
82
+ switch (refund.kind) {
83
+ case "full-reversal": return "full";
84
+ case "cumulative-snapshot":
85
+ case "event-delta": {
86
+ const money = refund.refundedMoney;
87
+ if (!Number.isSafeInteger(money?.amountMinor) || money.amountMinor <= 0
88
+ || money.amountMinor > original.amountMinor || money.currency !== original.currency) {
89
+ return null;
90
+ }
91
+ return money.amountMinor < original.amountMinor ? "partial" : "full";
92
+ }
93
+ }
94
+ return assertNever(refund);
95
+ }
96
+ function assertNever(value) {
97
+ throw new Error(`payment_evidence_unmapped:${String(value)}`);
98
+ }
99
+ function auditPayload(evidence) {
100
+ // Explicit typed fields only: unknown envelope properties never enter storage.
101
+ const normalized = {
102
+ provider: evidence.provider, providerEventId: evidence.providerEventId,
103
+ fingerprint: evidence.fingerprint,
104
+ providerOccurredAt: evidence.providerOccurredAt, kind: evidence.kind,
105
+ intentRef: evidence.intentRef, outcome: evidence.outcome,
106
+ ...(evidence.kind === "money" ? {
107
+ providerPaymentRef: evidence.providerPaymentRef, attemptRef: evidence.attemptRef,
108
+ money: evidence.money && money(evidence.money),
109
+ refusal: evidence.refusal && { code: evidence.refusal.code,
110
+ hints: [...evidence.refusal.hints], adviceCode: evidence.refusal.adviceCode },
111
+ refund: evidence.refund && refund(evidence.refund),
112
+ } : {
113
+ methodRef: evidence.methodRef, setupRef: evidence.setupRef,
114
+ methodLifecycle: lifecycle(evidence.methodLifecycle),
115
+ reusableMethod: evidence.reusableMethod && {
116
+ providerCustomerRef: evidence.reusableMethod.providerCustomerRef,
117
+ providerMethodRef: evidence.reusableMethod.providerMethodRef,
118
+ providerMandateRef: evidence.reusableMethod.providerMandateRef,
119
+ methodKind: evidence.reusableMethod.methodKind, status: evidence.reusableMethod.status,
120
+ },
121
+ }),
122
+ };
123
+ const payload = { ...evidence.providerAuditPayload };
124
+ if (evidence.kind === "money") {
125
+ // Host readers use the provider object reference and original payment money.
126
+ payload.providerPaymentId = evidence.providerPaymentRef;
127
+ payload.amountMinor = evidence.money?.amountMinor ?? null;
128
+ payload.currency = evidence.money?.currency ?? null;
129
+ // Snapshot reader namespaces are adapter-owned. Their charge identity has no
130
+ // typed home; their cumulative amount must come from the typed observation.
131
+ for (const [key, value] of Object.entries(payload)) {
132
+ if (value && typeof value === "object" && !Array.isArray(value) && "chargeId" in value) {
133
+ payload[key] = { ...value, amountRefundedMinor: evidence.refund?.kind === "cumulative-snapshot"
134
+ ? evidence.refund.refundedMoney?.amountMinor ?? null : null };
135
+ }
136
+ }
137
+ const failure = payload.failureEvidence;
138
+ if (failure && typeof failure === "object" && !Array.isArray(failure)) {
139
+ payload.failureEvidence = { ...failure, providerPaymentId: evidence.providerPaymentRef,
140
+ code: evidence.refusal?.code ?? null, adviceCode: evidence.refusal?.adviceCode ?? null };
141
+ }
142
+ }
143
+ payload.openlupEvidence = normalized;
144
+ return payload;
145
+ }
146
+ function money(value) {
147
+ return { amountMinor: value.amountMinor, currency: value.currency };
148
+ }
149
+ function refund(value) {
150
+ const base = { operationId: value.operationId, originalPaymentMoney: value.originalPaymentMoney && money(value.originalPaymentMoney) };
151
+ switch (value.kind) {
152
+ case "full-reversal": return { ...base, kind: value.kind, reason: value.reason };
153
+ case "cumulative-snapshot":
154
+ case "event-delta":
155
+ return { ...base, kind: value.kind, refundedMoney: value.refundedMoney && money(value.refundedMoney) };
156
+ }
157
+ return assertNever(value);
158
+ }
159
+ function lifecycle(value) {
160
+ return value && { kind: value.kind, providerKind: value.providerKind,
161
+ providerMethodRef: value.providerMethodRef, providerEventId: value.providerEventId,
162
+ occurredAt: value.occurredAt, replacement: value.replacement && {
163
+ schemeLabel: value.replacement.schemeLabel, lastDigits: value.replacement.lastDigits,
164
+ expiresAt: value.replacement.expiresAt,
165
+ } };
166
+ }
@@ -0,0 +1,119 @@
1
+ import type { PaymentIngressPort, PaymentVerifyNormalizePort, RefundEvidencePolicy, VerifiedMethodEvidence } from "./contracts/evidence.js";
2
+ declare const PAYMENT_WEBHOOK_EVENT_TYPES: readonly ["payment.succeeded", "payment.failed", "payment.refunded", "payment.requires_action", "payment.disputed", "setup.succeeded", "setup.failed", "setup.requires_action"];
3
+ type PaymentWebhookEventType = (typeof PAYMENT_WEBHOOK_EVENT_TYPES)[number];
4
+ export type ProviderPaymentWebhookKind = PaymentWebhookEventType;
5
+ export interface NormalizedProviderPaymentWebhook {
6
+ provider: string;
7
+ providerEventId: string;
8
+ eventType: ProviderPaymentWebhookKind;
9
+ providerPaymentId: string;
10
+ paymentIntentId?: string | null;
11
+ paymentAttemptId?: string | null;
12
+ amountMinor?: number | null;
13
+ currency?: string | null;
14
+ occurredAt: string;
15
+ refundKind?: "full" | "partial" | null;
16
+ rawPayload: Record<string, unknown>;
17
+ reusableMethod?: {
18
+ clientId: string;
19
+ subscriptionId?: string | null;
20
+ providerCustomerRef?: string | null;
21
+ providerMethodRef: string;
22
+ providerMandateRef?: string | null;
23
+ methodKind: string;
24
+ status: "pending_verification" | "active" | "inactive" | "expired" | "revoked";
25
+ consentSnapshot?: Record<string, unknown>;
26
+ } | null;
27
+ }
28
+ export interface PaymentWebhookControlPort {
29
+ ingestPaymentEvent(input: NormalizedProviderPaymentWebhook): Promise<{
30
+ paymentEventId: string;
31
+ paymentIntentId: string | null;
32
+ paymentAttemptId: string | null;
33
+ replayed: boolean;
34
+ occurredAt?: string;
35
+ }>;
36
+ applyPaymentResult?(input: {
37
+ idempotencyKey: string;
38
+ paymentIntentId: string;
39
+ paymentEventId: string;
40
+ resultStatus: "succeeded" | "failed" | "refunded" | "partially_refunded" | "disputed";
41
+ occurredAt: string;
42
+ failureReason: string | null;
43
+ }): Promise<{
44
+ replayed: boolean;
45
+ }>;
46
+ confirmSubscriptionActivation?(input: {
47
+ idempotencyKey: string;
48
+ paymentIntentId: string;
49
+ occurredAt: string;
50
+ methodRef: string | null;
51
+ methodKind: string | null;
52
+ }): Promise<{
53
+ confirmed: boolean;
54
+ status: string | null;
55
+ }>;
56
+ recoverPaidSubscriptionActivationWithCard?(input: {
57
+ idempotencyKey: string;
58
+ subscriptionId: string;
59
+ paymentMethodRef: string;
60
+ occurredAt: string;
61
+ }): Promise<{
62
+ recovered: boolean;
63
+ replayed: boolean;
64
+ reason: string | null;
65
+ }>;
66
+ /**
67
+ * Closes the ledger row of a `setup.*` event once the full setup pipeline
68
+ * resolved. Setup events carry no money result, so applyPaymentResult (the
69
+ * only other processed_at writer) never runs for them.
70
+ */
71
+ markSetupEventProcessed?(input: {
72
+ paymentEventId: string;
73
+ }): Promise<{
74
+ updated: boolean;
75
+ }>;
76
+ }
77
+ export interface PaymentWebhookMethodRefPort {
78
+ upsertFromWebhook(input: NormalizedProviderPaymentWebhook): Promise<{
79
+ replayed: boolean;
80
+ }>;
81
+ }
82
+ export type PaymentWebhookAfterProcessed = (input: {
83
+ event: NormalizedProviderPaymentWebhook;
84
+ ingested: {
85
+ paymentEventId: string;
86
+ paymentIntentId: string | null;
87
+ paymentAttemptId: string | null;
88
+ replayed: boolean;
89
+ };
90
+ resultStatus: "succeeded" | "failed" | "refunded" | "partially_refunded" | "disputed" | null;
91
+ }) => Promise<void>;
92
+ export interface PaymentWebhookProcessingPorts {
93
+ paymentControlPort: PaymentWebhookControlPort;
94
+ methodRefPort?: PaymentWebhookMethodRefPort;
95
+ afterProcessed?: PaymentWebhookAfterProcessed;
96
+ }
97
+ type OperationalEventRecorder = (event: {
98
+ name: string;
99
+ domain: string;
100
+ surface: string;
101
+ details: Record<string, unknown>;
102
+ }) => void;
103
+ /** Verified ingress boundary; persistence and transport response writing belong to the host. */
104
+ export declare function createVerifiedPaymentIngress({ provider, verifyPort, paymentControlPort, methodRefPort, afterProcessed, createPorts, resolveMethodOwner, refundEvidencePolicy, operationalEvents, }: {
105
+ provider: string;
106
+ verifyPort: PaymentVerifyNormalizePort;
107
+ paymentControlPort: PaymentWebhookControlPort;
108
+ methodRefPort?: PaymentWebhookMethodRefPort;
109
+ afterProcessed?: PaymentWebhookAfterProcessed;
110
+ createPorts?: (event: NormalizedProviderPaymentWebhook) => PaymentWebhookProcessingPorts | Promise<PaymentWebhookProcessingPorts>;
111
+ /** Host-owned identity; adapter metadata cannot supply client/subscription authority. */
112
+ resolveMethodOwner?: (evidence: VerifiedMethodEvidence) => Promise<{
113
+ clientId: string;
114
+ subscriptionId?: string | null;
115
+ } | null>;
116
+ refundEvidencePolicy?: RefundEvidencePolicy;
117
+ operationalEvents?: OperationalEventRecorder;
118
+ }): PaymentIngressPort;
119
+ export {};