@openlup/platform 0.19.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 (217) hide show
  1. package/CHANGELOG.md +161 -231
  2. package/README.md +182 -8
  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/dist/shipping/bindings.d.ts +5 -0
  139. package/dist/shipping/bindings.js +12 -2
  140. package/dist/shipping/index.d.ts +14 -0
  141. package/dist/shipping/index.js +5 -0
  142. package/package.json +22 -2
  143. package/release-gates.json +64 -3
  144. package/sql/20260810180000_payment_settlement_rail.sql +574 -0
  145. package/sql/20260815150000_payment_truth_reconciliation.sql +479 -0
  146. package/sql/20260826100000_late_payment_shifts_the_subscription_cycle.sql +215 -0
  147. package/sql/20261021200000_shipping_flat_rate_seed.sql +1 -0
  148. package/src/composition/composeBundle.ts +9 -0
  149. package/src/composition/index.ts +1 -0
  150. package/src/payments/adapterConformance.ts +118 -0
  151. package/src/payments/bindings.ts +17 -0
  152. package/src/payments/checkoutActivePaymentActionResolver.ts +97 -0
  153. package/src/payments/checkoutRecoveryPaymentResolver.ts +108 -0
  154. package/src/payments/contracts/adapter.ts +96 -0
  155. package/src/payments/contracts/conformance.ts +171 -0
  156. package/src/payments/contracts/evidence.ts +142 -0
  157. package/src/payments/contracts/index.ts +54 -0
  158. package/src/payments/contracts/preparedAttempt.ts +86 -0
  159. package/src/payments/contracts/reconciliation.ts +296 -0
  160. package/src/payments/contracts/repair.ts +95 -0
  161. package/src/payments/contracts/truth.ts +108 -0
  162. package/src/payments/eventConformanceRunner.ts +149 -0
  163. package/src/payments/index.ts +21 -0
  164. package/src/payments/paymentAttemptSilenceWindow.ts +26 -0
  165. package/src/payments/paymentMethodLifecycle.ts +50 -0
  166. package/src/payments/paymentProviderReconciliationAbsenceReopen.ts +130 -0
  167. package/src/payments/paymentProviderReconciliationEvidence.ts +117 -0
  168. package/src/payments/paymentProviderReconciliationWorker.ts +291 -0
  169. package/src/payments/paymentTruth.ts +131 -0
  170. package/src/payments/paymentVerifyNowHandler.ts +94 -0
  171. package/src/payments/paymentVerifyNowService.ts +125 -0
  172. package/src/payments/paymentWebhookPostProcessing.ts +36 -0
  173. package/src/payments/postgresPaymentTruth.ts +149 -0
  174. package/src/payments/preparedAttempt.ts +90 -0
  175. package/src/payments/preparedAttemptFinalization.ts +13 -0
  176. package/src/payments/schemaCapabilities.ts +10 -0
  177. package/src/payments/sql.ts +6 -0
  178. package/src/payments/stripe/classifyDecline.ts +62 -0
  179. package/src/payments/stripe/declineFailureHints.ts +89 -0
  180. package/src/payments/stripe/donorContracts.ts +87 -0
  181. package/src/payments/stripe/executionDecline.ts +126 -0
  182. package/src/payments/stripe/index.ts +235 -0
  183. package/src/payments/stripe/intentMapper.ts +58 -0
  184. package/src/payments/stripe/methodFacts.ts +21 -0
  185. package/src/payments/stripe/paymentFailureEvidence.ts +58 -0
  186. package/src/payments/stripe/stripeFailureEvidence.ts +115 -0
  187. package/src/payments/stripe/stripeIntentConfirmationEvidence.ts +34 -0
  188. package/src/payments/stripe/stripePaymentReconciliationProvider.ts +263 -0
  189. package/src/payments/stripe/stripeRefundEvidence.ts +19 -0
  190. package/src/payments/stripe/stripeSandboxPaymentExecutionAdapter.ts +283 -0
  191. package/src/payments/stripe/stripeWebhookNormalizer.ts +277 -0
  192. package/src/payments/stripe/webhookSignature.ts +55 -0
  193. package/src/payments/tpay/blikRecurringMapper.ts +42 -0
  194. package/src/payments/tpay/capabilities.ts +21 -0
  195. package/src/payments/tpay/classifyDecline.ts +62 -0
  196. package/src/payments/tpay/declineFailureHints.ts +90 -0
  197. package/src/payments/tpay/donorContracts.ts +104 -0
  198. package/src/payments/tpay/index.ts +257 -0
  199. package/src/payments/tpay/paymentFailureEvidence.ts +58 -0
  200. package/src/payments/tpay/preparedProviderAttempt.ts +38 -0
  201. package/src/payments/tpay/providerAttemptFailureDiagnostic.ts +99 -0
  202. package/src/payments/tpay/tpayCreateTransactionInput.ts +158 -0
  203. package/src/payments/tpay/tpayDeclineEvidence.ts +233 -0
  204. package/src/payments/tpay/tpayDispatchFailure.ts +186 -0
  205. package/src/payments/tpay/tpayFailureEvidence.ts +75 -0
  206. package/src/payments/tpay/tpayHttpClient.ts +290 -0
  207. package/src/payments/tpay/tpayHttpFailure.ts +186 -0
  208. package/src/payments/tpay/tpayPaymentExecutionAdapter.ts +297 -0
  209. package/src/payments/tpay/tpayPaymentReconciliationProvider.ts +285 -0
  210. package/src/payments/tpay/tpayTransactionPayload.ts +207 -0
  211. package/src/payments/tpay/wallClockTime.ts +57 -0
  212. package/src/payments/tpay/webhookJwsVerifier.ts +82 -0
  213. package/src/payments/tpay/webhookNormalizer.ts +279 -0
  214. package/src/payments/verifiedEvidenceProjection.ts +179 -0
  215. package/src/payments/verifiedIngress.ts +317 -0
  216. package/src/shipping/bindings.ts +18 -2
  217. package/src/shipping/index.ts +6 -0
