@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,290 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildTpayTransactionPayload,
|
|
3
|
+
type TpayPaymentError,
|
|
4
|
+
type TpayTransactionCreateInput,
|
|
5
|
+
type TpayTransactionCreated,
|
|
6
|
+
} from "./tpayTransactionPayload.js";
|
|
7
|
+
import { tpayTransactionDispatchBoundary } from "./tpayDispatchFailure.js";
|
|
8
|
+
import { readTpayHttpFailure, TpayHttpError } from "./tpayHttpFailure.js";
|
|
9
|
+
|
|
10
|
+
export { TpayHttpError } from "./tpayHttpFailure.js";
|
|
11
|
+
|
|
12
|
+
export interface TpayFetchLike {
|
|
13
|
+
(url: string, init?: {
|
|
14
|
+
method?: string;
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
body?: string;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
ok: boolean;
|
|
20
|
+
status: number;
|
|
21
|
+
json(): Promise<unknown>;
|
|
22
|
+
text(): Promise<string>;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TpayHttpClientOptions {
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
clientId: string;
|
|
29
|
+
clientSecret: string;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
fetchImpl?: TpayFetchLike;
|
|
32
|
+
now?: () => number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type {
|
|
36
|
+
TpayAliasInput,
|
|
37
|
+
TpayPayerInput,
|
|
38
|
+
TpayPaymentError,
|
|
39
|
+
TpayTransactionCreateInput,
|
|
40
|
+
TpayTransactionCreated,
|
|
41
|
+
} from "./tpayTransactionPayload.js";
|
|
42
|
+
|
|
43
|
+
export interface TpayPaymentChannel {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
fullName: string;
|
|
47
|
+
available: boolean;
|
|
48
|
+
onlinePayment: boolean;
|
|
49
|
+
instantRedirection: boolean;
|
|
50
|
+
groups: Array<{ id: number; name: string }>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface TpayHttpClient {
|
|
54
|
+
createTransaction(input: TpayTransactionCreateInput): Promise<TpayTransactionCreated>;
|
|
55
|
+
getTransaction(transactionId: string): Promise<TpayTransactionReadback>;
|
|
56
|
+
listPaymentChannels(): Promise<TpayPaymentChannel[]>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TpayTransactionReadback extends Record<string, unknown> {
|
|
60
|
+
transactionId: string;
|
|
61
|
+
title: string | null;
|
|
62
|
+
status: string;
|
|
63
|
+
transactionPaymentUrl: string | null;
|
|
64
|
+
amount: number | null;
|
|
65
|
+
currency: string | null;
|
|
66
|
+
requestId: string | null;
|
|
67
|
+
payments: Record<string, unknown>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface OAuthToken {
|
|
71
|
+
accessToken: string;
|
|
72
|
+
expiresAtMs: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const TOKEN_REFRESH_SKEW_MS = 60_000;
|
|
76
|
+
const DEFAULT_TPAY_TIMEOUT_MS = 10_000;
|
|
77
|
+
|
|
78
|
+
export function createTpayHttpClient({
|
|
79
|
+
baseUrl,
|
|
80
|
+
clientId,
|
|
81
|
+
clientSecret,
|
|
82
|
+
timeoutMs = DEFAULT_TPAY_TIMEOUT_MS,
|
|
83
|
+
fetchImpl = globalThis.fetch as unknown as TpayFetchLike,
|
|
84
|
+
now = () => Date.now(),
|
|
85
|
+
}: TpayHttpClientOptions): TpayHttpClient {
|
|
86
|
+
let token: OAuthToken | null = null;
|
|
87
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
88
|
+
|
|
89
|
+
function requestDeadline(): { expiresAtMs: number } {
|
|
90
|
+
return { expiresAtMs: Date.now() + timeoutMs };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function bearer(deadline: { expiresAtMs: number }): Promise<string> {
|
|
94
|
+
if (token && token.expiresAtMs - TOKEN_REFRESH_SKEW_MS > now()) {
|
|
95
|
+
return token.accessToken;
|
|
96
|
+
}
|
|
97
|
+
const body = new URLSearchParams({ client_id: clientId, client_secret: clientSecret });
|
|
98
|
+
const response = await fetchWithTimeout(fetchImpl, `${normalizedBaseUrl}/oauth/auth`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
101
|
+
body: body.toString(),
|
|
102
|
+
}, remainingTimeoutMs(deadline, "tpay_oauth_timeout"), "tpay_oauth_timeout");
|
|
103
|
+
const json = await readJson(response, "tpay_oauth_failed");
|
|
104
|
+
const accessToken = readString(json, "access_token");
|
|
105
|
+
const expiresIn = readNumber(json, "expires_in");
|
|
106
|
+
token = { accessToken, expiresAtMs: now() + expiresIn * 1000 };
|
|
107
|
+
return accessToken;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function request(
|
|
111
|
+
path: string,
|
|
112
|
+
init: { method?: string; body?: unknown } = {},
|
|
113
|
+
deadline = requestDeadline(),
|
|
114
|
+
) {
|
|
115
|
+
const headers: Record<string, string> = { Authorization: `Bearer ${await bearer(deadline)}` };
|
|
116
|
+
let body: string | undefined;
|
|
117
|
+
if (init.body !== undefined) {
|
|
118
|
+
headers["Content-Type"] = "application/json";
|
|
119
|
+
body = JSON.stringify(init.body);
|
|
120
|
+
}
|
|
121
|
+
const response = await fetchWithTimeout(fetchImpl, `${normalizedBaseUrl}${path}`, {
|
|
122
|
+
method: init.method ?? "GET",
|
|
123
|
+
headers,
|
|
124
|
+
body,
|
|
125
|
+
}, remainingTimeoutMs(deadline, "tpay_request_timeout"), "tpay_request_timeout");
|
|
126
|
+
return readJson(response, "tpay_request_failed");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
async createTransaction(input) {
|
|
131
|
+
const deadline = requestDeadline();
|
|
132
|
+
return tpayTransactionDispatchBoundary({
|
|
133
|
+
authenticate: () => bearer(deadline),
|
|
134
|
+
prepare: (accessToken) => ({
|
|
135
|
+
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
136
|
+
body: JSON.stringify(buildTpayTransactionPayload(input)),
|
|
137
|
+
timeoutMs: remainingTimeoutMs(deadline, "tpay_request_deadline_exhausted"),
|
|
138
|
+
}),
|
|
139
|
+
dispatch: ({ headers, body, timeoutMs }) => fetchWithTimeout(
|
|
140
|
+
fetchImpl,
|
|
141
|
+
`${normalizedBaseUrl}/transactions`,
|
|
142
|
+
{ method: "POST", headers, body },
|
|
143
|
+
timeoutMs,
|
|
144
|
+
"tpay_request_timeout",
|
|
145
|
+
),
|
|
146
|
+
decode: async (response) => {
|
|
147
|
+
const json = await readJson(response, "tpay_request_failed");
|
|
148
|
+
const payments = asRecord(json.payments);
|
|
149
|
+
return {
|
|
150
|
+
transactionId: readString(json, "transactionId"),
|
|
151
|
+
title: readString(json, "title"),
|
|
152
|
+
status: readString(json, "status"),
|
|
153
|
+
transactionPaymentUrl: readNullableString(json, "transactionPaymentUrl"),
|
|
154
|
+
requestId: readNullableString(json, "requestId"),
|
|
155
|
+
payIdEligible: typeof payments.payIdEligible === "boolean" ? payments.payIdEligible : null,
|
|
156
|
+
errors: readPaymentErrors(payments.errors),
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
async getTransaction(transactionId) {
|
|
163
|
+
const json = await request(
|
|
164
|
+
`/transactions/${encodeURIComponent(transactionId)}`,
|
|
165
|
+
{},
|
|
166
|
+
requestDeadline(),
|
|
167
|
+
);
|
|
168
|
+
return {
|
|
169
|
+
// Keep provider fields that recovery normalizes later (notably nested
|
|
170
|
+
// dates and paidAmount). Explicit canonical fields below remain the
|
|
171
|
+
// typed client contract.
|
|
172
|
+
...json,
|
|
173
|
+
transactionId: readString(json, "transactionId"),
|
|
174
|
+
title: readNullableString(json, "title"),
|
|
175
|
+
status: readString(json, "status"),
|
|
176
|
+
transactionPaymentUrl: readNullableString(json, "transactionPaymentUrl"),
|
|
177
|
+
amount: readNullableTransactionAmount(json, "amount"),
|
|
178
|
+
currency: readNullableString(json, "currency"),
|
|
179
|
+
requestId: readNullableString(json, "requestId"),
|
|
180
|
+
payments: asRecord(json.payments),
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
async listPaymentChannels() {
|
|
185
|
+
const json = await request("/transactions/channels", {}, requestDeadline());
|
|
186
|
+
const channels: unknown[] = Array.isArray((json as Record<string, unknown>).channels)
|
|
187
|
+
? (json as Record<string, unknown>).channels as unknown[]
|
|
188
|
+
: [];
|
|
189
|
+
return channels.map(mapChannel);
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function remainingTimeoutMs(deadline: { expiresAtMs: number }, timeoutCode: string): number {
|
|
195
|
+
const remaining = deadline.expiresAtMs - Date.now();
|
|
196
|
+
if (remaining <= 0) throw new TpayHttpError(timeoutCode, 504);
|
|
197
|
+
return remaining;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function fetchWithTimeout(
|
|
201
|
+
fetchImpl: TpayFetchLike,
|
|
202
|
+
url: string,
|
|
203
|
+
init: Parameters<TpayFetchLike>[1],
|
|
204
|
+
timeoutMs: number,
|
|
205
|
+
timeoutCode: string,
|
|
206
|
+
): ReturnType<TpayFetchLike> {
|
|
207
|
+
const controller = new AbortController();
|
|
208
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
209
|
+
try {
|
|
210
|
+
return await fetchImpl(url, { ...init, signal: controller.signal });
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (controller.signal.aborted) {
|
|
213
|
+
throw new TpayHttpError(timeoutCode, 504);
|
|
214
|
+
}
|
|
215
|
+
throw error;
|
|
216
|
+
} finally {
|
|
217
|
+
clearTimeout(timeout);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
222
|
+
return value && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function readPaymentErrors(value: unknown): TpayPaymentError[] {
|
|
226
|
+
if (!Array.isArray(value)) return [];
|
|
227
|
+
return value.map((entry) => {
|
|
228
|
+
const record = asRecord(entry);
|
|
229
|
+
return {
|
|
230
|
+
errorCode: String(record.errorCode ?? ""),
|
|
231
|
+
errorMessage: String(record.errorMessage ?? ""),
|
|
232
|
+
fieldName: typeof record.fieldName === "string" ? record.fieldName : null,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function readJson(response: Awaited<ReturnType<TpayFetchLike>>, code: string): Promise<Record<string, unknown>> {
|
|
238
|
+
if (!response.ok) {
|
|
239
|
+
const failure = await readTpayHttpFailure(response);
|
|
240
|
+
throw new TpayHttpError(code, response.status, failure.diagnostic, failure.refusedBeforeTransaction);
|
|
241
|
+
}
|
|
242
|
+
const json = await response.json();
|
|
243
|
+
if (!json || typeof json !== "object") throw new TpayHttpError("tpay_invalid_response", response.status);
|
|
244
|
+
return json as Record<string, unknown>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function mapChannel(value: unknown): TpayPaymentChannel {
|
|
248
|
+
const record = value && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
249
|
+
return {
|
|
250
|
+
id: String(record.id ?? ""),
|
|
251
|
+
name: String(record.name ?? ""),
|
|
252
|
+
fullName: String(record.fullName ?? record.name ?? ""),
|
|
253
|
+
available: record.available === true,
|
|
254
|
+
onlinePayment: record.onlinePayment === true,
|
|
255
|
+
instantRedirection: record.instantRedirection === true,
|
|
256
|
+
groups: Array.isArray(record.groups) ? record.groups.map(mapGroup) : [],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function mapGroup(value: unknown): { id: number; name: string } {
|
|
261
|
+
const record = value && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
262
|
+
return { id: Number(record.id ?? 0), name: String(record.name ?? "") };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function readString(value: Record<string, unknown>, key: string): string {
|
|
266
|
+
const raw = value[key];
|
|
267
|
+
if (typeof raw !== "string" || raw.length === 0) throw new TpayHttpError("tpay_invalid_response", 200);
|
|
268
|
+
return raw;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function readNullableString(value: Record<string, unknown>, key: string): string | null {
|
|
272
|
+
const raw = value[key];
|
|
273
|
+
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function readNumber(value: Record<string, unknown>, key: string): number {
|
|
277
|
+
const raw = value[key];
|
|
278
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) throw new TpayHttpError("tpay_invalid_response", 200);
|
|
279
|
+
return raw;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function readNullableTransactionAmount(value: Record<string, unknown>, key: string): number | null {
|
|
283
|
+
const raw = value[key];
|
|
284
|
+
if (typeof raw === "number") return Number.isFinite(raw) ? raw : null;
|
|
285
|
+
if (typeof raw !== "string") return null;
|
|
286
|
+
const trimmed = raw.trim();
|
|
287
|
+
if (!/^\d+(?:\.\d+)?$/.test(trimmed)) return null;
|
|
288
|
+
const parsed = Number(trimmed);
|
|
289
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
290
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loggableProviderErrorCode,
|
|
3
|
+
loggableProviderFieldName,
|
|
4
|
+
loggableProviderRequestId,
|
|
5
|
+
} from "./providerAttemptFailureDiagnostic.js";
|
|
6
|
+
|
|
7
|
+
export interface TpayHttpFailureDiagnostic {
|
|
8
|
+
httpStatus: number;
|
|
9
|
+
requestId: string | null;
|
|
10
|
+
providerErrorCodes: string[];
|
|
11
|
+
fieldNames: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The provider answered, and its answer is read for two independent things: the
|
|
16
|
+
* sanitized diagnostic that may be logged, and whether the refusal PROVES no
|
|
17
|
+
* transaction was created.
|
|
18
|
+
*/
|
|
19
|
+
export interface TpayHttpFailure {
|
|
20
|
+
diagnostic: TpayHttpFailureDiagnostic;
|
|
21
|
+
/**
|
|
22
|
+
* Tpay rejected the request document itself, so no transaction row can exist.
|
|
23
|
+
* `false` is the fail-closed answer and the only one any ambiguity produces.
|
|
24
|
+
*/
|
|
25
|
+
refusedBeforeTransaction: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Tpay error codes that describe the REQUEST DOCUMENT — a missing field, a wrong
|
|
30
|
+
* type, an out-of-range or malformed value, an unknown field, an unparseable
|
|
31
|
+
* body, a wrong content type. Every one of them is decided by request validation
|
|
32
|
+
* and therefore cannot be produced once a transaction row exists.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately excluded, and each for a reason: `crc_not_unique` names an
|
|
35
|
+
* ALREADY EXISTING transaction; `transaction_lock`, `transaction_blocked`,
|
|
36
|
+
* `payment_failed` and `ts:transaction:*` describe a transaction's outcome or a
|
|
37
|
+
* partially completed insert; `cannot_create_transaction`,
|
|
38
|
+
* `partner_cannot_create_transaction` and every channel/merchant availability
|
|
39
|
+
* code refuse for reasons whose ordering against creation we cannot prove from
|
|
40
|
+
* here; `general` and `internal_server_error` are opaque. A refusal carrying any
|
|
41
|
+
* of them keeps today's behavior and defers to reconciliation.
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ This list is matched against the RAW body, never against
|
|
44
|
+
* `TpayHttpFailureDiagnostic.providerErrorCodes` — that projection drops any code
|
|
45
|
+
* outside the loggable set, which would turn a mixed body into a uniformly safe
|
|
46
|
+
* one.
|
|
47
|
+
*/
|
|
48
|
+
export const TPAY_PRE_TRANSACTION_REFUSAL_CODES: ReadonlySet<string> = new Set([
|
|
49
|
+
"country_not_valid",
|
|
50
|
+
"field_required",
|
|
51
|
+
"incorrect_content_type",
|
|
52
|
+
"invalid_request_body",
|
|
53
|
+
"is_required",
|
|
54
|
+
"not_array",
|
|
55
|
+
"not_natural_int",
|
|
56
|
+
"not_string",
|
|
57
|
+
"not_valid",
|
|
58
|
+
"too_low",
|
|
59
|
+
"ts:country:not_valid",
|
|
60
|
+
"ts:payer:phone:not_valid",
|
|
61
|
+
"unexpected_field",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
/** The one HTTP status on which Tpay reports request validation. */
|
|
65
|
+
const TPAY_REQUEST_VALIDATION_STATUS = 400;
|
|
66
|
+
|
|
67
|
+
const MAX_FAILURE_BODY_BYTES = 16_384;
|
|
68
|
+
const MAX_DIAGNOSTIC_ITEMS = 5;
|
|
69
|
+
|
|
70
|
+
export async function readTpayHttpFailure(response: {
|
|
71
|
+
status: number;
|
|
72
|
+
text(): Promise<string>;
|
|
73
|
+
}): Promise<TpayHttpFailure> {
|
|
74
|
+
const base = emptyDiagnostic(response.status);
|
|
75
|
+
try {
|
|
76
|
+
const body = await response.text();
|
|
77
|
+
if (body.length > MAX_FAILURE_BODY_BYTES) return unreadableFailure(base);
|
|
78
|
+
const parsed = JSON.parse(body) as unknown;
|
|
79
|
+
const root = asRecord(parsed);
|
|
80
|
+
const payment = asRecord(root.payments);
|
|
81
|
+
const errors = [...readArray(root.errors), ...readArray(payment.errors)];
|
|
82
|
+
const rawErrorCodes = errors.map((entry) => asRecord(entry).errorCode);
|
|
83
|
+
return {
|
|
84
|
+
diagnostic: {
|
|
85
|
+
...base,
|
|
86
|
+
requestId: loggableProviderRequestId(root.requestId) ? root.requestId : null,
|
|
87
|
+
providerErrorCodes: allowedValues(errors.map((entry) => asRecord(entry).errorCode), loggableProviderErrorCode),
|
|
88
|
+
fieldNames: allowedValues(errors.map((entry) => asRecord(entry).fieldName), loggableProviderFieldName),
|
|
89
|
+
},
|
|
90
|
+
refusedBeforeTransaction: refusedBeforeTransaction(response.status, rawErrorCodes),
|
|
91
|
+
};
|
|
92
|
+
} catch {
|
|
93
|
+
return unreadableFailure(base);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether this answer proves Tpay created nothing.
|
|
99
|
+
*
|
|
100
|
+
* Three conditions, all required. The status must be exactly the validation
|
|
101
|
+
* status: a 409 may name an existing transaction, a 429 or any 5xx may follow
|
|
102
|
+
* one that was created before the response was lost. The body must have yielded
|
|
103
|
+
* at least one error code, so an opaque refusal from an edge proxy — which we
|
|
104
|
+
* cannot attribute to Tpay's validator — releases nothing. And EVERY raw code
|
|
105
|
+
* must be allow-listed, including codes this repository does not otherwise
|
|
106
|
+
* recognize, so one unknown code in a mixed body is enough to refuse.
|
|
107
|
+
*/
|
|
108
|
+
function refusedBeforeTransaction(status: number, rawErrorCodes: unknown[]): boolean {
|
|
109
|
+
return status === TPAY_REQUEST_VALIDATION_STATUS
|
|
110
|
+
&& rawErrorCodes.length > 0
|
|
111
|
+
&& rawErrorCodes.every((code) =>
|
|
112
|
+
typeof code === "string" && TPAY_PRE_TRANSACTION_REFUSAL_CODES.has(code)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function unreadableFailure(diagnostic: TpayHttpFailureDiagnostic): TpayHttpFailure {
|
|
117
|
+
return { diagnostic, refusedBeforeTransaction: false };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function tpayHttpFailureDiagnostic(error: unknown): TpayHttpFailureDiagnostic | null {
|
|
121
|
+
if (!error || typeof error !== "object") return null;
|
|
122
|
+
const value = asRecord((error as { failureDiagnostic?: unknown }).failureDiagnostic);
|
|
123
|
+
const httpStatus = value.httpStatus;
|
|
124
|
+
const requestId = value.requestId;
|
|
125
|
+
const providerErrorCodes = value.providerErrorCodes;
|
|
126
|
+
const fieldNames = value.fieldNames;
|
|
127
|
+
if (!Number.isInteger(httpStatus) || (httpStatus as number) < 100 || (httpStatus as number) > 599) return null;
|
|
128
|
+
if (requestId !== null && !loggableProviderRequestId(requestId)) return null;
|
|
129
|
+
if (!validAllowedValues(providerErrorCodes, loggableProviderErrorCode)
|
|
130
|
+
|| !validAllowedValues(fieldNames, loggableProviderFieldName)) return null;
|
|
131
|
+
return {
|
|
132
|
+
httpStatus: httpStatus as number,
|
|
133
|
+
requestId: requestId as string | null,
|
|
134
|
+
providerErrorCodes: [...providerErrorCodes],
|
|
135
|
+
fieldNames: [...fieldNames],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class TpayHttpError extends Error {
|
|
140
|
+
readonly code: string;
|
|
141
|
+
readonly status: number;
|
|
142
|
+
readonly failureDiagnostic: TpayHttpFailureDiagnostic | null;
|
|
143
|
+
/**
|
|
144
|
+
* Set only from a body this client actually parsed. It never crosses a
|
|
145
|
+
* boundary as a claim on its own — the dispatch boundary turns it into typed
|
|
146
|
+
* evidence, and the control-plane RPC re-proves the attempt independently.
|
|
147
|
+
*/
|
|
148
|
+
readonly refusedBeforeTransaction: boolean;
|
|
149
|
+
|
|
150
|
+
constructor(
|
|
151
|
+
code: string,
|
|
152
|
+
status: number,
|
|
153
|
+
failureDiagnostic: TpayHttpFailureDiagnostic | null = null,
|
|
154
|
+
refusedBeforeTransaction = false,
|
|
155
|
+
) {
|
|
156
|
+
super(code);
|
|
157
|
+
this.name = "TpayHttpError";
|
|
158
|
+
this.code = code;
|
|
159
|
+
this.status = status;
|
|
160
|
+
this.failureDiagnostic = failureDiagnostic;
|
|
161
|
+
this.refusedBeforeTransaction = refusedBeforeTransaction;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function emptyDiagnostic(httpStatus: number): TpayHttpFailureDiagnostic {
|
|
166
|
+
return { httpStatus, requestId: null, providerErrorCodes: [], fieldNames: [] };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
170
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
171
|
+
? value as Record<string, unknown>
|
|
172
|
+
: {};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readArray(value: unknown): unknown[] {
|
|
176
|
+
return Array.isArray(value) ? value : [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function allowedValues(values: unknown[], allowed: (value: unknown) => value is string): string[] {
|
|
180
|
+
return [...new Set(values.filter(allowed))].slice(0, MAX_DIAGNOSTIC_ITEMS);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function validAllowedValues(value: unknown, allowed: (item: unknown) => item is string): value is string[] {
|
|
184
|
+
return Array.isArray(value) && value.length <= MAX_DIAGNOSTIC_ITEMS
|
|
185
|
+
&& value.every(allowed);
|
|
186
|
+
}
|