@openlup/platform 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -231
- package/README.md +172 -6
- 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/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/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
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { buildTpayTransactionPayload, } from "./tpayTransactionPayload.js";
|
|
2
|
+
import { tpayTransactionDispatchBoundary } from "./tpayDispatchFailure.js";
|
|
3
|
+
import { readTpayHttpFailure, TpayHttpError } from "./tpayHttpFailure.js";
|
|
4
|
+
export { TpayHttpError } from "./tpayHttpFailure.js";
|
|
5
|
+
const TOKEN_REFRESH_SKEW_MS = 60_000;
|
|
6
|
+
const DEFAULT_TPAY_TIMEOUT_MS = 10_000;
|
|
7
|
+
export function createTpayHttpClient({ baseUrl, clientId, clientSecret, timeoutMs = DEFAULT_TPAY_TIMEOUT_MS, fetchImpl = globalThis.fetch, now = () => Date.now(), }) {
|
|
8
|
+
let token = null;
|
|
9
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
10
|
+
function requestDeadline() {
|
|
11
|
+
return { expiresAtMs: Date.now() + timeoutMs };
|
|
12
|
+
}
|
|
13
|
+
async function bearer(deadline) {
|
|
14
|
+
if (token && token.expiresAtMs - TOKEN_REFRESH_SKEW_MS > now()) {
|
|
15
|
+
return token.accessToken;
|
|
16
|
+
}
|
|
17
|
+
const body = new URLSearchParams({ client_id: clientId, client_secret: clientSecret });
|
|
18
|
+
const response = await fetchWithTimeout(fetchImpl, `${normalizedBaseUrl}/oauth/auth`, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
21
|
+
body: body.toString(),
|
|
22
|
+
}, remainingTimeoutMs(deadline, "tpay_oauth_timeout"), "tpay_oauth_timeout");
|
|
23
|
+
const json = await readJson(response, "tpay_oauth_failed");
|
|
24
|
+
const accessToken = readString(json, "access_token");
|
|
25
|
+
const expiresIn = readNumber(json, "expires_in");
|
|
26
|
+
token = { accessToken, expiresAtMs: now() + expiresIn * 1000 };
|
|
27
|
+
return accessToken;
|
|
28
|
+
}
|
|
29
|
+
async function request(path, init = {}, deadline = requestDeadline()) {
|
|
30
|
+
const headers = { Authorization: `Bearer ${await bearer(deadline)}` };
|
|
31
|
+
let body;
|
|
32
|
+
if (init.body !== undefined) {
|
|
33
|
+
headers["Content-Type"] = "application/json";
|
|
34
|
+
body = JSON.stringify(init.body);
|
|
35
|
+
}
|
|
36
|
+
const response = await fetchWithTimeout(fetchImpl, `${normalizedBaseUrl}${path}`, {
|
|
37
|
+
method: init.method ?? "GET",
|
|
38
|
+
headers,
|
|
39
|
+
body,
|
|
40
|
+
}, remainingTimeoutMs(deadline, "tpay_request_timeout"), "tpay_request_timeout");
|
|
41
|
+
return readJson(response, "tpay_request_failed");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
async createTransaction(input) {
|
|
45
|
+
const deadline = requestDeadline();
|
|
46
|
+
return tpayTransactionDispatchBoundary({
|
|
47
|
+
authenticate: () => bearer(deadline),
|
|
48
|
+
prepare: (accessToken) => ({
|
|
49
|
+
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
50
|
+
body: JSON.stringify(buildTpayTransactionPayload(input)),
|
|
51
|
+
timeoutMs: remainingTimeoutMs(deadline, "tpay_request_deadline_exhausted"),
|
|
52
|
+
}),
|
|
53
|
+
dispatch: ({ headers, body, timeoutMs }) => fetchWithTimeout(fetchImpl, `${normalizedBaseUrl}/transactions`, { method: "POST", headers, body }, timeoutMs, "tpay_request_timeout"),
|
|
54
|
+
decode: async (response) => {
|
|
55
|
+
const json = await readJson(response, "tpay_request_failed");
|
|
56
|
+
const payments = asRecord(json.payments);
|
|
57
|
+
return {
|
|
58
|
+
transactionId: readString(json, "transactionId"),
|
|
59
|
+
title: readString(json, "title"),
|
|
60
|
+
status: readString(json, "status"),
|
|
61
|
+
transactionPaymentUrl: readNullableString(json, "transactionPaymentUrl"),
|
|
62
|
+
requestId: readNullableString(json, "requestId"),
|
|
63
|
+
payIdEligible: typeof payments.payIdEligible === "boolean" ? payments.payIdEligible : null,
|
|
64
|
+
errors: readPaymentErrors(payments.errors),
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
async getTransaction(transactionId) {
|
|
70
|
+
const json = await request(`/transactions/${encodeURIComponent(transactionId)}`, {}, requestDeadline());
|
|
71
|
+
return {
|
|
72
|
+
// Keep provider fields that recovery normalizes later (notably nested
|
|
73
|
+
// dates and paidAmount). Explicit canonical fields below remain the
|
|
74
|
+
// typed client contract.
|
|
75
|
+
...json,
|
|
76
|
+
transactionId: readString(json, "transactionId"),
|
|
77
|
+
title: readNullableString(json, "title"),
|
|
78
|
+
status: readString(json, "status"),
|
|
79
|
+
transactionPaymentUrl: readNullableString(json, "transactionPaymentUrl"),
|
|
80
|
+
amount: readNullableTransactionAmount(json, "amount"),
|
|
81
|
+
currency: readNullableString(json, "currency"),
|
|
82
|
+
requestId: readNullableString(json, "requestId"),
|
|
83
|
+
payments: asRecord(json.payments),
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
async listPaymentChannels() {
|
|
87
|
+
const json = await request("/transactions/channels", {}, requestDeadline());
|
|
88
|
+
const channels = Array.isArray(json.channels)
|
|
89
|
+
? json.channels
|
|
90
|
+
: [];
|
|
91
|
+
return channels.map(mapChannel);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function remainingTimeoutMs(deadline, timeoutCode) {
|
|
96
|
+
const remaining = deadline.expiresAtMs - Date.now();
|
|
97
|
+
if (remaining <= 0)
|
|
98
|
+
throw new TpayHttpError(timeoutCode, 504);
|
|
99
|
+
return remaining;
|
|
100
|
+
}
|
|
101
|
+
async function fetchWithTimeout(fetchImpl, url, init, timeoutMs, timeoutCode) {
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
104
|
+
try {
|
|
105
|
+
return await fetchImpl(url, { ...init, signal: controller.signal });
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (controller.signal.aborted) {
|
|
109
|
+
throw new TpayHttpError(timeoutCode, 504);
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function asRecord(value) {
|
|
118
|
+
return value && typeof value === "object" ? value : {};
|
|
119
|
+
}
|
|
120
|
+
function readPaymentErrors(value) {
|
|
121
|
+
if (!Array.isArray(value))
|
|
122
|
+
return [];
|
|
123
|
+
return value.map((entry) => {
|
|
124
|
+
const record = asRecord(entry);
|
|
125
|
+
return {
|
|
126
|
+
errorCode: String(record.errorCode ?? ""),
|
|
127
|
+
errorMessage: String(record.errorMessage ?? ""),
|
|
128
|
+
fieldName: typeof record.fieldName === "string" ? record.fieldName : null,
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async function readJson(response, code) {
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
const failure = await readTpayHttpFailure(response);
|
|
135
|
+
throw new TpayHttpError(code, response.status, failure.diagnostic, failure.refusedBeforeTransaction);
|
|
136
|
+
}
|
|
137
|
+
const json = await response.json();
|
|
138
|
+
if (!json || typeof json !== "object")
|
|
139
|
+
throw new TpayHttpError("tpay_invalid_response", response.status);
|
|
140
|
+
return json;
|
|
141
|
+
}
|
|
142
|
+
function mapChannel(value) {
|
|
143
|
+
const record = value && typeof value === "object" ? value : {};
|
|
144
|
+
return {
|
|
145
|
+
id: String(record.id ?? ""),
|
|
146
|
+
name: String(record.name ?? ""),
|
|
147
|
+
fullName: String(record.fullName ?? record.name ?? ""),
|
|
148
|
+
available: record.available === true,
|
|
149
|
+
onlinePayment: record.onlinePayment === true,
|
|
150
|
+
instantRedirection: record.instantRedirection === true,
|
|
151
|
+
groups: Array.isArray(record.groups) ? record.groups.map(mapGroup) : [],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function mapGroup(value) {
|
|
155
|
+
const record = value && typeof value === "object" ? value : {};
|
|
156
|
+
return { id: Number(record.id ?? 0), name: String(record.name ?? "") };
|
|
157
|
+
}
|
|
158
|
+
function readString(value, key) {
|
|
159
|
+
const raw = value[key];
|
|
160
|
+
if (typeof raw !== "string" || raw.length === 0)
|
|
161
|
+
throw new TpayHttpError("tpay_invalid_response", 200);
|
|
162
|
+
return raw;
|
|
163
|
+
}
|
|
164
|
+
function readNullableString(value, key) {
|
|
165
|
+
const raw = value[key];
|
|
166
|
+
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
167
|
+
}
|
|
168
|
+
function readNumber(value, key) {
|
|
169
|
+
const raw = value[key];
|
|
170
|
+
if (typeof raw !== "number" || !Number.isFinite(raw))
|
|
171
|
+
throw new TpayHttpError("tpay_invalid_response", 200);
|
|
172
|
+
return raw;
|
|
173
|
+
}
|
|
174
|
+
function readNullableTransactionAmount(value, key) {
|
|
175
|
+
const raw = value[key];
|
|
176
|
+
if (typeof raw === "number")
|
|
177
|
+
return Number.isFinite(raw) ? raw : null;
|
|
178
|
+
if (typeof raw !== "string")
|
|
179
|
+
return null;
|
|
180
|
+
const trimmed = raw.trim();
|
|
181
|
+
if (!/^\d+(?:\.\d+)?$/.test(trimmed))
|
|
182
|
+
return null;
|
|
183
|
+
const parsed = Number(trimmed);
|
|
184
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
185
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface TpayHttpFailureDiagnostic {
|
|
2
|
+
httpStatus: number;
|
|
3
|
+
requestId: string | null;
|
|
4
|
+
providerErrorCodes: string[];
|
|
5
|
+
fieldNames: string[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The provider answered, and its answer is read for two independent things: the
|
|
9
|
+
* sanitized diagnostic that may be logged, and whether the refusal PROVES no
|
|
10
|
+
* transaction was created.
|
|
11
|
+
*/
|
|
12
|
+
export interface TpayHttpFailure {
|
|
13
|
+
diagnostic: TpayHttpFailureDiagnostic;
|
|
14
|
+
/**
|
|
15
|
+
* Tpay rejected the request document itself, so no transaction row can exist.
|
|
16
|
+
* `false` is the fail-closed answer and the only one any ambiguity produces.
|
|
17
|
+
*/
|
|
18
|
+
refusedBeforeTransaction: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Tpay error codes that describe the REQUEST DOCUMENT — a missing field, a wrong
|
|
22
|
+
* type, an out-of-range or malformed value, an unknown field, an unparseable
|
|
23
|
+
* body, a wrong content type. Every one of them is decided by request validation
|
|
24
|
+
* and therefore cannot be produced once a transaction row exists.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately excluded, and each for a reason: `crc_not_unique` names an
|
|
27
|
+
* ALREADY EXISTING transaction; `transaction_lock`, `transaction_blocked`,
|
|
28
|
+
* `payment_failed` and `ts:transaction:*` describe a transaction's outcome or a
|
|
29
|
+
* partially completed insert; `cannot_create_transaction`,
|
|
30
|
+
* `partner_cannot_create_transaction` and every channel/merchant availability
|
|
31
|
+
* code refuse for reasons whose ordering against creation we cannot prove from
|
|
32
|
+
* here; `general` and `internal_server_error` are opaque. A refusal carrying any
|
|
33
|
+
* of them keeps today's behavior and defers to reconciliation.
|
|
34
|
+
*
|
|
35
|
+
* ⚠️ This list is matched against the RAW body, never against
|
|
36
|
+
* `TpayHttpFailureDiagnostic.providerErrorCodes` — that projection drops any code
|
|
37
|
+
* outside the loggable set, which would turn a mixed body into a uniformly safe
|
|
38
|
+
* one.
|
|
39
|
+
*/
|
|
40
|
+
export declare const TPAY_PRE_TRANSACTION_REFUSAL_CODES: ReadonlySet<string>;
|
|
41
|
+
export declare function readTpayHttpFailure(response: {
|
|
42
|
+
status: number;
|
|
43
|
+
text(): Promise<string>;
|
|
44
|
+
}): Promise<TpayHttpFailure>;
|
|
45
|
+
export declare function tpayHttpFailureDiagnostic(error: unknown): TpayHttpFailureDiagnostic | null;
|
|
46
|
+
export declare class TpayHttpError extends Error {
|
|
47
|
+
readonly code: string;
|
|
48
|
+
readonly status: number;
|
|
49
|
+
readonly failureDiagnostic: TpayHttpFailureDiagnostic | null;
|
|
50
|
+
/**
|
|
51
|
+
* Set only from a body this client actually parsed. It never crosses a
|
|
52
|
+
* boundary as a claim on its own — the dispatch boundary turns it into typed
|
|
53
|
+
* evidence, and the control-plane RPC re-proves the attempt independently.
|
|
54
|
+
*/
|
|
55
|
+
readonly refusedBeforeTransaction: boolean;
|
|
56
|
+
constructor(code: string, status: number, failureDiagnostic?: TpayHttpFailureDiagnostic | null, refusedBeforeTransaction?: boolean);
|
|
57
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { loggableProviderErrorCode, loggableProviderFieldName, loggableProviderRequestId, } from "./providerAttemptFailureDiagnostic.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tpay error codes that describe the REQUEST DOCUMENT — a missing field, a wrong
|
|
4
|
+
* type, an out-of-range or malformed value, an unknown field, an unparseable
|
|
5
|
+
* body, a wrong content type. Every one of them is decided by request validation
|
|
6
|
+
* and therefore cannot be produced once a transaction row exists.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately excluded, and each for a reason: `crc_not_unique` names an
|
|
9
|
+
* ALREADY EXISTING transaction; `transaction_lock`, `transaction_blocked`,
|
|
10
|
+
* `payment_failed` and `ts:transaction:*` describe a transaction's outcome or a
|
|
11
|
+
* partially completed insert; `cannot_create_transaction`,
|
|
12
|
+
* `partner_cannot_create_transaction` and every channel/merchant availability
|
|
13
|
+
* code refuse for reasons whose ordering against creation we cannot prove from
|
|
14
|
+
* here; `general` and `internal_server_error` are opaque. A refusal carrying any
|
|
15
|
+
* of them keeps today's behavior and defers to reconciliation.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ This list is matched against the RAW body, never against
|
|
18
|
+
* `TpayHttpFailureDiagnostic.providerErrorCodes` — that projection drops any code
|
|
19
|
+
* outside the loggable set, which would turn a mixed body into a uniformly safe
|
|
20
|
+
* one.
|
|
21
|
+
*/
|
|
22
|
+
export const TPAY_PRE_TRANSACTION_REFUSAL_CODES = new Set([
|
|
23
|
+
"country_not_valid",
|
|
24
|
+
"field_required",
|
|
25
|
+
"incorrect_content_type",
|
|
26
|
+
"invalid_request_body",
|
|
27
|
+
"is_required",
|
|
28
|
+
"not_array",
|
|
29
|
+
"not_natural_int",
|
|
30
|
+
"not_string",
|
|
31
|
+
"not_valid",
|
|
32
|
+
"too_low",
|
|
33
|
+
"ts:country:not_valid",
|
|
34
|
+
"ts:payer:phone:not_valid",
|
|
35
|
+
"unexpected_field",
|
|
36
|
+
]);
|
|
37
|
+
/** The one HTTP status on which Tpay reports request validation. */
|
|
38
|
+
const TPAY_REQUEST_VALIDATION_STATUS = 400;
|
|
39
|
+
const MAX_FAILURE_BODY_BYTES = 16_384;
|
|
40
|
+
const MAX_DIAGNOSTIC_ITEMS = 5;
|
|
41
|
+
export async function readTpayHttpFailure(response) {
|
|
42
|
+
const base = emptyDiagnostic(response.status);
|
|
43
|
+
try {
|
|
44
|
+
const body = await response.text();
|
|
45
|
+
if (body.length > MAX_FAILURE_BODY_BYTES)
|
|
46
|
+
return unreadableFailure(base);
|
|
47
|
+
const parsed = JSON.parse(body);
|
|
48
|
+
const root = asRecord(parsed);
|
|
49
|
+
const payment = asRecord(root.payments);
|
|
50
|
+
const errors = [...readArray(root.errors), ...readArray(payment.errors)];
|
|
51
|
+
const rawErrorCodes = errors.map((entry) => asRecord(entry).errorCode);
|
|
52
|
+
return {
|
|
53
|
+
diagnostic: {
|
|
54
|
+
...base,
|
|
55
|
+
requestId: loggableProviderRequestId(root.requestId) ? root.requestId : null,
|
|
56
|
+
providerErrorCodes: allowedValues(errors.map((entry) => asRecord(entry).errorCode), loggableProviderErrorCode),
|
|
57
|
+
fieldNames: allowedValues(errors.map((entry) => asRecord(entry).fieldName), loggableProviderFieldName),
|
|
58
|
+
},
|
|
59
|
+
refusedBeforeTransaction: refusedBeforeTransaction(response.status, rawErrorCodes),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return unreadableFailure(base);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether this answer proves Tpay created nothing.
|
|
68
|
+
*
|
|
69
|
+
* Three conditions, all required. The status must be exactly the validation
|
|
70
|
+
* status: a 409 may name an existing transaction, a 429 or any 5xx may follow
|
|
71
|
+
* one that was created before the response was lost. The body must have yielded
|
|
72
|
+
* at least one error code, so an opaque refusal from an edge proxy — which we
|
|
73
|
+
* cannot attribute to Tpay's validator — releases nothing. And EVERY raw code
|
|
74
|
+
* must be allow-listed, including codes this repository does not otherwise
|
|
75
|
+
* recognize, so one unknown code in a mixed body is enough to refuse.
|
|
76
|
+
*/
|
|
77
|
+
function refusedBeforeTransaction(status, rawErrorCodes) {
|
|
78
|
+
return status === TPAY_REQUEST_VALIDATION_STATUS
|
|
79
|
+
&& rawErrorCodes.length > 0
|
|
80
|
+
&& rawErrorCodes.every((code) => typeof code === "string" && TPAY_PRE_TRANSACTION_REFUSAL_CODES.has(code));
|
|
81
|
+
}
|
|
82
|
+
function unreadableFailure(diagnostic) {
|
|
83
|
+
return { diagnostic, refusedBeforeTransaction: false };
|
|
84
|
+
}
|
|
85
|
+
export function tpayHttpFailureDiagnostic(error) {
|
|
86
|
+
if (!error || typeof error !== "object")
|
|
87
|
+
return null;
|
|
88
|
+
const value = asRecord(error.failureDiagnostic);
|
|
89
|
+
const httpStatus = value.httpStatus;
|
|
90
|
+
const requestId = value.requestId;
|
|
91
|
+
const providerErrorCodes = value.providerErrorCodes;
|
|
92
|
+
const fieldNames = value.fieldNames;
|
|
93
|
+
if (!Number.isInteger(httpStatus) || httpStatus < 100 || httpStatus > 599)
|
|
94
|
+
return null;
|
|
95
|
+
if (requestId !== null && !loggableProviderRequestId(requestId))
|
|
96
|
+
return null;
|
|
97
|
+
if (!validAllowedValues(providerErrorCodes, loggableProviderErrorCode)
|
|
98
|
+
|| !validAllowedValues(fieldNames, loggableProviderFieldName))
|
|
99
|
+
return null;
|
|
100
|
+
return {
|
|
101
|
+
httpStatus: httpStatus,
|
|
102
|
+
requestId: requestId,
|
|
103
|
+
providerErrorCodes: [...providerErrorCodes],
|
|
104
|
+
fieldNames: [...fieldNames],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export class TpayHttpError extends Error {
|
|
108
|
+
code;
|
|
109
|
+
status;
|
|
110
|
+
failureDiagnostic;
|
|
111
|
+
/**
|
|
112
|
+
* Set only from a body this client actually parsed. It never crosses a
|
|
113
|
+
* boundary as a claim on its own — the dispatch boundary turns it into typed
|
|
114
|
+
* evidence, and the control-plane RPC re-proves the attempt independently.
|
|
115
|
+
*/
|
|
116
|
+
refusedBeforeTransaction;
|
|
117
|
+
constructor(code, status, failureDiagnostic = null, refusedBeforeTransaction = false) {
|
|
118
|
+
super(code);
|
|
119
|
+
this.name = "TpayHttpError";
|
|
120
|
+
this.code = code;
|
|
121
|
+
this.status = status;
|
|
122
|
+
this.failureDiagnostic = failureDiagnostic;
|
|
123
|
+
this.refusedBeforeTransaction = refusedBeforeTransaction;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function emptyDiagnostic(httpStatus) {
|
|
127
|
+
return { httpStatus, requestId: null, providerErrorCodes: [], fieldNames: [] };
|
|
128
|
+
}
|
|
129
|
+
function asRecord(value) {
|
|
130
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
131
|
+
? value
|
|
132
|
+
: {};
|
|
133
|
+
}
|
|
134
|
+
function readArray(value) {
|
|
135
|
+
return Array.isArray(value) ? value : [];
|
|
136
|
+
}
|
|
137
|
+
function allowedValues(values, allowed) {
|
|
138
|
+
return [...new Set(values.filter(allowed))].slice(0, MAX_DIAGNOSTIC_ITEMS);
|
|
139
|
+
}
|
|
140
|
+
function validAllowedValues(value, allowed) {
|
|
141
|
+
return Array.isArray(value) && value.length <= MAX_DIAGNOSTIC_ITEMS
|
|
142
|
+
&& value.every(allowed);
|
|
143
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PaymentExecutionPort } from "./donorContracts.js";
|
|
2
|
+
import type { PaymentExecutionInput } from "./donorContracts.js";
|
|
3
|
+
import type { TpayHttpClient, TpayPayerInput } from "./tpayHttpClient.js";
|
|
4
|
+
/** The one place this adapter spells its own provider identity. */
|
|
5
|
+
export declare const PROVIDER_KIND: "tpay";
|
|
6
|
+
export interface TpayPaymentExecutionAdapterOptions {
|
|
7
|
+
client: TpayHttpClient;
|
|
8
|
+
notificationUrl: string;
|
|
9
|
+
successUrl: string;
|
|
10
|
+
errorUrl: string;
|
|
11
|
+
oneClickEnabled?: boolean;
|
|
12
|
+
aliasRegistrationOnCodeEnabled?: boolean;
|
|
13
|
+
/** One public switch for admitting new subscription BLIK Model O mandates. */
|
|
14
|
+
modelOActivationEnabled?: boolean;
|
|
15
|
+
resolvePayer?: (input: PaymentExecutionInput) => TpayPayerInput | null;
|
|
16
|
+
}
|
|
17
|
+
export declare function createTpayPaymentExecutionAdapter({ client, notificationUrl, successUrl, errorUrl, oneClickEnabled, aliasRegistrationOnCodeEnabled, modelOActivationEnabled, resolvePayer, }: TpayPaymentExecutionAdapterOptions): PaymentExecutionPort;
|
|
18
|
+
export declare class TpayExecutionInputError extends Error {
|
|
19
|
+
constructor(message: string);
|
|
20
|
+
}
|
|
@@ -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;
|