@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,297 @@
|
|
|
1
|
+
import { failureEvidencePayload } from "./paymentFailureEvidence.js";
|
|
2
|
+
import { tpayFailureEvidence } from "./tpayFailureEvidence.js";
|
|
3
|
+
import type { PaymentExecutionPort } from "./donorContracts.js";
|
|
4
|
+
import type {
|
|
5
|
+
PaymentExecutionInput,
|
|
6
|
+
PaymentExecutionResult,
|
|
7
|
+
TpayTransientProviderInput,
|
|
8
|
+
} from "./donorContracts.js";
|
|
9
|
+
import {
|
|
10
|
+
aliasValue,
|
|
11
|
+
buildCreateTransactionInput,
|
|
12
|
+
resolveRecurringModel,
|
|
13
|
+
} from "./tpayCreateTransactionInput.js";
|
|
14
|
+
import { declineCodeReading } from "./declineFailureHints.js";
|
|
15
|
+
import {
|
|
16
|
+
assertableHints,
|
|
17
|
+
isMandateRefusal,
|
|
18
|
+
mandateCapabilityReadable,
|
|
19
|
+
readAttemptDeclineObservation,
|
|
20
|
+
declineEvidenceSource,
|
|
21
|
+
states,
|
|
22
|
+
} from "./tpayDeclineEvidence.js";
|
|
23
|
+
import type {
|
|
24
|
+
TpayHttpClient,
|
|
25
|
+
TpayPayerInput,
|
|
26
|
+
TpayTransactionCreateInput,
|
|
27
|
+
} from "./tpayHttpClient.js";
|
|
28
|
+
import {
|
|
29
|
+
tpayTrustedPreDispatchFailure,
|
|
30
|
+
tpayTrustedRefusalFailure,
|
|
31
|
+
} from "./tpayDispatchFailure.js";
|
|
32
|
+
import { ProviderAttemptPreDispatchError } from "./preparedProviderAttempt.js";
|
|
33
|
+
|
|
34
|
+
/** The one place this adapter spells its own provider identity. */
|
|
35
|
+
export const PROVIDER_KIND = "tpay" as const;
|
|
36
|
+
|
|
37
|
+
export interface TpayPaymentExecutionAdapterOptions {
|
|
38
|
+
client: TpayHttpClient;
|
|
39
|
+
notificationUrl: string;
|
|
40
|
+
successUrl: string;
|
|
41
|
+
errorUrl: string;
|
|
42
|
+
oneClickEnabled?: boolean;
|
|
43
|
+
aliasRegistrationOnCodeEnabled?: boolean;
|
|
44
|
+
/** One public switch for admitting new subscription BLIK Model O mandates. */
|
|
45
|
+
modelOActivationEnabled?: boolean;
|
|
46
|
+
resolvePayer?: (input: PaymentExecutionInput) => TpayPayerInput | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createTpayPaymentExecutionAdapter({
|
|
50
|
+
client,
|
|
51
|
+
notificationUrl,
|
|
52
|
+
successUrl,
|
|
53
|
+
errorUrl,
|
|
54
|
+
oneClickEnabled = false,
|
|
55
|
+
aliasRegistrationOnCodeEnabled = false,
|
|
56
|
+
modelOActivationEnabled = false,
|
|
57
|
+
resolvePayer = defaultPayer,
|
|
58
|
+
}: TpayPaymentExecutionAdapterOptions): PaymentExecutionPort {
|
|
59
|
+
const validateInput = (input: PaymentExecutionInput) => {
|
|
60
|
+
const transient = requireTpayTransientInput(input, {
|
|
61
|
+
oneClickEnabled,
|
|
62
|
+
modelOActivationEnabled,
|
|
63
|
+
});
|
|
64
|
+
if (input.currency !== "PLN") throw new TpayExecutionInputError("tpay_currency_not_supported");
|
|
65
|
+
const payer = resolvePayer(input);
|
|
66
|
+
if (!payer) throw new TpayExecutionInputError("tpay_payer_missing");
|
|
67
|
+
return { transient, payer };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
validateInput(input): void {
|
|
72
|
+
validateInput(input);
|
|
73
|
+
},
|
|
74
|
+
async execute(input): Promise<PaymentExecutionResult> {
|
|
75
|
+
const { transient, payer } = validateInput(input);
|
|
76
|
+
|
|
77
|
+
const request = buildCreateTransactionInput({
|
|
78
|
+
input,
|
|
79
|
+
transient,
|
|
80
|
+
payer,
|
|
81
|
+
notificationUrl,
|
|
82
|
+
successUrl,
|
|
83
|
+
errorUrl,
|
|
84
|
+
aliasRegistrationOnCodeEnabled,
|
|
85
|
+
modelOActivationEnabled,
|
|
86
|
+
});
|
|
87
|
+
let response;
|
|
88
|
+
try {
|
|
89
|
+
response = await client.createTransaction(request);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// Two independent proofs Tpay holds no transaction: never asked, or it refused the request
|
|
92
|
+
// document. Both release the attempt; everything else stays ambiguous and rethrows.
|
|
93
|
+
const noCharge = tpayTrustedPreDispatchFailure(error) ?? tpayTrustedRefusalFailure(error);
|
|
94
|
+
if (noCharge) throw new ProviderAttemptPreDispatchError(noCharge);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
const providerAttemptId = response.title || response.transactionId;
|
|
98
|
+
// This provider reports a synchronous decline ONLY here. `result`, `status`
|
|
99
|
+
// and `payments.status` all still read "success"/"pending" on a rejected
|
|
100
|
+
// transaction, so they cannot be used to detect one.
|
|
101
|
+
const rejections = response.errors ?? [];
|
|
102
|
+
const rejection = rejections[0] ?? null;
|
|
103
|
+
// The provider has carried a hard refusal code since the first minute, but
|
|
104
|
+
// only in the transaction's attempt log — never in the create response,
|
|
105
|
+
// whose codes are generic. So ask for it the moment the provider says no,
|
|
106
|
+
// instead of learning it hours later from the reconciliation poll.
|
|
107
|
+
// Fail-open: `null` means "read nothing", and every branch below then
|
|
108
|
+
// behaves exactly as it did before this read existed.
|
|
109
|
+
const observation = rejection
|
|
110
|
+
? await readAttemptDeclineObservation(client, response.transactionId)
|
|
111
|
+
: null;
|
|
112
|
+
const attemptCode = observation?.code ?? null;
|
|
113
|
+
// A code the table cannot read decides nothing. Promoting a known-unknown
|
|
114
|
+
// such as `"100"` to a verdict would silence the message fallback for the
|
|
115
|
+
// one code we openly admit we cannot read — a regression, not a fix.
|
|
116
|
+
const attemptReading = attemptCode === null ? null : declineCodeReading(attemptCode);
|
|
117
|
+
const decidingAttemptReading = attemptReading !== null && states(attemptReading)
|
|
118
|
+
? attemptReading
|
|
119
|
+
: null;
|
|
120
|
+
const mandateUnsupported = decidingAttemptReading
|
|
121
|
+
? mandateCapabilityReadable(transient.flow)
|
|
122
|
+
&& response.payIdEligible !== true
|
|
123
|
+
&& (decidingAttemptReading.hints?.includes("mandateUnsupported") ?? false)
|
|
124
|
+
: rejection !== null && rejections.some((error) =>
|
|
125
|
+
isMandateRefusal(transient.flow, response.payIdEligible, error.errorMessage)
|
|
126
|
+
);
|
|
127
|
+
// Read here, where the code vocabulary is owned, so the control plane
|
|
128
|
+
// classifies the refusal without ever seeing a provider-native code. The
|
|
129
|
+
// attempt log wins when it says something, because it is the carrier the
|
|
130
|
+
// provider itself writes the refusal into; the create response's own code
|
|
131
|
+
// is the fallback reading.
|
|
132
|
+
// The advice half is suppressed when this adapter is already asserting a
|
|
133
|
+
// mandate refusal: advice outranks hints by design in the taxonomy, and
|
|
134
|
+
// the identity code is only the FIRST error, so a generic terminal code
|
|
135
|
+
// arriving ahead of the mandate explanation would otherwise mask the one
|
|
136
|
+
// reading that says the stored reference — not the instrument — is dead.
|
|
137
|
+
const reading = decidingAttemptReading ?? declineCodeReading(rejection?.errorCode);
|
|
138
|
+
const declineEvidence = rejection ? declineEvidenceSource({
|
|
139
|
+
attemptCode, decidingAttemptReading, mandateUnsupported, reading, errorCode: rejection.errorCode,
|
|
140
|
+
}) : null;
|
|
141
|
+
// A capability code is read as mandate evidence only while registering a
|
|
142
|
+
// mandate. The same opaque code on one-time or stored-mandate charges must
|
|
143
|
+
// not claim that a mandate is dead. The rule itself lives beside the
|
|
144
|
+
// refusal evidence it qualifies, because the reconciliation rail funnels
|
|
145
|
+
// its hints through the same one — a refusal must not classify by which
|
|
146
|
+
// rail saw it.
|
|
147
|
+
const neutralReasonHints = assertableHints(reading.hints, transient.flow);
|
|
148
|
+
const providerDecline = rejection
|
|
149
|
+
? {
|
|
150
|
+
code: rejection.errorCode,
|
|
151
|
+
// Preserve the first provider code as the stable decline identity, but
|
|
152
|
+
// inspect every error for the capability signal. The provider may put a
|
|
153
|
+
// generic payment failure first and the mandate explanation later.
|
|
154
|
+
mandateUnsupported,
|
|
155
|
+
...(reading.adviceCode && !mandateUnsupported ? { adviceCode: reading.adviceCode } : {}),
|
|
156
|
+
...(neutralReasonHints?.length ? { neutralReasonHints } : {}),
|
|
157
|
+
}
|
|
158
|
+
: null;
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
provider: PROVIDER_KIND,
|
|
162
|
+
providerAttemptId,
|
|
163
|
+
providerSessionId: response.transactionId,
|
|
164
|
+
// Stays non-terminal even on a decline: the control plane owns terminal
|
|
165
|
+
// transitions and reads `providerDecline` below to make one.
|
|
166
|
+
attemptStatus: "processing",
|
|
167
|
+
providerDecline,
|
|
168
|
+
nextActionKind: !providerDecline && transient.flow === "pbl_one_time" && response.transactionPaymentUrl
|
|
169
|
+
? "redirect"
|
|
170
|
+
: null,
|
|
171
|
+
redirectUrl: providerDecline ? null : response.transactionPaymentUrl,
|
|
172
|
+
// A rejected activation must not publish a payment method reference.
|
|
173
|
+
paymentMethodRef: transient.flow === "blik_recurring_activation" && !providerDecline
|
|
174
|
+
? aliasValue(input.paymentIntentId)
|
|
175
|
+
: null,
|
|
176
|
+
// A declined transaction produces no webhook at all: Tpay's `tr_status`
|
|
177
|
+
// only ever carries `true` or `chargeback`. Claiming otherwise would
|
|
178
|
+
// strand the attempt waiting for a callback that never arrives.
|
|
179
|
+
webhookExpected: !providerDecline,
|
|
180
|
+
recoveryRequired: false,
|
|
181
|
+
requestPayload: {
|
|
182
|
+
providerIdempotencyKey: input.providerIdempotencyKey,
|
|
183
|
+
providerRequestFingerprint: input.providerRequestFingerprint,
|
|
184
|
+
providerFlow: transient.flow,
|
|
185
|
+
amountMinor: input.amountMinor,
|
|
186
|
+
currency: input.currency,
|
|
187
|
+
orderRef: input.orderRef,
|
|
188
|
+
channelId: transient.flow === "pbl_one_time" ? transient.channelId : undefined,
|
|
189
|
+
// The model actually sent, not the one the client asked for — the single
|
|
190
|
+
// activation switch can make those diverge, and this field is where a
|
|
191
|
+
// diagnosis looks for the truth.
|
|
192
|
+
recurringModel: transient.flow === "blik_recurring_activation"
|
|
193
|
+
? resolveRecurringModel(transient.recurringModel, modelOActivationEnabled)
|
|
194
|
+
: undefined,
|
|
195
|
+
methodRefPresent: transient.flow === "recurring_charge" || transient.flow === "blik_one_click"
|
|
196
|
+
? Boolean(input.paymentMethodRef)
|
|
197
|
+
: undefined,
|
|
198
|
+
methodRefAliasType: transient.flow === "recurring_charge" || transient.flow === "blik_one_click"
|
|
199
|
+
? input.paymentMethodAliasType ?? "PAYID"
|
|
200
|
+
: undefined,
|
|
201
|
+
aliasRegistrationRequested: transient.flow === "blik_one_time"
|
|
202
|
+
? aliasRegistrationOnCodeEnabled
|
|
203
|
+
: undefined,
|
|
204
|
+
},
|
|
205
|
+
responsePayload: {
|
|
206
|
+
providerStatus: response.status,
|
|
207
|
+
providerAttemptId,
|
|
208
|
+
providerSessionId: response.transactionId,
|
|
209
|
+
requestId: response.requestId,
|
|
210
|
+
redirectUrlPresent: Boolean(response.transactionPaymentUrl),
|
|
211
|
+
payIdEligible: response.payIdEligible,
|
|
212
|
+
// Codes only. `errorMessage` is free-form prose that can carry
|
|
213
|
+
// payer-identifying text, so it never reaches our stored payload.
|
|
214
|
+
providerErrorCodes: (response.errors ?? []).map((error) => error.errorCode),
|
|
215
|
+
...(declineEvidence ? { declineEvidence } : {}),
|
|
216
|
+
...failureEvidencePayload(observation ? tpayFailureEvidence({
|
|
217
|
+
source: "execution", providerPaymentId: response.transactionId, refusalVerified: true,
|
|
218
|
+
flow: transient.flow, observation, errorCode: rejection?.errorCode, reading,
|
|
219
|
+
}) : null),
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function requireTpayTransientInput(
|
|
227
|
+
input: PaymentExecutionInput,
|
|
228
|
+
options: { oneClickEnabled: boolean; modelOActivationEnabled: boolean },
|
|
229
|
+
): TpayTransientProviderInput {
|
|
230
|
+
if (input.providerFlow === "blik_one_click") {
|
|
231
|
+
if (!options.oneClickEnabled) throw new TpayExecutionInputError("tpay_blik_one_click_not_enabled");
|
|
232
|
+
if (input.mode !== "one_time") throw new TpayExecutionInputError("tpay_blik_one_click_requires_one_time");
|
|
233
|
+
if (!input.paymentMethodRef) throw new TpayExecutionInputError("tpay_blik_one_click_payid_missing");
|
|
234
|
+
if (input.transientProviderInput && input.transientProviderInput.flow !== "blik_one_click") {
|
|
235
|
+
throw new TpayExecutionInputError("tpay_provider_flow_mismatch");
|
|
236
|
+
}
|
|
237
|
+
return { provider: PROVIDER_KIND, flow: "blik_one_click" };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (input.providerFlow === "recurring_charge") {
|
|
241
|
+
if (input.mode !== "subscription_cycle") throw new TpayExecutionInputError("tpay_recurring_charge_requires_cycle");
|
|
242
|
+
if (!input.paymentMethodRef) throw new TpayExecutionInputError("tpay_recurring_charge_payid_missing");
|
|
243
|
+
if (input.paymentMethodAliasType && input.paymentMethodAliasType !== "PAYID") {
|
|
244
|
+
throw new TpayExecutionInputError("tpay_recurring_charge_requires_payid");
|
|
245
|
+
}
|
|
246
|
+
if (input.paymentMethodRecurringModel !== "O") {
|
|
247
|
+
throw new TpayExecutionInputError("tpay_recurring_charge_requires_model_o");
|
|
248
|
+
}
|
|
249
|
+
if (input.transientProviderInput && input.transientProviderInput.flow !== "recurring_charge") {
|
|
250
|
+
throw new TpayExecutionInputError("tpay_provider_flow_mismatch");
|
|
251
|
+
}
|
|
252
|
+
return { provider: PROVIDER_KIND, flow: "recurring_charge" };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const transient = input.transientProviderInput;
|
|
256
|
+
if (!transient || transient.provider !== PROVIDER_KIND) throw new TpayExecutionInputError("tpay_transient_input_missing");
|
|
257
|
+
if (input.providerFlow && input.providerFlow !== transient.flow) {
|
|
258
|
+
throw new TpayExecutionInputError("tpay_provider_flow_mismatch");
|
|
259
|
+
}
|
|
260
|
+
if (transient.flow === "blik_one_time" || transient.flow === "blik_recurring_activation") {
|
|
261
|
+
if (!transient.blikToken) throw new TpayExecutionInputError("tpay_blik_token_missing");
|
|
262
|
+
}
|
|
263
|
+
if (transient.flow === "pbl_one_time" && !transient.channelId) {
|
|
264
|
+
throw new TpayExecutionInputError("tpay_channel_id_missing");
|
|
265
|
+
}
|
|
266
|
+
if (transient.flow === "pbl_one_time" && input.mode !== "one_time") {
|
|
267
|
+
throw new TpayExecutionInputError("tpay_pbl_recurring_not_supported");
|
|
268
|
+
}
|
|
269
|
+
if (transient.flow === "blik_recurring_activation") {
|
|
270
|
+
if (!options.modelOActivationEnabled) {
|
|
271
|
+
throw new TpayExecutionInputError("tpay_blik_recurring_activation_not_enabled");
|
|
272
|
+
}
|
|
273
|
+
if (transient.recurringModel !== "O") {
|
|
274
|
+
throw new TpayExecutionInputError("tpay_blik_model_o_required");
|
|
275
|
+
}
|
|
276
|
+
if (input.mode !== "subscription_cycle") {
|
|
277
|
+
throw new TpayExecutionInputError("tpay_blik_recurring_activation_requires_subscription_cycle");
|
|
278
|
+
}
|
|
279
|
+
if (input.saveForFutureUse !== true) {
|
|
280
|
+
throw new TpayExecutionInputError("tpay_blik_recurring_activation_requires_save_for_future_use");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return transient;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function defaultPayer(input: PaymentExecutionInput): TpayPayerInput | null {
|
|
287
|
+
if (input.payer) return input.payer;
|
|
288
|
+
if (!input.customerRef) return null;
|
|
289
|
+
return { email: input.customerRef, name: "Customer" };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export class TpayExecutionInputError extends Error {
|
|
293
|
+
constructor(message: string) {
|
|
294
|
+
super(message);
|
|
295
|
+
this.name = "TpayExecutionInputError";
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProviderReconciliationStatus,
|
|
3
|
+
} from "./donorContracts.js";
|
|
4
|
+
import type {
|
|
5
|
+
PaymentProviderRecoveryProvider,
|
|
6
|
+
ProviderRecoveryAction,
|
|
7
|
+
} from "./donorContracts.js";
|
|
8
|
+
import type { TpayHttpClient } from "./tpayHttpClient.js";
|
|
9
|
+
import { refusalCapture, tpayFailureEvidence } from "./tpayFailureEvidence.js";
|
|
10
|
+
import { failureEvidencePayload } from "./paymentFailureEvidence.js";
|
|
11
|
+
import { parseInstant, wallClockToUtc } from "./wallClockTime.js";
|
|
12
|
+
import { declineCodeReading, type DeclineCodeReading } from "./declineFailureHints.js";
|
|
13
|
+
import { attemptDeclineObservation, lastRefusalRow } from "./tpayDeclineEvidence.js";
|
|
14
|
+
|
|
15
|
+
export function createTpayPaymentReconciliationProvider(
|
|
16
|
+
client: Pick<TpayHttpClient, "getTransaction">,
|
|
17
|
+
): PaymentProviderRecoveryProvider {
|
|
18
|
+
return {
|
|
19
|
+
resolvePaymentReference(input) {
|
|
20
|
+
// Tpay's API address is its transactionId. `title`/TR-* is merchant
|
|
21
|
+
// display data, not a readback identifier.
|
|
22
|
+
return input.providerSessionId;
|
|
23
|
+
},
|
|
24
|
+
async readRecoveryPayment(input) {
|
|
25
|
+
const transaction = await client.getTransaction(input.providerPaymentId);
|
|
26
|
+
const status = normalizeTpayTransaction(transaction);
|
|
27
|
+
const identityMatches = tpayIdentityMatches(transaction, input.providerPaymentId);
|
|
28
|
+
const configuredMoneyMatches = tpayConfiguredMoneyMatches(transaction, input.attempt);
|
|
29
|
+
const manualReviewRequired = tpayMoneyMoved(transaction);
|
|
30
|
+
let clientAction: ProviderRecoveryAction | null = null;
|
|
31
|
+
if (identityMatches && !manualReviewRequired && status.status === "pending") {
|
|
32
|
+
const url = validatedTpayPaymentUrl(transaction);
|
|
33
|
+
clientAction = url ? { kind: "redirect", url } : null;
|
|
34
|
+
}
|
|
35
|
+
return { status, identityMatches, configuredMoneyMatches, manualReviewRequired, clientAction };
|
|
36
|
+
},
|
|
37
|
+
async readPayment(input) {
|
|
38
|
+
const transaction = await client.getTransaction(input.providerPaymentId);
|
|
39
|
+
return normalizeTpayTransaction(transaction);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function tpayMoneyMoved(transaction: Record<string, unknown>): boolean {
|
|
45
|
+
const status = readStatus(transaction).trim().toLowerCase();
|
|
46
|
+
return status === "chargeback" || status === "refunded";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function tpayIdentityMatches(transaction: Record<string, unknown>, providerPaymentId: string): boolean {
|
|
50
|
+
const actual = readOptionalString(transaction, "transactionId") ?? readOptionalString(transaction, "id");
|
|
51
|
+
return actual === providerPaymentId;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function tpayConfiguredMoneyMatches(
|
|
55
|
+
transaction: Record<string, unknown>,
|
|
56
|
+
attempt: { amountMinor: number; currency: string },
|
|
57
|
+
): boolean {
|
|
58
|
+
return readConfiguredAmountMinor(transaction) === attempt.amountMinor
|
|
59
|
+
&& readCurrency(transaction)?.toUpperCase() === attempt.currency.toUpperCase();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const TPAY_PAYMENT_HOSTS = new Set(["secure.tpay.com", "secure.sandbox.tpay.com"]);
|
|
63
|
+
|
|
64
|
+
function validatedTpayPaymentUrl(transaction: Record<string, unknown>): string | null {
|
|
65
|
+
const raw = transaction.transactionPaymentUrl;
|
|
66
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
67
|
+
try {
|
|
68
|
+
const parsed = new URL(raw);
|
|
69
|
+
return parsed.protocol === "https:"
|
|
70
|
+
&& parsed.port === ""
|
|
71
|
+
&& parsed.username === ""
|
|
72
|
+
&& parsed.password === ""
|
|
73
|
+
&& TPAY_PAYMENT_HOSTS.has(parsed.hostname.toLowerCase())
|
|
74
|
+
? parsed.toString()
|
|
75
|
+
: null;
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function normalizeTpayTransaction(
|
|
82
|
+
transaction: Record<string, unknown>,
|
|
83
|
+
): ProviderReconciliationStatus {
|
|
84
|
+
const providerStatus = readStatus(transaction);
|
|
85
|
+
const statusFromField = normalizeTpayStatus(providerStatus);
|
|
86
|
+
// This rail leaves its status field non-terminal when the bank refuses, reporting
|
|
87
|
+
// the reason beside it in TWO carriers instead — per attempt, and in an errors
|
|
88
|
+
// array — so reading only the status field left a refused charge polling forever
|
|
89
|
+
// (what stranded a renewal on 2026-08-12). Attempts win when both carry a code:
|
|
90
|
+
// being the dated carrier, they are the only one that can say which is current.
|
|
91
|
+
const payments = record(transaction.payments);
|
|
92
|
+
const refusal = statusFromField !== "succeeded"
|
|
93
|
+
? lastRefusal(payments.attempts, "paymentErrorCode") ?? lastRefusal(payments.errors, "errorCode")
|
|
94
|
+
: null;
|
|
95
|
+
const status = refusal ? "failed" as const : statusFromField;
|
|
96
|
+
return {
|
|
97
|
+
status,
|
|
98
|
+
providerStatus,
|
|
99
|
+
occurredAt: refusal?.occurredAt ?? readDate(transaction),
|
|
100
|
+
failureReason: refusal
|
|
101
|
+
? `tpay_decline_${refusal.code}`
|
|
102
|
+
: status === "failed" ? `tpay_${providerStatus}` : null,
|
|
103
|
+
amountMinor: readAmountMinor(transaction),
|
|
104
|
+
currency: readCurrency(transaction),
|
|
105
|
+
rawPayload: sanitizedTpayTransaction(transaction, providerStatus, refusal),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface AttemptRefusal {
|
|
110
|
+
code: string;
|
|
111
|
+
occurredAt: string | null;
|
|
112
|
+
reading: DeclineCodeReading;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The last row carrying a refusal code, dated and read. The scan is shared with
|
|
117
|
+
* the execution path, which needs the same "which code does this stand on"
|
|
118
|
+
* answer the instant the provider refuses; only the date and the reading are
|
|
119
|
+
* added. Undated rows yield no instant, so the caller uses the charge's own date.
|
|
120
|
+
*/
|
|
121
|
+
function lastRefusal(rows: unknown, codeKey: string): AttemptRefusal | null {
|
|
122
|
+
const refusal = lastRefusalRow(rows, codeKey);
|
|
123
|
+
if (!refusal) return null;
|
|
124
|
+
return {
|
|
125
|
+
code: refusal.code,
|
|
126
|
+
occurredAt: parseAttemptDate(refusal.row.date),
|
|
127
|
+
reading: declineCodeReading(refusal.code),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* This rail labels no timestamp with a zone, on either carrier, and both are its
|
|
133
|
+
* own local wall clock. Reading them as UTC moved every instant this adapter
|
|
134
|
+
* produced one or two hours into the future: on 2026-08-15 a decline that landed
|
|
135
|
+
* 14:46Z was recorded as 16:46Z, later than the cron run that wrote it. The zone
|
|
136
|
+
* belongs to the rail, not to the merchant, so it is named here — beside the
|
|
137
|
+
* carriers it explains — and passed to the generic conversion.
|
|
138
|
+
*/
|
|
139
|
+
const REPORTING_TIME_ZONE = "Europe/Warsaw";
|
|
140
|
+
|
|
141
|
+
/** Attempt timestamps arrive as `DD.MM.YYYY HH:mm`, which `Date.parse` misreads. */
|
|
142
|
+
function parseAttemptDate(value: unknown): string | null {
|
|
143
|
+
if (typeof value !== "string") return null;
|
|
144
|
+
const match = /^(\d{2})\.(\d{2})\.(\d{4})[ T](\d{2}):(\d{2})/.exec(value.trim());
|
|
145
|
+
if (!match) return parseDate(value);
|
|
146
|
+
const [, day, month, year, hour, minute] = match;
|
|
147
|
+
return wallClockToUtc(`${year}-${month}-${day}T${hour}:${minute}:00`, REPORTING_TIME_ZONE);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeTpayStatus(status: string): ProviderReconciliationStatus["status"] {
|
|
151
|
+
const normalized = status.trim().toLowerCase();
|
|
152
|
+
if (["correct", "paid", "true", "success", "settled"].includes(normalized)) return "succeeded";
|
|
153
|
+
if ([
|
|
154
|
+
"failed",
|
|
155
|
+
"failure",
|
|
156
|
+
"error",
|
|
157
|
+
"false",
|
|
158
|
+
"declined",
|
|
159
|
+
"rejected",
|
|
160
|
+
"cancelled",
|
|
161
|
+
"canceled",
|
|
162
|
+
"expired",
|
|
163
|
+
].includes(normalized)) {
|
|
164
|
+
return "failed";
|
|
165
|
+
}
|
|
166
|
+
if ([
|
|
167
|
+
"new",
|
|
168
|
+
"pending",
|
|
169
|
+
"created",
|
|
170
|
+
"processing",
|
|
171
|
+
"in_progress",
|
|
172
|
+
"authorized",
|
|
173
|
+
"requires_action",
|
|
174
|
+
"pending_confirmation",
|
|
175
|
+
].includes(normalized)) {
|
|
176
|
+
return "pending";
|
|
177
|
+
}
|
|
178
|
+
return "unknown";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readStatus(transaction: Record<string, unknown>): string {
|
|
182
|
+
for (const key of ["status", "tr_status", "transactionStatus", "state"]) {
|
|
183
|
+
const raw = transaction[key];
|
|
184
|
+
if (typeof raw === "string" && raw.trim()) return raw.trim();
|
|
185
|
+
if (typeof raw === "boolean") return raw ? "true" : "false";
|
|
186
|
+
}
|
|
187
|
+
return "unknown";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function readDate(transaction: Record<string, unknown>): string | null {
|
|
191
|
+
const nestedDate = record(transaction.date);
|
|
192
|
+
for (const key of ["realization", "creation"]) {
|
|
193
|
+
const parsed = parseDate(nestedDate[key]);
|
|
194
|
+
if (parsed) return parsed;
|
|
195
|
+
}
|
|
196
|
+
for (const key of ["date", "updatedAt", "updated_at", "createdAt", "created_at"]) {
|
|
197
|
+
const parsed = parseDate(transaction[key]);
|
|
198
|
+
if (parsed) return parsed;
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The transaction carrier sends `YYYY-MM-DD HH:mm:ss` — zone-less, so read in the rail's zone. */
|
|
204
|
+
function parseDate(value: unknown): string | null {
|
|
205
|
+
return parseInstant(value, REPORTING_TIME_ZONE);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function readAmountMinor(transaction: Record<string, unknown>): number | null {
|
|
209
|
+
return readAmountMinorFrom(
|
|
210
|
+
transaction,
|
|
211
|
+
["amountMinor", "paidAmountMinor", "tr_amount_minor", "tr_paid_minor"],
|
|
212
|
+
["paidAmount", "amount", "tr_paid", "tr_amount"],
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function readConfiguredAmountMinor(transaction: Record<string, unknown>): number | null {
|
|
217
|
+
return readAmountMinorFrom(transaction, ["amountMinor", "tr_amount_minor"], ["amount", "tr_amount"]);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readAmountMinorFrom(
|
|
221
|
+
transaction: Record<string, unknown>,
|
|
222
|
+
minorKeys: readonly string[],
|
|
223
|
+
majorKeys: readonly string[],
|
|
224
|
+
): number | null {
|
|
225
|
+
for (const key of minorKeys) {
|
|
226
|
+
const raw = transaction[key];
|
|
227
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return Math.round(raw);
|
|
228
|
+
if (typeof raw === "string" && /^\d+$/.test(raw.trim())) return Number(raw);
|
|
229
|
+
}
|
|
230
|
+
for (const key of majorKeys) {
|
|
231
|
+
const raw = transaction[key];
|
|
232
|
+
const value = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw.replace(",", ".")) : NaN;
|
|
233
|
+
if (Number.isFinite(value)) return Math.round(value * 100);
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function readCurrency(transaction: Record<string, unknown>): string | null {
|
|
239
|
+
for (const key of ["currency", "tr_currency"]) {
|
|
240
|
+
const raw = transaction[key];
|
|
241
|
+
if (typeof raw === "string" && raw.trim().length === 3) return raw.trim().toUpperCase();
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Classification goes through the shared `classifyDecline` seam, the same one
|
|
248
|
+
* the synchronous path and the card rail use, so there is exactly one place a
|
|
249
|
+
* refusal becomes a class. Only neutral evidence crosses: this provider's code
|
|
250
|
+
* vocabulary is translated here and travels no further. Since PR 2724 the class
|
|
251
|
+
* reaches the case and becomes the payer's cause sentence; it is no cadence input.
|
|
252
|
+
*/
|
|
253
|
+
|
|
254
|
+
function sanitizedTpayTransaction(
|
|
255
|
+
transaction: Record<string, unknown>,
|
|
256
|
+
providerStatus: string,
|
|
257
|
+
refusal: AttemptRefusal | null = null,
|
|
258
|
+
): Record<string, unknown> {
|
|
259
|
+
return {
|
|
260
|
+
// Before this, the only record of why a charge failed was discarded on
|
|
261
|
+
// every poll.
|
|
262
|
+
...(refusal ? refusalCapture(refusal, `tpay_decline_${refusal.code}`) : {}),
|
|
263
|
+
...failureEvidencePayload(refusal || ["failed", "failure", "error", "false", "declined", "rejected"].includes(providerStatus.trim().toLowerCase()) ? tpayFailureEvidence({
|
|
264
|
+
source: "readback", providerPaymentId: readOptionalString(transaction, "transactionId"),
|
|
265
|
+
refusalVerified: true, flow: null,
|
|
266
|
+
observation: refusal ? { code: refusal.code, disposition: "present" } : attemptDeclineObservation(transaction), reading: refusal?.reading,
|
|
267
|
+
}) : null),
|
|
268
|
+
provider: "tpay",
|
|
269
|
+
transactionId: readOptionalString(transaction, "transactionId") ?? readOptionalString(transaction, "id"),
|
|
270
|
+
title: readOptionalString(transaction, "title") ?? readOptionalString(transaction, "tr_id"),
|
|
271
|
+
status: providerStatus,
|
|
272
|
+
requestIdPresent: Boolean(readOptionalString(transaction, "requestId")),
|
|
273
|
+
amountMinorPresent: readAmountMinor(transaction) !== null,
|
|
274
|
+
currency: readCurrency(transaction),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function readOptionalString(value: Record<string, unknown>, key: string): string | null {
|
|
279
|
+
const raw = value[key];
|
|
280
|
+
return typeof raw === "string" && raw.trim() ? raw.trim() : null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function record(value: unknown): Record<string, unknown> {
|
|
284
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
285
|
+
}
|