@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.
- package/CHANGELOG.md +161 -231
- package/README.md +182 -8
- package/dist/composition/composeBundle.d.ts +6 -0
- package/dist/composition/composeBundle.js +2 -0
- package/dist/composition/index.js +1 -0
- package/dist/payments/adapterConformance.d.ts +8 -0
- package/dist/payments/adapterConformance.js +109 -0
- package/dist/payments/bindings.d.ts +3 -0
- package/dist/payments/bindings.js +17 -0
- package/dist/payments/checkoutActivePaymentActionResolver.d.ts +7 -0
- package/dist/payments/checkoutActivePaymentActionResolver.js +77 -0
- package/dist/payments/checkoutRecoveryPaymentResolver.d.ts +8 -0
- package/dist/payments/checkoutRecoveryPaymentResolver.js +87 -0
- package/dist/payments/contracts/adapter.d.ts +113 -0
- package/dist/payments/contracts/adapter.js +1 -0
- package/dist/payments/contracts/conformance.d.ts +155 -0
- package/dist/payments/contracts/conformance.js +1 -0
- package/dist/payments/contracts/evidence.d.ts +157 -0
- package/dist/payments/contracts/evidence.js +1 -0
- package/dist/payments/contracts/index.d.ts +10 -0
- package/dist/payments/contracts/index.js +1 -0
- package/dist/payments/contracts/preparedAttempt.d.ts +69 -0
- package/dist/payments/contracts/preparedAttempt.js +1 -0
- package/dist/payments/contracts/reconciliation.d.ts +303 -0
- package/dist/payments/contracts/reconciliation.js +1 -0
- package/dist/payments/contracts/repair.d.ts +92 -0
- package/dist/payments/contracts/repair.js +1 -0
- package/dist/payments/contracts/truth.d.ts +92 -0
- package/dist/payments/contracts/truth.js +1 -0
- package/dist/payments/eventConformanceRunner.d.ts +12 -0
- package/dist/payments/eventConformanceRunner.js +125 -0
- package/dist/payments/index.d.ts +16 -0
- package/dist/payments/index.js +16 -0
- package/dist/payments/paymentAttemptSilenceWindow.d.ts +10 -0
- package/dist/payments/paymentAttemptSilenceWindow.js +19 -0
- package/dist/payments/paymentMethodLifecycle.d.ts +11 -0
- package/dist/payments/paymentMethodLifecycle.js +37 -0
- package/dist/payments/paymentProviderReconciliationAbsenceReopen.d.ts +7 -0
- package/dist/payments/paymentProviderReconciliationAbsenceReopen.js +100 -0
- package/dist/payments/paymentProviderReconciliationEvidence.d.ts +36 -0
- package/dist/payments/paymentProviderReconciliationEvidence.js +91 -0
- package/dist/payments/paymentProviderReconciliationWorker.d.ts +4 -0
- package/dist/payments/paymentProviderReconciliationWorker.js +245 -0
- package/dist/payments/paymentTruth.d.ts +8 -0
- package/dist/payments/paymentTruth.js +125 -0
- package/dist/payments/paymentVerifyNowHandler.d.ts +27 -0
- package/dist/payments/paymentVerifyNowHandler.js +70 -0
- package/dist/payments/paymentVerifyNowService.d.ts +23 -0
- package/dist/payments/paymentVerifyNowService.js +93 -0
- package/dist/payments/paymentWebhookPostProcessing.d.ts +22 -0
- package/dist/payments/paymentWebhookPostProcessing.js +15 -0
- package/dist/payments/postgresPaymentTruth.d.ts +10 -0
- package/dist/payments/postgresPaymentTruth.js +126 -0
- package/dist/payments/preparedAttempt.d.ts +6 -0
- package/dist/payments/preparedAttempt.js +70 -0
- package/dist/payments/preparedAttemptFinalization.d.ts +5 -0
- package/dist/payments/preparedAttemptFinalization.js +12 -0
- package/dist/payments/schemaCapabilities.d.ts +10 -0
- package/dist/payments/schemaCapabilities.js +10 -0
- package/dist/payments/sql.d.ts +20 -0
- package/dist/payments/sql.js +6 -0
- package/dist/payments/stripe/classifyDecline.d.ts +46 -0
- package/dist/payments/stripe/classifyDecline.js +58 -0
- package/dist/payments/stripe/declineFailureHints.d.ts +46 -0
- package/dist/payments/stripe/declineFailureHints.js +87 -0
- package/dist/payments/stripe/donorContracts.d.ts +120 -0
- package/dist/payments/stripe/donorContracts.js +1 -0
- package/dist/payments/stripe/executionDecline.d.ts +53 -0
- package/dist/payments/stripe/executionDecline.js +107 -0
- package/dist/payments/stripe/index.d.ts +16 -0
- package/dist/payments/stripe/index.js +239 -0
- package/dist/payments/stripe/intentMapper.d.ts +26 -0
- package/dist/payments/stripe/intentMapper.js +51 -0
- package/dist/payments/stripe/methodFacts.d.ts +10 -0
- package/dist/payments/stripe/methodFacts.js +12 -0
- package/dist/payments/stripe/paymentFailureEvidence.d.ts +26 -0
- package/dist/payments/stripe/paymentFailureEvidence.js +33 -0
- package/dist/payments/stripe/stripeFailureEvidence.d.ts +41 -0
- package/dist/payments/stripe/stripeFailureEvidence.js +82 -0
- package/dist/payments/stripe/stripeIntentConfirmationEvidence.d.ts +28 -0
- package/dist/payments/stripe/stripeIntentConfirmationEvidence.js +32 -0
- package/dist/payments/stripe/stripePaymentReconciliationProvider.d.ts +35 -0
- package/dist/payments/stripe/stripePaymentReconciliationProvider.js +222 -0
- package/dist/payments/stripe/stripeRefundEvidence.d.ts +8 -0
- package/dist/payments/stripe/stripeRefundEvidence.js +17 -0
- package/dist/payments/stripe/stripeSandboxPaymentExecutionAdapter.d.ts +45 -0
- package/dist/payments/stripe/stripeSandboxPaymentExecutionAdapter.js +220 -0
- package/dist/payments/stripe/stripeWebhookNormalizer.d.ts +14 -0
- package/dist/payments/stripe/stripeWebhookNormalizer.js +243 -0
- package/dist/payments/stripe/webhookSignature.d.ts +14 -0
- package/dist/payments/stripe/webhookSignature.js +43 -0
- package/dist/payments/tpay/blikRecurringMapper.d.ts +37 -0
- package/dist/payments/tpay/blikRecurringMapper.js +24 -0
- package/dist/payments/tpay/capabilities.d.ts +3 -0
- package/dist/payments/tpay/capabilities.js +18 -0
- package/dist/payments/tpay/classifyDecline.d.ts +46 -0
- package/dist/payments/tpay/classifyDecline.js +58 -0
- package/dist/payments/tpay/declineFailureHints.d.ts +72 -0
- package/dist/payments/tpay/declineFailureHints.js +37 -0
- package/dist/payments/tpay/donorContracts.d.ts +140 -0
- package/dist/payments/tpay/donorContracts.js +1 -0
- package/dist/payments/tpay/index.d.ts +43 -0
- package/dist/payments/tpay/index.js +264 -0
- package/dist/payments/tpay/paymentFailureEvidence.d.ts +26 -0
- package/dist/payments/tpay/paymentFailureEvidence.js +33 -0
- package/dist/payments/tpay/preparedProviderAttempt.d.ts +22 -0
- package/dist/payments/tpay/preparedProviderAttempt.js +24 -0
- package/dist/payments/tpay/providerAttemptFailureDiagnostic.d.ts +3 -0
- package/dist/payments/tpay/providerAttemptFailureDiagnostic.js +95 -0
- package/dist/payments/tpay/tpayCreateTransactionInput.d.ts +25 -0
- package/dist/payments/tpay/tpayCreateTransactionInput.js +128 -0
- package/dist/payments/tpay/tpayDeclineEvidence.d.ts +126 -0
- package/dist/payments/tpay/tpayDeclineEvidence.js +177 -0
- package/dist/payments/tpay/tpayDispatchFailure.d.ts +60 -0
- package/dist/payments/tpay/tpayDispatchFailure.js +121 -0
- package/dist/payments/tpay/tpayFailureEvidence.d.ts +20 -0
- package/dist/payments/tpay/tpayFailureEvidence.js +61 -0
- package/dist/payments/tpay/tpayHttpClient.d.ts +52 -0
- package/dist/payments/tpay/tpayHttpClient.js +185 -0
- package/dist/payments/tpay/tpayHttpFailure.d.ts +57 -0
- package/dist/payments/tpay/tpayHttpFailure.js +143 -0
- package/dist/payments/tpay/tpayPaymentExecutionAdapter.d.ts +20 -0
- package/dist/payments/tpay/tpayPaymentExecutionAdapter.js +249 -0
- package/dist/payments/tpay/tpayPaymentReconciliationProvider.d.ts +5 -0
- package/dist/payments/tpay/tpayPaymentReconciliationProvider.js +244 -0
- package/dist/payments/tpay/tpayTransactionPayload.d.ts +120 -0
- package/dist/payments/tpay/tpayTransactionPayload.js +96 -0
- package/dist/payments/tpay/wallClockTime.d.ts +31 -0
- package/dist/payments/tpay/wallClockTime.js +57 -0
- package/dist/payments/tpay/webhookJwsVerifier.d.ts +17 -0
- package/dist/payments/tpay/webhookJwsVerifier.js +51 -0
- package/dist/payments/tpay/webhookNormalizer.d.ts +54 -0
- package/dist/payments/tpay/webhookNormalizer.js +200 -0
- package/dist/payments/verifiedEvidenceProjection.d.ts +12 -0
- package/dist/payments/verifiedEvidenceProjection.js +166 -0
- package/dist/payments/verifiedIngress.d.ts +119 -0
- package/dist/payments/verifiedIngress.js +220 -0
- package/dist/shipping/bindings.d.ts +5 -0
- package/dist/shipping/bindings.js +12 -2
- package/dist/shipping/index.d.ts +14 -0
- package/dist/shipping/index.js +5 -0
- package/package.json +22 -2
- package/release-gates.json +64 -3
- package/sql/20260810180000_payment_settlement_rail.sql +574 -0
- package/sql/20260815150000_payment_truth_reconciliation.sql +479 -0
- package/sql/20260826100000_late_payment_shifts_the_subscription_cycle.sql +215 -0
- package/sql/20261021200000_shipping_flat_rate_seed.sql +1 -0
- package/src/composition/composeBundle.ts +9 -0
- package/src/composition/index.ts +1 -0
- package/src/payments/adapterConformance.ts +118 -0
- package/src/payments/bindings.ts +17 -0
- package/src/payments/checkoutActivePaymentActionResolver.ts +97 -0
- package/src/payments/checkoutRecoveryPaymentResolver.ts +108 -0
- package/src/payments/contracts/adapter.ts +96 -0
- package/src/payments/contracts/conformance.ts +171 -0
- package/src/payments/contracts/evidence.ts +142 -0
- package/src/payments/contracts/index.ts +54 -0
- package/src/payments/contracts/preparedAttempt.ts +86 -0
- package/src/payments/contracts/reconciliation.ts +296 -0
- package/src/payments/contracts/repair.ts +95 -0
- package/src/payments/contracts/truth.ts +108 -0
- package/src/payments/eventConformanceRunner.ts +149 -0
- package/src/payments/index.ts +21 -0
- package/src/payments/paymentAttemptSilenceWindow.ts +26 -0
- package/src/payments/paymentMethodLifecycle.ts +50 -0
- package/src/payments/paymentProviderReconciliationAbsenceReopen.ts +130 -0
- package/src/payments/paymentProviderReconciliationEvidence.ts +117 -0
- package/src/payments/paymentProviderReconciliationWorker.ts +291 -0
- package/src/payments/paymentTruth.ts +131 -0
- package/src/payments/paymentVerifyNowHandler.ts +94 -0
- package/src/payments/paymentVerifyNowService.ts +125 -0
- package/src/payments/paymentWebhookPostProcessing.ts +36 -0
- package/src/payments/postgresPaymentTruth.ts +149 -0
- package/src/payments/preparedAttempt.ts +90 -0
- package/src/payments/preparedAttemptFinalization.ts +13 -0
- package/src/payments/schemaCapabilities.ts +10 -0
- package/src/payments/sql.ts +6 -0
- package/src/payments/stripe/classifyDecline.ts +62 -0
- package/src/payments/stripe/declineFailureHints.ts +89 -0
- package/src/payments/stripe/donorContracts.ts +87 -0
- package/src/payments/stripe/executionDecline.ts +126 -0
- package/src/payments/stripe/index.ts +235 -0
- package/src/payments/stripe/intentMapper.ts +58 -0
- package/src/payments/stripe/methodFacts.ts +21 -0
- package/src/payments/stripe/paymentFailureEvidence.ts +58 -0
- package/src/payments/stripe/stripeFailureEvidence.ts +115 -0
- package/src/payments/stripe/stripeIntentConfirmationEvidence.ts +34 -0
- package/src/payments/stripe/stripePaymentReconciliationProvider.ts +263 -0
- package/src/payments/stripe/stripeRefundEvidence.ts +19 -0
- package/src/payments/stripe/stripeSandboxPaymentExecutionAdapter.ts +283 -0
- package/src/payments/stripe/stripeWebhookNormalizer.ts +277 -0
- package/src/payments/stripe/webhookSignature.ts +55 -0
- package/src/payments/tpay/blikRecurringMapper.ts +42 -0
- package/src/payments/tpay/capabilities.ts +21 -0
- package/src/payments/tpay/classifyDecline.ts +62 -0
- package/src/payments/tpay/declineFailureHints.ts +90 -0
- package/src/payments/tpay/donorContracts.ts +104 -0
- package/src/payments/tpay/index.ts +257 -0
- package/src/payments/tpay/paymentFailureEvidence.ts +58 -0
- package/src/payments/tpay/preparedProviderAttempt.ts +38 -0
- package/src/payments/tpay/providerAttemptFailureDiagnostic.ts +99 -0
- package/src/payments/tpay/tpayCreateTransactionInput.ts +158 -0
- package/src/payments/tpay/tpayDeclineEvidence.ts +233 -0
- package/src/payments/tpay/tpayDispatchFailure.ts +186 -0
- package/src/payments/tpay/tpayFailureEvidence.ts +75 -0
- package/src/payments/tpay/tpayHttpClient.ts +290 -0
- package/src/payments/tpay/tpayHttpFailure.ts +186 -0
- package/src/payments/tpay/tpayPaymentExecutionAdapter.ts +297 -0
- package/src/payments/tpay/tpayPaymentReconciliationProvider.ts +285 -0
- package/src/payments/tpay/tpayTransactionPayload.ts +207 -0
- package/src/payments/tpay/wallClockTime.ts +57 -0
- package/src/payments/tpay/webhookJwsVerifier.ts +82 -0
- package/src/payments/tpay/webhookNormalizer.ts +279 -0
- package/src/payments/verifiedEvidenceProjection.ts +179 -0
- package/src/payments/verifiedIngress.ts +317 -0
- package/src/shipping/bindings.ts +18 -2
- 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>;
|