@@ -0,0 +1,249 @@
1
+ import { failureEvidencePayload } from "./paymentFailureEvidence.js";
2
+ import { tpayFailureEvidence } from "./tpayFailureEvidence.js";
3
+ import { aliasValue, buildCreateTransactionInput, resolveRecurringModel, } from "./tpayCreateTransactionInput.js";
4
+ import { declineCodeReading } from "./declineFailureHints.js";
5
+ import { assertableHints, isMandateRefusal, mandateCapabilityReadable, readAttemptDeclineObservation, declineEvidenceSource, states, } from "./tpayDeclineEvidence.js";
6
+ import { tpayTrustedPreDispatchFailure, tpayTrustedRefusalFailure, } from "./tpayDispatchFailure.js";
7
+ import { ProviderAttemptPreDispatchError } from "./preparedProviderAttempt.js";
8
+ /** The one place this adapter spells its own provider identity. */
9
+ export const PROVIDER_KIND = "tpay";
10
+ export function createTpayPaymentExecutionAdapter({ client, notificationUrl, successUrl, errorUrl, oneClickEnabled = false, aliasRegistrationOnCodeEnabled = false, modelOActivationEnabled = false, resolvePayer = defaultPayer, }) {
11
+ const validateInput = (input) => {
12
+ const transient = requireTpayTransientInput(input, {
13
+ oneClickEnabled,
14
+ modelOActivationEnabled,
15
+ });
16
+ if (input.currency !== "PLN")
17
+ throw new TpayExecutionInputError("tpay_currency_not_supported");
18
+ const payer = resolvePayer(input);
19
+ if (!payer)
20
+ throw new TpayExecutionInputError("tpay_payer_missing");
21
+ return { transient, payer };
22
+ };
23
+ return {
24
+ validateInput(input) {
25
+ validateInput(input);
26
+ },
27
+ async execute(input) {
28
+ const { transient, payer } = validateInput(input);
29
+ const request = buildCreateTransactionInput({
30
+ input,
31
+ transient,
32
+ payer,
33
+ notificationUrl,
34
+ successUrl,
35
+ errorUrl,
36
+ aliasRegistrationOnCodeEnabled,
37
+ modelOActivationEnabled,
38
+ });
39
+ let response;
40
+ try {
41
+ response = await client.createTransaction(request);
42
+ }
43
+ catch (error) {
44
+ // Two independent proofs Tpay holds no transaction: never asked, or it refused the request
45
+ // document. Both release the attempt; everything else stays ambiguous and rethrows.
46
+ const noCharge = tpayTrustedPreDispatchFailure(error) ?? tpayTrustedRefusalFailure(error);
47
+ if (noCharge)
48
+ throw new ProviderAttemptPreDispatchError(noCharge);
49
+ throw error;
50
+ }
51
+ const providerAttemptId = response.title || response.transactionId;
52
+ // This provider reports a synchronous decline ONLY here. `result`, `status`
53
+ // and `payments.status` all still read "success"/"pending" on a rejected
54
+ // transaction, so they cannot be used to detect one.
55
+ const rejections = response.errors ?? [];
56
+ const rejection = rejections[0] ?? null;
57
+ // The provider has carried a hard refusal code since the first minute, but
58
+ // only in the transaction's attempt log — never in the create response,
59
+ // whose codes are generic. So ask for it the moment the provider says no,
60
+ // instead of learning it hours later from the reconciliation poll.
61
+ // Fail-open: `null` means "read nothing", and every branch below then
62
+ // behaves exactly as it did before this read existed.
63
+ const observation = rejection
64
+ ? await readAttemptDeclineObservation(client, response.transactionId)
65
+ : null;
66
+ const attemptCode = observation?.code ?? null;
67
+ // A code the table cannot read decides nothing. Promoting a known-unknown
68
+ // such as `"100"` to a verdict would silence the message fallback for the
69
+ // one code we openly admit we cannot read — a regression, not a fix.
70
+ const attemptReading = attemptCode === null ? null : declineCodeReading(attemptCode);
71
+ const decidingAttemptReading = attemptReading !== null && states(attemptReading)
72
+ ? attemptReading
73
+ : null;
74
+ const mandateUnsupported = decidingAttemptReading
75
+ ? mandateCapabilityReadable(transient.flow)
76
+ && response.payIdEligible !== true
77
+ && (decidingAttemptReading.hints?.includes("mandateUnsupported") ?? false)
78
+ : rejection !== null && rejections.some((error) => isMandateRefusal(transient.flow, response.payIdEligible, error.errorMessage));
79
+ // Read here, where the code vocabulary is owned, so the control plane
80
+ // classifies the refusal without ever seeing a provider-native code. The
81
+ // attempt log wins when it says something, because it is the carrier the
82
+ // provider itself writes the refusal into; the create response's own code
83
+ // is the fallback reading.
84
+ // The advice half is suppressed when this adapter is already asserting a
85
+ // mandate refusal: advice outranks hints by design in the taxonomy, and
86
+ // the identity code is only the FIRST error, so a generic terminal code
87
+ // arriving ahead of the mandate explanation would otherwise mask the one
88
+ // reading that says the stored reference — not the instrument — is dead.
89
+ const reading = decidingAttemptReading ?? declineCodeReading(rejection?.errorCode);
90
+ const declineEvidence = rejection ? declineEvidenceSource({
91
+ attemptCode, decidingAttemptReading, mandateUnsupported, reading, errorCode: rejection.errorCode,
92
+ }) : null;
93
+ // A capability code is read as mandate evidence only while registering a
94
+ // mandate. The same opaque code on one-time or stored-mandate charges must
95
+ // not claim that a mandate is dead. The rule itself lives beside the
96
+ // refusal evidence it qualifies, because the reconciliation rail funnels
97
+ // its hints through the same one — a refusal must not classify by which
98
+ // rail saw it.
99
+ const neutralReasonHints = assertableHints(reading.hints, transient.flow);
100
+ const providerDecline = rejection
101
+ ? {
102
+ code: rejection.errorCode,
103
+ // Preserve the first provider code as the stable decline identity, but
104
+ // inspect every error for the capability signal. The provider may put a
105
+ // generic payment failure first and the mandate explanation later.
106
+ mandateUnsupported,
107
+ ...(reading.adviceCode && !mandateUnsupported ? { adviceCode: reading.adviceCode } : {}),
108
+ ...(neutralReasonHints?.length ? { neutralReasonHints } : {}),
109
+ }
110
+ : null;
111
+ return {
112
+ provider: PROVIDER_KIND,
113
+ providerAttemptId,
114
+ providerSessionId: response.transactionId,
115
+ // Stays non-terminal even on a decline: the control plane owns terminal
116
+ // transitions and reads `providerDecline` below to make one.
117
+ attemptStatus: "processing",
118
+ providerDecline,
119
+ nextActionKind: !providerDecline && transient.flow === "pbl_one_time" && response.transactionPaymentUrl
120
+ ? "redirect"
121
+ : null,
122
+ redirectUrl: providerDecline ? null : response.transactionPaymentUrl,
123
+ // A rejected activation must not publish a payment method reference.
124
+ paymentMethodRef: transient.flow === "blik_recurring_activation" && !providerDecline
125
+ ? aliasValue(input.paymentIntentId)
126
+ : null,
127
+ // A declined transaction produces no webhook at all: Tpay's `tr_status`
128
+ // only ever carries `true` or `chargeback`. Claiming otherwise would
129
+ // strand the attempt waiting for a callback that never arrives.
130
+ webhookExpected: !providerDecline,
131
+ recoveryRequired: false,
132
+ requestPayload: {
133
+ providerIdempotencyKey: input.providerIdempotencyKey,
134
+ providerRequestFingerprint: input.providerRequestFingerprint,
135
+ providerFlow: transient.flow,
136
+ amountMinor: input.amountMinor,
137
+ currency: input.currency,
138
+ orderRef: input.orderRef,
139
+ channelId: transient.flow === "pbl_one_time" ? transient.channelId : undefined,
140
+ // The model actually sent, not the one the client asked for — the single
141
+ // activation switch can make those diverge, and this field is where a
142
+ // diagnosis looks for the truth.
143
+ recurringModel: transient.flow === "blik_recurring_activation"
144
+ ? resolveRecurringModel(transient.recurringModel, modelOActivationEnabled)
145
+ : undefined,
146
+ methodRefPresent: transient.flow === "recurring_charge" || transient.flow === "blik_one_click"
147
+ ? Boolean(input.paymentMethodRef)
148
+ : undefined,
149
+ methodRefAliasType: transient.flow === "recurring_charge" || transient.flow === "blik_one_click"
150
+ ? input.paymentMethodAliasType ?? "PAYID"
151
+ : undefined,
152
+ aliasRegistrationRequested: transient.flow === "blik_one_time"
153
+ ? aliasRegistrationOnCodeEnabled
154
+ : undefined,
155
+ },
156
+ responsePayload: {
157
+ providerStatus: response.status,
158
+ providerAttemptId,
159
+ providerSessionId: response.transactionId,
160
+ requestId: response.requestId,
161
+ redirectUrlPresent: Boolean(response.transactionPaymentUrl),
162
+ payIdEligible: response.payIdEligible,
163
+ // Codes only. `errorMessage` is free-form prose that can carry
164
+ // payer-identifying text, so it never reaches our stored payload.
165
+ providerErrorCodes: (response.errors ?? []).map((error) => error.errorCode),
166
+ ...(declineEvidence ? { declineEvidence } : {}),
167
+ ...failureEvidencePayload(observation ? tpayFailureEvidence({
168
+ source: "execution", providerPaymentId: response.transactionId, refusalVerified: true,
169
+ flow: transient.flow, observation, errorCode: rejection?.errorCode, reading,
170
+ }) : null),
171
+ },
172
+ };
173
+ },
174
+ };
175
+ }
176
+ function requireTpayTransientInput(input, options) {
177
+ if (input.providerFlow === "blik_one_click") {
178
+ if (!options.oneClickEnabled)
179
+ throw new TpayExecutionInputError("tpay_blik_one_click_not_enabled");
180
+ if (input.mode !== "one_time")
181
+ throw new TpayExecutionInputError("tpay_blik_one_click_requires_one_time");
182
+ if (!input.paymentMethodRef)
183
+ throw new TpayExecutionInputError("tpay_blik_one_click_payid_missing");
184
+ if (input.transientProviderInput && input.transientProviderInput.flow !== "blik_one_click") {
185
+ throw new TpayExecutionInputError("tpay_provider_flow_mismatch");
186
+ }
187
+ return { provider: PROVIDER_KIND, flow: "blik_one_click" };
188
+ }
189
+ if (input.providerFlow === "recurring_charge") {
190
+ if (input.mode !== "subscription_cycle")
191
+ throw new TpayExecutionInputError("tpay_recurring_charge_requires_cycle");
192
+ if (!input.paymentMethodRef)
193
+ throw new TpayExecutionInputError("tpay_recurring_charge_payid_missing");
194
+ if (input.paymentMethodAliasType && input.paymentMethodAliasType !== "PAYID") {
195
+ throw new TpayExecutionInputError("tpay_recurring_charge_requires_payid");
196
+ }
197
+ if (input.paymentMethodRecurringModel !== "O") {
198
+ throw new TpayExecutionInputError("tpay_recurring_charge_requires_model_o");
199
+ }
200
+ if (input.transientProviderInput && input.transientProviderInput.flow !== "recurring_charge") {
201
+ throw new TpayExecutionInputError("tpay_provider_flow_mismatch");
202
+ }
203
+ return { provider: PROVIDER_KIND, flow: "recurring_charge" };
204
+ }
205
+ const transient = input.transientProviderInput;
206
+ if (!transient || transient.provider !== PROVIDER_KIND)
207
+ throw new TpayExecutionInputError("tpay_transient_input_missing");
208
+ if (input.providerFlow && input.providerFlow !== transient.flow) {
209
+ throw new TpayExecutionInputError("tpay_provider_flow_mismatch");
210
+ }
211
+ if (transient.flow === "blik_one_time" || transient.flow === "blik_recurring_activation") {
212
+ if (!transient.blikToken)
213
+ throw new TpayExecutionInputError("tpay_blik_token_missing");
214
+ }
215
+ if (transient.flow === "pbl_one_time" && !transient.channelId) {
216
+ throw new TpayExecutionInputError("tpay_channel_id_missing");
217
+ }
218
+ if (transient.flow === "pbl_one_time" && input.mode !== "one_time") {
219
+ throw new TpayExecutionInputError("tpay_pbl_recurring_not_supported");
220
+ }
221
+ if (transient.flow === "blik_recurring_activation") {
222
+ if (!options.modelOActivationEnabled) {
223
+ throw new TpayExecutionInputError("tpay_blik_recurring_activation_not_enabled");
224
+ }
225
+ if (transient.recurringModel !== "O") {
226
+ throw new TpayExecutionInputError("tpay_blik_model_o_required");
227
+ }
228
+ if (input.mode !== "subscription_cycle") {
229
+ throw new TpayExecutionInputError("tpay_blik_recurring_activation_requires_subscription_cycle");
230
+ }
231
+ if (input.saveForFutureUse !== true) {
232
+ throw new TpayExecutionInputError("tpay_blik_recurring_activation_requires_save_for_future_use");
233
+ }
234
+ }
235
+ return transient;
236
+ }
237
+ function defaultPayer(input) {
238
+ if (input.payer)
239
+ return input.payer;
240
+ if (!input.customerRef)
241
+ return null;
242
+ return { email: input.customerRef, name: "Customer" };
243
+ }
244
+ export class TpayExecutionInputError extends Error {
245
+ constructor(message) {
246
+ super(message);
247
+ this.name = "TpayExecutionInputError";
248
+ }
249
+ }
@@ -0,0 +1,5 @@
1
+ import type { ProviderReconciliationStatus } from "./donorContracts.js";
2
+ import type { PaymentProviderRecoveryProvider } from "./donorContracts.js";
3
+ import type { TpayHttpClient } from "./tpayHttpClient.js";
4
+ export declare function createTpayPaymentReconciliationProvider(client: Pick<TpayHttpClient, "getTransaction">): PaymentProviderRecoveryProvider;
5
+ export declare function normalizeTpayTransaction(transaction: Record<string, unknown>): ProviderReconciliationStatus;
@@ -0,0 +1,244 @@
1
+ import { refusalCapture, tpayFailureEvidence } from "./tpayFailureEvidence.js";
2
+ import { failureEvidencePayload } from "./paymentFailureEvidence.js";
3
+ import { parseInstant, wallClockToUtc } from "./wallClockTime.js";
4
+ import { declineCodeReading } from "./declineFailureHints.js";
5
+ import { attemptDeclineObservation, lastRefusalRow } from "./tpayDeclineEvidence.js";
6
+ export function createTpayPaymentReconciliationProvider(client) {
7
+ return {
8
+ resolvePaymentReference(input) {
9
+ // Tpay's API address is its transactionId. `title`/TR-* is merchant
10
+ // display data, not a readback identifier.
11
+ return input.providerSessionId;
12
+ },
13
+ async readRecoveryPayment(input) {
14
+ const transaction = await client.getTransaction(input.providerPaymentId);
15
+ const status = normalizeTpayTransaction(transaction);
16
+ const identityMatches = tpayIdentityMatches(transaction, input.providerPaymentId);
17
+ const configuredMoneyMatches = tpayConfiguredMoneyMatches(transaction, input.attempt);
18
+ const manualReviewRequired = tpayMoneyMoved(transaction);
19
+ let clientAction = null;
20
+ if (identityMatches && !manualReviewRequired && status.status === "pending") {
21
+ const url = validatedTpayPaymentUrl(transaction);
22
+ clientAction = url ? { kind: "redirect", url } : null;
23
+ }
24
+ return { status, identityMatches, configuredMoneyMatches, manualReviewRequired, clientAction };
25
+ },
26
+ async readPayment(input) {
27
+ const transaction = await client.getTransaction(input.providerPaymentId);
28
+ return normalizeTpayTransaction(transaction);
29
+ },
30
+ };
31
+ }
32
+ function tpayMoneyMoved(transaction) {
33
+ const status = readStatus(transaction).trim().toLowerCase();
34
+ return status === "chargeback" || status === "refunded";
35
+ }
36
+ function tpayIdentityMatches(transaction, providerPaymentId) {
37
+ const actual = readOptionalString(transaction, "transactionId") ?? readOptionalString(transaction, "id");
38
+ return actual === providerPaymentId;
39
+ }
40
+ function tpayConfiguredMoneyMatches(transaction, attempt) {
41
+ return readConfiguredAmountMinor(transaction) === attempt.amountMinor
42
+ && readCurrency(transaction)?.toUpperCase() === attempt.currency.toUpperCase();
43
+ }
44
+ const TPAY_PAYMENT_HOSTS = new Set(["secure.tpay.com", "secure.sandbox.tpay.com"]);
45
+ function validatedTpayPaymentUrl(transaction) {
46
+ const raw = transaction.transactionPaymentUrl;
47
+ if (typeof raw !== "string" || raw.length === 0)
48
+ return null;
49
+ try {
50
+ const parsed = new URL(raw);
51
+ return parsed.protocol === "https:"
52
+ && parsed.port === ""
53
+ && parsed.username === ""
54
+ && parsed.password === ""
55
+ && TPAY_PAYMENT_HOSTS.has(parsed.hostname.toLowerCase())
56
+ ? parsed.toString()
57
+ : null;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ export function normalizeTpayTransaction(transaction) {
64
+ const providerStatus = readStatus(transaction);
65
+ const statusFromField = normalizeTpayStatus(providerStatus);
66
+ // This rail leaves its status field non-terminal when the bank refuses, reporting
67
+ // the reason beside it in TWO carriers instead — per attempt, and in an errors
68
+ // array — so reading only the status field left a refused charge polling forever
69
+ // (what stranded a renewal on 2026-08-12). Attempts win when both carry a code:
70
+ // being the dated carrier, they are the only one that can say which is current.
71
+ const payments = record(transaction.payments);
72
+ const refusal = statusFromField !== "succeeded"
73
+ ? lastRefusal(payments.attempts, "paymentErrorCode") ?? lastRefusal(payments.errors, "errorCode")
74
+ : null;
75
+ const status = refusal ? "failed" : statusFromField;
76
+ return {
77
+ status,
78
+ providerStatus,
79
+ occurredAt: refusal?.occurredAt ?? readDate(transaction),
80
+ failureReason: refusal
81
+ ? `tpay_decline_${refusal.code}`
82
+ : status === "failed" ? `tpay_${providerStatus}` : null,
83
+ amountMinor: readAmountMinor(transaction),
84
+ currency: readCurrency(transaction),
85
+ rawPayload: sanitizedTpayTransaction(transaction, providerStatus, refusal),
86
+ };
87
+ }
88
+ /**
89
+ * The last row carrying a refusal code, dated and read. The scan is shared with
90
+ * the execution path, which needs the same "which code does this stand on"
91
+ * answer the instant the provider refuses; only the date and the reading are
92
+ * added. Undated rows yield no instant, so the caller uses the charge's own date.
93
+ */
94
+ function lastRefusal(rows, codeKey) {
95
+ const refusal = lastRefusalRow(rows, codeKey);
96
+ if (!refusal)
97
+ return null;
98
+ return {
99
+ code: refusal.code,
100
+ occurredAt: parseAttemptDate(refusal.row.date),
101
+ reading: declineCodeReading(refusal.code),
102
+ };
103
+ }
104
+ /**
105
+ * This rail labels no timestamp with a zone, on either carrier, and both are its
106
+ * own local wall clock. Reading them as UTC moved every instant this adapter
107
+ * produced one or two hours into the future: on 2026-08-15 a decline that landed
108
+ * 14:46Z was recorded as 16:46Z, later than the cron run that wrote it. The zone
109
+ * belongs to the rail, not to the merchant, so it is named here — beside the
110
+ * carriers it explains — and passed to the generic conversion.
111
+ */
112
+ const REPORTING_TIME_ZONE = "Europe/Warsaw";
113
+ /** Attempt timestamps arrive as `DD.MM.YYYY HH:mm`, which `Date.parse` misreads. */
114
+ function parseAttemptDate(value) {
115
+ if (typeof value !== "string")
116
+ return null;
117
+ const match = /^(\d{2})\.(\d{2})\.(\d{4})[ T](\d{2}):(\d{2})/.exec(value.trim());
118
+ if (!match)
119
+ return parseDate(value);
120
+ const [, day, month, year, hour, minute] = match;
121
+ return wallClockToUtc(`${year}-${month}-${day}T${hour}:${minute}:00`, REPORTING_TIME_ZONE);
122
+ }
123
+ function normalizeTpayStatus(status) {
124
+ const normalized = status.trim().toLowerCase();
125
+ if (["correct", "paid", "true", "success", "settled"].includes(normalized))
126
+ return "succeeded";
127
+ if ([
128
+ "failed",
129
+ "failure",
130
+ "error",
131
+ "false",
132
+ "declined",
133
+ "rejected",
134
+ "cancelled",
135
+ "canceled",
136
+ "expired",
137
+ ].includes(normalized)) {
138
+ return "failed";
139
+ }
140
+ if ([
141
+ "new",
142
+ "pending",
143
+ "created",
144
+ "processing",
145
+ "in_progress",
146
+ "authorized",
147
+ "requires_action",
148
+ "pending_confirmation",
149
+ ].includes(normalized)) {
150
+ return "pending";
151
+ }
152
+ return "unknown";
153
+ }
154
+ function readStatus(transaction) {
155
+ for (const key of ["status", "tr_status", "transactionStatus", "state"]) {
156
+ const raw = transaction[key];
157
+ if (typeof raw === "string" && raw.trim())
158
+ return raw.trim();
159
+ if (typeof raw === "boolean")
160
+ return raw ? "true" : "false";
161
+ }
162
+ return "unknown";
163
+ }
164
+ function readDate(transaction) {
165
+ const nestedDate = record(transaction.date);
166
+ for (const key of ["realization", "creation"]) {
167
+ const parsed = parseDate(nestedDate[key]);
168
+ if (parsed)
169
+ return parsed;
170
+ }
171
+ for (const key of ["date", "updatedAt", "updated_at", "createdAt", "created_at"]) {
172
+ const parsed = parseDate(transaction[key]);
173
+ if (parsed)
174
+ return parsed;
175
+ }
176
+ return null;
177
+ }
178
+ /** The transaction carrier sends `YYYY-MM-DD HH:mm:ss` — zone-less, so read in the rail's zone. */
179
+ function parseDate(value) {
180
+ return parseInstant(value, REPORTING_TIME_ZONE);
181
+ }
182
+ function readAmountMinor(transaction) {
183
+ return readAmountMinorFrom(transaction, ["amountMinor", "paidAmountMinor", "tr_amount_minor", "tr_paid_minor"], ["paidAmount", "amount", "tr_paid", "tr_amount"]);
184
+ }
185
+ function readConfiguredAmountMinor(transaction) {
186
+ return readAmountMinorFrom(transaction, ["amountMinor", "tr_amount_minor"], ["amount", "tr_amount"]);
187
+ }
188
+ function readAmountMinorFrom(transaction, minorKeys, majorKeys) {
189
+ for (const key of minorKeys) {
190
+ const raw = transaction[key];
191
+ if (typeof raw === "number" && Number.isFinite(raw))
192
+ return Math.round(raw);
193
+ if (typeof raw === "string" && /^\d+$/.test(raw.trim()))
194
+ return Number(raw);
195
+ }
196
+ for (const key of majorKeys) {
197
+ const raw = transaction[key];
198
+ const value = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw.replace(",", ".")) : NaN;
199
+ if (Number.isFinite(value))
200
+ return Math.round(value * 100);
201
+ }
202
+ return null;
203
+ }
204
+ function readCurrency(transaction) {
205
+ for (const key of ["currency", "tr_currency"]) {
206
+ const raw = transaction[key];
207
+ if (typeof raw === "string" && raw.trim().length === 3)
208
+ return raw.trim().toUpperCase();
209
+ }
210
+ return null;
211
+ }
212
+ /**
213
+ * Classification goes through the shared `classifyDecline` seam, the same one
214
+ * the synchronous path and the card rail use, so there is exactly one place a
215
+ * refusal becomes a class. Only neutral evidence crosses: this provider's code
216
+ * vocabulary is translated here and travels no further. Since PR 2724 the class
217
+ * reaches the case and becomes the payer's cause sentence; it is no cadence input.
218
+ */
219
+ function sanitizedTpayTransaction(transaction, providerStatus, refusal = null) {
220
+ return {
221
+ // Before this, the only record of why a charge failed was discarded on
222
+ // every poll.
223
+ ...(refusal ? refusalCapture(refusal, `tpay_decline_${refusal.code}`) : {}),
224
+ ...failureEvidencePayload(refusal || ["failed", "failure", "error", "false", "declined", "rejected"].includes(providerStatus.trim().toLowerCase()) ? tpayFailureEvidence({
225
+ source: "readback", providerPaymentId: readOptionalString(transaction, "transactionId"),
226
+ refusalVerified: true, flow: null,
227
+ observation: refusal ? { code: refusal.code, disposition: "present" } : attemptDeclineObservation(transaction), reading: refusal?.reading,
228
+ }) : null),
229
+ provider: "tpay",
230
+ transactionId: readOptionalString(transaction, "transactionId") ?? readOptionalString(transaction, "id"),
231
+ title: readOptionalString(transaction, "title") ?? readOptionalString(transaction, "tr_id"),
232
+ status: providerStatus,
233
+ requestIdPresent: Boolean(readOptionalString(transaction, "requestId")),
234
+ amountMinorPresent: readAmountMinor(transaction) !== null,
235
+ currency: readCurrency(transaction),
236
+ };
237
+ }
238
+ function readOptionalString(value, key) {
239
+ const raw = value[key];
240
+ return typeof raw === "string" && raw.trim() ? raw.trim() : null;
241
+ }
242
+ function record(value) {
243
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
244
+ }
@@ -0,0 +1,120 @@
1
+ export interface TpayPayerInput {
2
+ email: string;
3
+ name: string;
4
+ ip?: string | null;
5
+ userAgent?: string | null;
6
+ }
7
+ /**
8
+ * BLIK alias sent on transaction create.
9
+ *
10
+ * `UID` is merchant-scoped one-click; `PAYID` is the recurring mandate.
11
+ * `recommendedAuthLevel` is a SIBLING of `autopayment`, not a field inside it
12
+ * (Tpay SDK `Model/Objects/Transactions/Alias.php`), and Tpay documents it as
13
+ * available and required only for autopayment model `O`, where it omits the
14
+ * per-charge authorization screen.
15
+ */
16
+ export interface TpayAliasInput {
17
+ value: string;
18
+ type: "UID" | "PAYID";
19
+ label?: string;
20
+ recommendedAuthLevel?: "NOCONFREQ";
21
+ /**
22
+ * Collect immediately instead of giving the payer 72 hours to approve. Required
23
+ * on every model `O` charge; available for models `A` and `O` only.
24
+ */
25
+ noDelay?: boolean;
26
+ /**
27
+ * `frequency` is deliberately absent: Tpay requires it for model `A`, treats it
28
+ * as optional for `M`, and does not accept it for `O`. We only ever register
29
+ * variable-amount mandates, so no caller needs it.
30
+ */
31
+ autopayment?: {
32
+ model: "A" | "M" | "O";
33
+ };
34
+ }
35
+ export interface TpayTransactionCreateInput {
36
+ amount: number;
37
+ currency: "PLN";
38
+ description: string;
39
+ hiddenDescription: string;
40
+ payer: TpayPayerInput;
41
+ notificationUrl: string;
42
+ successUrl: string;
43
+ errorUrl: string;
44
+ groupId?: number;
45
+ channelId?: string;
46
+ blikPaymentType?: 2;
47
+ blikToken?: string;
48
+ /**
49
+ * Ask BLIK to reject the transaction outright when the payer's bank does not
50
+ * support recurring payments, instead of taking the money and silently never
51
+ * showing the mandate invitation. Only meaningful when registering a `PAYID`
52
+ * alias; never set it for a plain one-off BLIK payment.
53
+ */
54
+ refuseNoPayId?: boolean;
55
+ alias?: TpayAliasInput | null;
56
+ simulatorContext?: {
57
+ orderId?: string;
58
+ paymentIntentId: string;
59
+ clientId?: string;
60
+ /** Stable per provider attempt; simulator-only and never sent to Tpay. */
61
+ providerAttemptKey?: string;
62
+ /**
63
+ * In-account terminal path the simulator panel returns to after the buyer
64
+ * settles (account `returnContext` only). Absent for the public flow, which
65
+ * keeps the simulator page's own `/checkout/payment` return so the
66
+ * public path stays byte-identical (and client-side localized).
67
+ */
68
+ returnPath?: string;
69
+ };
70
+ }
71
+ /** One entry of Tpay's `payments.errors[]`. `errorCode` is low-cardinality. */
72
+ export interface TpayPaymentError {
73
+ errorCode: string;
74
+ errorMessage: string;
75
+ fieldName: string | null;
76
+ }
77
+ /**
78
+ * Create-transaction result.
79
+ *
80
+ * ⚠️ Tpay signals a synchronous BLIK rejection ONLY through `payments.errors[]`
81
+ * and the absence of `payments.payIdEligible`. Verified against production
82
+ * 2026-07-20: a rejected recurring BLIK still returns HTTP 200 with
83
+ * `result: "success"`, top-level `status: "pending"` AND `payments.status:
84
+ * "pending"`. Those three fields therefore prove nothing — never branch on them
85
+ * to decide success. `payments.method` is equally useless: it reads
86
+ * `"pay_by_link"` even for a settled BLIK PayID charge.
87
+ *
88
+ * This matters because Tpay has no async channel for failures — the transaction
89
+ * webhook's `tr_status` only ever carries `true` or `chargeback`, so a decline
90
+ * that is not read out of this response is never reported at all.
91
+ */
92
+ export interface TpayTransactionCreated {
93
+ transactionId: string;
94
+ title: string;
95
+ status: string;
96
+ transactionPaymentUrl: string | null;
97
+ requestId: string | null;
98
+ /**
99
+ * Whether BLIK considers the payer's bank capable of registering a PayID
100
+ * mandate. Present and `true` when eligible; absent entirely when Tpay
101
+ * rejected the transaction.
102
+ *
103
+ * ⚠️ UNDOCUMENTED — observed empirically, absent from Tpay's OpenAPI spec and
104
+ * from their official SDK. Treat as a hint that may vanish without notice and
105
+ * always keep `errors` as the fallback signal.
106
+ */
107
+ payIdEligible: boolean | null;
108
+ /** Synchronous provider errors. Empty when the request was accepted. */
109
+ errors: TpayPaymentError[];
110
+ }
111
+ /**
112
+ * Builds the `POST /transactions` wire payload.
113
+ *
114
+ * Every BLIK-specific field lives under `pay.blikPaymentData`; `aliases` carries
115
+ * the mandate. `refuseNoPayId` sits beside `aliases` (not inside it), while
116
+ * `recommendedAuthLevel` and `noDelay` sit inside the alias object as siblings of
117
+ * `autopayment` — matching Tpay's SDK object model and verified against
118
+ * production on 2026-07-20.
119
+ */
120
+ export declare function buildTpayTransactionPayload(input: TpayTransactionCreateInput): Record<string, unknown>;