@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,61 @@
|
|
|
1
|
+
import { serializeFailureEvidence } from "./paymentFailureEvidence.js";
|
|
2
|
+
import { assertableHints } from "./tpayDeclineEvidence.js";
|
|
3
|
+
import { classifyDecline } from "./classifyDecline.js";
|
|
4
|
+
/** Known execution construction, never a conclusion drawn from the PSP name. */
|
|
5
|
+
function methodFor(flow) {
|
|
6
|
+
if (flow === "blik_one_time" || flow === "blik_recurring_activation") {
|
|
7
|
+
return { kind: "blik", recoveryMethodKey: "blik", interaction: "new_instrument" };
|
|
8
|
+
}
|
|
9
|
+
if (flow === "blik_one_click" || flow === "blik_recurring_saved") {
|
|
10
|
+
return { kind: "blik", recoveryMethodKey: "blik_one_click", interaction: "stored_instrument" };
|
|
11
|
+
}
|
|
12
|
+
return flow === "pbl_one_time"
|
|
13
|
+
? { kind: "bank_transfer", recoveryMethodKey: "transfer", interaction: "new_instrument" } : null;
|
|
14
|
+
}
|
|
15
|
+
export function tpayFailureEvidence(input) {
|
|
16
|
+
const method = methodFor(input.flow);
|
|
17
|
+
const advice = input.observation.identityMismatch ? null : input.reading?.adviceCode ?? null;
|
|
18
|
+
const operation = input.flow === "blik_recurring_activation"
|
|
19
|
+
? "recurring_setup" : method?.interaction === "stored_instrument"
|
|
20
|
+
? "stored_method_payment" : method ? "one_time_payment" : null;
|
|
21
|
+
return serializeFailureEvidence({
|
|
22
|
+
version: 1, source: input.source, disposition: input.observation.disposition,
|
|
23
|
+
refusalVerified: input.refusalVerified, providerPaymentId: input.providerPaymentId, providerChargeId: null,
|
|
24
|
+
code: input.errorCode ?? null, declineCode: input.observation.identityMismatch ? null : input.observation.code,
|
|
25
|
+
adviceCode: advice,
|
|
26
|
+
// This table is explicitly inferred; it must never masquerade as issuer advice.
|
|
27
|
+
adviceOrigin: advice ? "inferred" : "unknown",
|
|
28
|
+
operation, method, methodSource: method ? "execution_contract" : "unknown",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Numeric code hypotheses are not promoted into new customer-facing facts. */
|
|
32
|
+
export function normalizeTpayFailureEvidence(input) {
|
|
33
|
+
const evidence = serializeFailureEvidence(input);
|
|
34
|
+
// The operation is context, not a cause: a mistyped or expired BLIK code is
|
|
35
|
+
// refused inside a mandate registration too. Only the persisted mandate
|
|
36
|
+
// decision (`failure_reason`) may say the agreement itself was refused.
|
|
37
|
+
return {
|
|
38
|
+
refusalVerified: evidence.refusalVerified,
|
|
39
|
+
cause: "generic_decline",
|
|
40
|
+
certainty: "unknown", disclosure: "safe",
|
|
41
|
+
method: evidence.method, operation: evidence.operation, advice: null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Existing reconciliation classification; moved intact, never fed new evidence. */
|
|
45
|
+
export function refusalCapture(refusal, failureReasonKey) {
|
|
46
|
+
const hints = assertableHints(refusal.reading.hints, null) ?? [];
|
|
47
|
+
const classification = classifyDecline({
|
|
48
|
+
code: refusal.code,
|
|
49
|
+
mandateUnsupported: false,
|
|
50
|
+
declineCode: refusal.code,
|
|
51
|
+
...(refusal.reading.adviceCode ? { adviceCode: refusal.reading.adviceCode } : {}),
|
|
52
|
+
...(hints.length > 0 ? { neutralReasonHints: hints } : {}),
|
|
53
|
+
}, { failureReasonKey });
|
|
54
|
+
return {
|
|
55
|
+
declineCode: refusal.code,
|
|
56
|
+
adviceCode: refusal.reading.adviceCode ?? null,
|
|
57
|
+
neutralReasonHints: hints,
|
|
58
|
+
failureClass: classification.failureClass,
|
|
59
|
+
failureClassDecidedBy: classification.decidedBy,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type TpayTransactionCreateInput, type TpayTransactionCreated } from "./tpayTransactionPayload.js";
|
|
2
|
+
export { TpayHttpError } from "./tpayHttpFailure.js";
|
|
3
|
+
export interface TpayFetchLike {
|
|
4
|
+
(url: string, init?: {
|
|
5
|
+
method?: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
body?: string;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
ok: boolean;
|
|
11
|
+
status: number;
|
|
12
|
+
json(): Promise<unknown>;
|
|
13
|
+
text(): Promise<string>;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
export interface TpayHttpClientOptions {
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
clientId: string;
|
|
19
|
+
clientSecret: string;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
fetchImpl?: TpayFetchLike;
|
|
22
|
+
now?: () => number;
|
|
23
|
+
}
|
|
24
|
+
export type { TpayAliasInput, TpayPayerInput, TpayPaymentError, TpayTransactionCreateInput, TpayTransactionCreated, } from "./tpayTransactionPayload.js";
|
|
25
|
+
export interface TpayPaymentChannel {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
fullName: string;
|
|
29
|
+
available: boolean;
|
|
30
|
+
onlinePayment: boolean;
|
|
31
|
+
instantRedirection: boolean;
|
|
32
|
+
groups: Array<{
|
|
33
|
+
id: number;
|
|
34
|
+
name: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
export interface TpayHttpClient {
|
|
38
|
+
createTransaction(input: TpayTransactionCreateInput): Promise<TpayTransactionCreated>;
|
|
39
|
+
getTransaction(transactionId: string): Promise<TpayTransactionReadback>;
|
|
40
|
+
listPaymentChannels(): Promise<TpayPaymentChannel[]>;
|
|
41
|
+
}
|
|
42
|
+
export interface TpayTransactionReadback extends Record<string, unknown> {
|
|
43
|
+
transactionId: string;
|
|
44
|
+
title: string | null;
|
|
45
|
+
status: string;
|
|
46
|
+
transactionPaymentUrl: string | null;
|
|
47
|
+
amount: number | null;
|
|
48
|
+
currency: string | null;
|
|
49
|
+
requestId: string | null;
|
|
50
|
+
payments: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
export declare function createTpayHttpClient({ baseUrl, clientId, clientSecret, timeoutMs, fetchImpl, now, }: TpayHttpClientOptions): TpayHttpClient;
|
|
@@ -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
|
+
}
|