@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,140 @@
|
|
|
1
|
+
/** Private structural contracts for the unchanged execution and observation slices. */
|
|
2
|
+
import type { PaymentExecutionBaseInput, PaymentExecutionBaseResult } from "@openlup/core/payment";
|
|
3
|
+
import type { ProviderReadPurpose } from "../contracts/index.js";
|
|
4
|
+
export type PaymentProviderFlow = "one_time_payment" | "setup_reusable_method" | "off_session_payment" | "blik_one_time" | "blik_one_click" | "pbl_one_time" | "blik_recurring_activation" | "recurring_charge";
|
|
5
|
+
export declare const PAYMENT_NEXT_ACTION_KINDS: readonly ["redirect", "3ds_challenge", "sca_required"];
|
|
6
|
+
export type PaymentNextActionKind = (typeof PAYMENT_NEXT_ACTION_KINDS)[number];
|
|
7
|
+
export interface PaymentExecutionInput extends PaymentExecutionBaseInput {
|
|
8
|
+
orderRef: string;
|
|
9
|
+
orderId?: string;
|
|
10
|
+
subjectId?: string | null;
|
|
11
|
+
returnContext?: "public" | "account";
|
|
12
|
+
payer?: {
|
|
13
|
+
email: string;
|
|
14
|
+
name: string;
|
|
15
|
+
ip?: string | null;
|
|
16
|
+
userAgent?: string | null;
|
|
17
|
+
};
|
|
18
|
+
transientProviderInput?: TpayTransientProviderInput | null;
|
|
19
|
+
paymentMethodAliasType?: "UID" | "PAYID";
|
|
20
|
+
paymentMethodRecurringModel?: "O" | "M";
|
|
21
|
+
providerFlow?: PaymentProviderFlow;
|
|
22
|
+
customerRef?: string;
|
|
23
|
+
paymentMethodRef?: string;
|
|
24
|
+
saveForFutureUse?: boolean;
|
|
25
|
+
clientId?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface PaymentExecutionResult extends PaymentExecutionBaseResult {
|
|
28
|
+
nextActionKind: PaymentNextActionKind | null;
|
|
29
|
+
redirectUrl?: string | null;
|
|
30
|
+
clientSecret?: string | null;
|
|
31
|
+
customerProviderRef?: string | null;
|
|
32
|
+
paymentMethodRef?: string | null;
|
|
33
|
+
webhookExpected?: boolean;
|
|
34
|
+
recoveryRequired?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface PaymentExecutionPort {
|
|
37
|
+
validateInput?(input: PaymentExecutionInput): void;
|
|
38
|
+
execute(input: PaymentExecutionInput): Promise<PaymentExecutionResult>;
|
|
39
|
+
}
|
|
40
|
+
export type PaymentWebhookEventType = "payment.succeeded" | "payment.failed" | "payment.refunded" | "payment.requires_action" | "payment.disputed" | "setup.succeeded" | "setup.failed" | "setup.requires_action";
|
|
41
|
+
export interface CanonicalPaymentEvent {
|
|
42
|
+
provider_event_id: string;
|
|
43
|
+
event_type: PaymentWebhookEventType;
|
|
44
|
+
payment_provider_id: string;
|
|
45
|
+
amount_minor?: number;
|
|
46
|
+
next_action_kind?: PaymentNextActionKind;
|
|
47
|
+
raw_payload: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
export interface NormalizedProviderPaymentWebhook {
|
|
50
|
+
provider: string;
|
|
51
|
+
providerEventId: string;
|
|
52
|
+
eventType: PaymentWebhookEventType;
|
|
53
|
+
providerPaymentId: string;
|
|
54
|
+
paymentIntentId?: string | null;
|
|
55
|
+
paymentAttemptId?: string | null;
|
|
56
|
+
amountMinor?: number | null;
|
|
57
|
+
currency?: string | null;
|
|
58
|
+
occurredAt: string;
|
|
59
|
+
refundKind?: "full" | "partial" | null;
|
|
60
|
+
rawPayload: Record<string, unknown>;
|
|
61
|
+
reusableMethod?: {
|
|
62
|
+
clientId: string;
|
|
63
|
+
subscriptionId?: string | null;
|
|
64
|
+
providerCustomerRef?: string | null;
|
|
65
|
+
providerMethodRef: string;
|
|
66
|
+
providerMandateRef?: string | null;
|
|
67
|
+
methodKind: "card";
|
|
68
|
+
status: "active";
|
|
69
|
+
consentSnapshot?: Record<string, unknown>;
|
|
70
|
+
} | null;
|
|
71
|
+
}
|
|
72
|
+
export interface ProviderReconciliationStatus {
|
|
73
|
+
status: "succeeded" | "failed" | "pending" | "unknown";
|
|
74
|
+
providerStatus: string;
|
|
75
|
+
occurredAt: string | null;
|
|
76
|
+
failureReason: string | null;
|
|
77
|
+
amountMinor: number | null;
|
|
78
|
+
currency: string | null;
|
|
79
|
+
rawPayload: Record<string, unknown>;
|
|
80
|
+
}
|
|
81
|
+
export interface ClaimedPaymentAttempt {
|
|
82
|
+
paymentAttemptId: string;
|
|
83
|
+
paymentIntentId: string;
|
|
84
|
+
paymentId: string;
|
|
85
|
+
orderId: string;
|
|
86
|
+
subscriptionId: string | null;
|
|
87
|
+
subscriptionCycleId: string | null;
|
|
88
|
+
provider: string;
|
|
89
|
+
providerPaymentId: string | null;
|
|
90
|
+
providerAttemptId: string | null;
|
|
91
|
+
providerSessionId: string | null;
|
|
92
|
+
attemptStatus: string;
|
|
93
|
+
intentStatus: string;
|
|
94
|
+
amountMinor: number;
|
|
95
|
+
currency: string;
|
|
96
|
+
orderMode: "one_time" | "subscription_cycle";
|
|
97
|
+
cycleRetryAttempt: number;
|
|
98
|
+
cycleNextRetryAt: string | null;
|
|
99
|
+
localUpdatedAt: string;
|
|
100
|
+
}
|
|
101
|
+
export type ProviderRecoveryAction = {
|
|
102
|
+
kind: "redirect";
|
|
103
|
+
url: string;
|
|
104
|
+
};
|
|
105
|
+
export interface PaymentProviderRecoveryProvider {
|
|
106
|
+
resolvePaymentReference(input: {
|
|
107
|
+
providerAttemptId: string | null;
|
|
108
|
+
intentProviderPaymentId: string | null;
|
|
109
|
+
providerSessionId: string | null;
|
|
110
|
+
}): string | null;
|
|
111
|
+
readPayment(input: {
|
|
112
|
+
providerPaymentId: string;
|
|
113
|
+
attempt?: ClaimedPaymentAttempt;
|
|
114
|
+
purpose?: ProviderReadPurpose;
|
|
115
|
+
}): Promise<ProviderReconciliationStatus>;
|
|
116
|
+
readRecoveryPayment(input: {
|
|
117
|
+
providerPaymentId: string;
|
|
118
|
+
attempt: Pick<ClaimedPaymentAttempt, "amountMinor" | "currency">;
|
|
119
|
+
purpose?: ProviderReadPurpose;
|
|
120
|
+
}): Promise<{
|
|
121
|
+
status: ProviderReconciliationStatus;
|
|
122
|
+
identityMatches: boolean;
|
|
123
|
+
configuredMoneyMatches: boolean;
|
|
124
|
+
manualReviewRequired: boolean;
|
|
125
|
+
clientAction: ProviderRecoveryAction | null;
|
|
126
|
+
}>;
|
|
127
|
+
closePayment?(input: {
|
|
128
|
+
providerPaymentId: string;
|
|
129
|
+
}): Promise<ProviderReconciliationStatus>;
|
|
130
|
+
findPaymentByLocalIntent?(input: {
|
|
131
|
+
attempt: ClaimedPaymentAttempt;
|
|
132
|
+
}): Promise<"found" | "absent">;
|
|
133
|
+
}
|
|
134
|
+
export interface TpayTransientProviderInput {
|
|
135
|
+
provider: "tpay";
|
|
136
|
+
flow: "blik_one_time" | "blik_one_click" | "pbl_one_time" | "blik_recurring_activation" | "recurring_charge";
|
|
137
|
+
blikToken?: string;
|
|
138
|
+
channelId?: string;
|
|
139
|
+
recurringModel?: "O" | "M";
|
|
140
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PAYMENT_NEXT_ACTION_KINDS = ["redirect", "3ds_challenge", "sca_required"];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { PaymentAdapter } from "../contracts/index.js";
|
|
2
|
+
export { tpayPaymentCapabilities } from "./capabilities.js";
|
|
3
|
+
import { type TpayJwsCertificateResolver } from "./webhookJwsVerifier.js";
|
|
4
|
+
/** Explicit host configuration. clock returns UTC seconds; imports perform no I/O. */
|
|
5
|
+
export interface TpayPaymentAdapterConfig {
|
|
6
|
+
http: {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
clientId: string;
|
|
9
|
+
clientSecret: string;
|
|
10
|
+
fetchImpl: (url: string, init?: {
|
|
11
|
+
method?: string;
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
body?: string;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}) => Promise<{
|
|
16
|
+
ok: boolean;
|
|
17
|
+
status: number;
|
|
18
|
+
json(): Promise<unknown>;
|
|
19
|
+
text(): Promise<string>;
|
|
20
|
+
}>;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
};
|
|
23
|
+
notification: {
|
|
24
|
+
rootCertificatePem: string;
|
|
25
|
+
/** HTTPS certificate directory ending in a slash; resolver must enforce redirect/egress policy. */
|
|
26
|
+
allowedCertificateUrlPrefix: string;
|
|
27
|
+
resolveCertificate: TpayJwsCertificateResolver;
|
|
28
|
+
/** Seven days by default: generous retry allowance assumed until provider schedule is established. */
|
|
29
|
+
maxNotificationAgeSeconds?: number;
|
|
30
|
+
/** Maximum future signed issue time relative to receipt; defaults to 300 seconds. */
|
|
31
|
+
maxClockSkewSeconds?: number;
|
|
32
|
+
};
|
|
33
|
+
urls: {
|
|
34
|
+
notificationUrl: string;
|
|
35
|
+
successUrl: string;
|
|
36
|
+
errorUrl: string;
|
|
37
|
+
};
|
|
38
|
+
modelOActivationEnabled: boolean;
|
|
39
|
+
mode: "live" | "test";
|
|
40
|
+
clock?: () => number;
|
|
41
|
+
}
|
|
42
|
+
/** Construction explicitly selects this adapter; the host owns account activation and I/O policy. */
|
|
43
|
+
export declare function createTpayPaymentAdapter(config: TpayPaymentAdapterConfig): PaymentAdapter;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { createHash, X509Certificate } from "node:crypto";
|
|
2
|
+
import { classifyPaymentFailure, isPaymentRetryAdviceCode } from "@openlup/core/payment";
|
|
3
|
+
import { tpayPaymentCapabilities } from "./capabilities.js";
|
|
4
|
+
export { tpayPaymentCapabilities } from "./capabilities.js";
|
|
5
|
+
import { createTpayHttpClient } from "./tpayHttpClient.js";
|
|
6
|
+
import { createTpayPaymentExecutionAdapter } from "./tpayPaymentExecutionAdapter.js";
|
|
7
|
+
import { ProviderAttemptPreDispatchError } from "./preparedProviderAttempt.js";
|
|
8
|
+
import { createTpayPaymentReconciliationProvider } from "./tpayPaymentReconciliationProvider.js";
|
|
9
|
+
import { verifyTpayJwsSignature } from "./webhookJwsVerifier.js";
|
|
10
|
+
import { normalizeTpayWebhook, toNormalizedProviderTpayWebhook } from "./webhookNormalizer.js";
|
|
11
|
+
const refusal = (reason) => ({ kind: "refusal", reason });
|
|
12
|
+
/** Construction explicitly selects this adapter; the host owns account activation and I/O policy. */
|
|
13
|
+
export function createTpayPaymentAdapter(config) {
|
|
14
|
+
const capabilities = tpayPaymentCapabilities;
|
|
15
|
+
const notification = config.notification;
|
|
16
|
+
if (!notification?.rootCertificatePem || typeof notification.resolveCertificate !== "function")
|
|
17
|
+
throw new Error("Required certificate configuration missing");
|
|
18
|
+
const prefix = new URL(notification.allowedCertificateUrlPrefix);
|
|
19
|
+
if (prefix.protocol !== "https:" || prefix.username || prefix.password || prefix.search || prefix.hash
|
|
20
|
+
|| !notification.allowedCertificateUrlPrefix.endsWith("/"))
|
|
21
|
+
throw new Error("Invalid certificate URL prefix");
|
|
22
|
+
const root = new X509Certificate(notification.rootCertificatePem);
|
|
23
|
+
const maxAge = notification.maxNotificationAgeSeconds ?? 7 * 24 * 60 * 60;
|
|
24
|
+
const maxClockSkew = notification.maxClockSkewSeconds ?? 300;
|
|
25
|
+
if (!Number.isFinite(maxAge) || maxAge < 0 || !Number.isFinite(maxClockSkew) || maxClockSkew < 0 || !["test", "live"].includes(config.mode)
|
|
26
|
+
|| typeof config.http.fetchImpl !== "function" || !config.http.clientId || !config.http.clientSecret
|
|
27
|
+
|| typeof config.modelOActivationEnabled !== "boolean")
|
|
28
|
+
throw new Error("Invalid payment configuration");
|
|
29
|
+
for (const url of [config.http.baseUrl, ...Object.values(config.urls)]) {
|
|
30
|
+
const parsed = new URL(url);
|
|
31
|
+
if (parsed.protocol !== "https:" || parsed.username || parsed.password)
|
|
32
|
+
throw new Error("Invalid payment URL");
|
|
33
|
+
}
|
|
34
|
+
const now = config.clock ?? (() => Date.now() / 1000);
|
|
35
|
+
const client = createTpayHttpClient({ ...config.http, now: () => now() * 1000 });
|
|
36
|
+
const execution = createTpayPaymentExecutionAdapter({ client, ...config.urls,
|
|
37
|
+
modelOActivationEnabled: config.modelOActivationEnabled });
|
|
38
|
+
const reconciliation = createTpayPaymentReconciliationProvider(client);
|
|
39
|
+
const validateHostInput = (input) => {
|
|
40
|
+
if (input.mode !== config.mode || !["initial", "recovery", "renewal-unattended"].includes(input.purpose)
|
|
41
|
+
|| !capabilities.supportedCurrencies.includes(input.money.currency))
|
|
42
|
+
throw new Error("Unsupported payment input");
|
|
43
|
+
};
|
|
44
|
+
const donorInput = (input) => {
|
|
45
|
+
if (!input.attemptRef || !input.intentRef || !input.fingerprint || !input.providerIdempotencyKey
|
|
46
|
+
|| !Number.isSafeInteger(input.money.amountMinor) || input.money.amountMinor <= 0
|
|
47
|
+
|| !input.payer?.email || (input.payer.name !== undefined && typeof input.payer.name !== "string")) {
|
|
48
|
+
throw new Error("Invalid payment input");
|
|
49
|
+
}
|
|
50
|
+
const renewal = input.purpose === "renewal-unattended";
|
|
51
|
+
const family = input.methodSelection?.family;
|
|
52
|
+
if (renewal ? !input.storedMethod?.reference || input.storedMethod.mandate.recurringModel !== "O"
|
|
53
|
+
|| input.customerAuthorization !== undefined
|
|
54
|
+
: family !== "bank-redirect" && family !== "one-time-code")
|
|
55
|
+
throw new Error("Unsupported payment flow");
|
|
56
|
+
if (!renewal && (family === "bank-redirect" ? !input.methodSelection?.channelRef || input.customerAuthorization !== undefined
|
|
57
|
+
: !input.customerAuthorization))
|
|
58
|
+
throw new Error("Required payment context missing");
|
|
59
|
+
return {
|
|
60
|
+
idempotencyKey: input.attemptRef, providerIdempotencyKey: input.providerIdempotencyKey,
|
|
61
|
+
providerRequestFingerprint: input.fingerprint, paymentIntentId: input.intentRef, orderRef: input.intentRef,
|
|
62
|
+
amountMinor: input.money.amountMinor, currency: input.money.currency,
|
|
63
|
+
mode: renewal ? "subscription_cycle" : "one_time",
|
|
64
|
+
payer: { email: input.payer.email, name: input.payer.name?.trim() ? input.payer.name : "Customer" },
|
|
65
|
+
...(renewal ? { providerFlow: "recurring_charge", paymentMethodRef: input.storedMethod.reference,
|
|
66
|
+
paymentMethodAliasType: "PAYID", paymentMethodRecurringModel: "O" } : {
|
|
67
|
+
transientProviderInput: family === "bank-redirect"
|
|
68
|
+
? { provider: "tpay", flow: "pbl_one_time", channelId: input.methodSelection.channelRef }
|
|
69
|
+
: { provider: "tpay", flow: "blik_one_time", blikToken: input.customerAuthorization },
|
|
70
|
+
}),
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const validateInput = (input) => {
|
|
74
|
+
validateHostInput(input);
|
|
75
|
+
execution.validateInput?.(donorInput(input));
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
capabilities,
|
|
79
|
+
attemptPort: {
|
|
80
|
+
validateInput,
|
|
81
|
+
async execute(input) {
|
|
82
|
+
validateHostInput(input);
|
|
83
|
+
let prepared;
|
|
84
|
+
try {
|
|
85
|
+
prepared = donorInput(input);
|
|
86
|
+
execution.validateInput?.(prepared);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
const reason = { code: "fix_and_retry_customer_action", hints: ["dataInvalid"] };
|
|
90
|
+
return { outcome: { kind: "decline", refusal: reason },
|
|
91
|
+
dispatch: { kind: "notDispatched", evidence: { exact: true, phase: "before-dispatch", reason } }, diagnostics: {} };
|
|
92
|
+
}
|
|
93
|
+
let result;
|
|
94
|
+
try {
|
|
95
|
+
result = await execution.execute(prepared);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (!(error instanceof ProviderAttemptPreDispatchError))
|
|
99
|
+
throw error;
|
|
100
|
+
// A proven document refusal needs corrected data; other trusted no-charge failures can retry.
|
|
101
|
+
const hints = error.phase === "response_decode"
|
|
102
|
+
&& error.code === "tpay_request_refused" && error.dispatchState === "refused" ? ["dataInvalid"] : ["transient"];
|
|
103
|
+
const reason = { code: classifyPaymentFailure({ neutralReasonHints: hints }).failureClass, hints };
|
|
104
|
+
return { outcome: { kind: "decline", refusal: reason },
|
|
105
|
+
dispatch: { kind: "notDispatched", evidence: { exact: true, phase: "before-dispatch", reason } }, diagnostics: {} };
|
|
106
|
+
}
|
|
107
|
+
// Only these neutral facts cross the boundary; donor payloads and transient payer/authorization stay local.
|
|
108
|
+
return {
|
|
109
|
+
outcome: result.providerDecline ? { kind: "decline", refusal: neutralDecline(result.providerDecline) }
|
|
110
|
+
: { kind: "attempt", status: result.attemptStatus },
|
|
111
|
+
// API transaction ID is required for readback; notification titles remain distinct.
|
|
112
|
+
dispatch: result.providerSessionId ? { kind: "dispatched", providerPaymentRef: result.providerSessionId } : { kind: "unknown" },
|
|
113
|
+
...(result.nextActionKind === "redirect" && result.redirectUrl
|
|
114
|
+
? { handoff: { kind: "redirect", url: result.redirectUrl } } : {}),
|
|
115
|
+
diagnostics: { webhookExpected: result.webhookExpected === true, recoveryRequired: result.recoveryRequired === true },
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
verifyPort: {
|
|
120
|
+
async verifyAndNormalize(input) {
|
|
121
|
+
const header = Object.entries(input.headers).find(([key]) => key.toLowerCase() === "x-jws-signature")?.[1];
|
|
122
|
+
if (typeof header !== "string")
|
|
123
|
+
return refusal("signature");
|
|
124
|
+
const rawBody = Buffer.from(input.rawBody).toString("utf8");
|
|
125
|
+
if (!Buffer.from(rawBody).equals(Buffer.from(input.rawBody)))
|
|
126
|
+
return refusal("malformed");
|
|
127
|
+
try {
|
|
128
|
+
const current = now() * 1000;
|
|
129
|
+
if (!validAt(root, current))
|
|
130
|
+
return refusal("signature");
|
|
131
|
+
const verified = await verifyTpayJwsSignature({ rawBody, signatureHeader: header,
|
|
132
|
+
rootCertificatePem: notification.rootCertificatePem, allowedX5uPrefix: notification.allowedCertificateUrlPrefix,
|
|
133
|
+
resolveCertificate: async (url) => {
|
|
134
|
+
const pem = await notification.resolveCertificate(url);
|
|
135
|
+
if (!validAt(new X509Certificate(pem), current))
|
|
136
|
+
throw new Error("Certificate outside validity window");
|
|
137
|
+
return pem;
|
|
138
|
+
} });
|
|
139
|
+
if (!verified)
|
|
140
|
+
return refusal("signature");
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return refusal("signature");
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
// Only a signed protected-header issued-at assertion supplies notification time.
|
|
147
|
+
const protectedHeader = JSON.parse(Buffer.from(header.split(".")[0], "base64url").toString("utf8"));
|
|
148
|
+
const asserted = protectedHeader.iat;
|
|
149
|
+
if (asserted !== undefined && (typeof asserted !== "number" || !Number.isFinite(asserted)
|
|
150
|
+
|| !Number.isFinite(new Date(asserted * 1000).getTime())))
|
|
151
|
+
return refusal("malformed");
|
|
152
|
+
const occurred = typeof asserted === "number" ? new Date(asserted * 1000).toISOString() : null;
|
|
153
|
+
if (occurred !== null) {
|
|
154
|
+
const received = Date.parse(input.receivedAt) / 1000;
|
|
155
|
+
if (!Number.isFinite(received))
|
|
156
|
+
return refusal("malformed");
|
|
157
|
+
if (received - asserted > maxAge || asserted - received > maxClockSkew)
|
|
158
|
+
return refusal("stale");
|
|
159
|
+
}
|
|
160
|
+
return mapVerifiedTpayEvent(rawBody, occurred, config.mode, capabilities.supportedCurrencies);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return refusal("malformed");
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
readbackPort: {
|
|
168
|
+
async readPayment(input) {
|
|
169
|
+
const base = { identity: input.identity, expectedMoney: input.expectedMoney, purpose: input.purpose };
|
|
170
|
+
if (input.identity.provider !== "tpay" || !input.identity.providerPaymentRef)
|
|
171
|
+
return { ...base, kind: "unsupported" };
|
|
172
|
+
try {
|
|
173
|
+
const reading = await reconciliation.readRecoveryPayment({
|
|
174
|
+
providerPaymentId: input.identity.providerPaymentRef, purpose: input.purpose, attempt: input.expectedMoney,
|
|
175
|
+
});
|
|
176
|
+
const observed = reading.status;
|
|
177
|
+
if (!reading.identityMatches || !reading.configuredMoneyMatches || reading.manualReviewRequired
|
|
178
|
+
|| !Number.isSafeInteger(input.expectedMoney.amountMinor) || input.expectedMoney.amountMinor <= 0
|
|
179
|
+
|| observed.status === "unknown")
|
|
180
|
+
return unknown(base);
|
|
181
|
+
// Success and nonzero refused money must match; unpaid refusals retain the configured obligation.
|
|
182
|
+
if ((observed.status === "succeeded" || (observed.status === "failed" && observed.amountMinor !== null && observed.amountMinor !== 0))
|
|
183
|
+
&& (observed.amountMinor !== input.expectedMoney.amountMinor
|
|
184
|
+
|| observed.currency !== input.expectedMoney.currency.toUpperCase()))
|
|
185
|
+
return unknown(base);
|
|
186
|
+
return { ...base, kind: observed.status, observedMoney: { amountMinor: input.expectedMoney.amountMinor,
|
|
187
|
+
currency: input.expectedMoney.currency.toUpperCase() },
|
|
188
|
+
providerOccurredAt: observed.occurredAt };
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return unknown(base);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function neutralDecline(decline) {
|
|
198
|
+
const hints = decline.neutralReasonHints ?? [];
|
|
199
|
+
return { code: classifyPaymentFailure({ adviceCode: decline.adviceCode, neutralReasonHints: hints }).failureClass, hints,
|
|
200
|
+
...(decline.adviceCode && isPaymentRetryAdviceCode(decline.adviceCode) ? { adviceCode: decline.adviceCode } : {}) };
|
|
201
|
+
}
|
|
202
|
+
function validAt(certificate, instant) {
|
|
203
|
+
return Number.isFinite(instant) && Date.parse(certificate.validFrom) <= instant && instant <= Date.parse(certificate.validTo);
|
|
204
|
+
}
|
|
205
|
+
function unknown(base) {
|
|
206
|
+
return { ...base, kind: "unknown", reason: "indeterminate" };
|
|
207
|
+
}
|
|
208
|
+
/** All donor outcomes outside the current neutral union are refused here. */
|
|
209
|
+
function mapTpayOutcome(event) {
|
|
210
|
+
switch (event) {
|
|
211
|
+
case "payment.succeeded": return "succeeded";
|
|
212
|
+
case "payment.failed": return "failed";
|
|
213
|
+
case "payment.requires_action": return "pending";
|
|
214
|
+
case "payment.refunded": return "refunded";
|
|
215
|
+
default: return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function mapVerifiedTpayEvent(rawBody, occurred, mode, supportedCurrencies) {
|
|
219
|
+
const webhook = normalizeTpayWebhook(rawBody);
|
|
220
|
+
const event = webhook.event;
|
|
221
|
+
const payload = event.raw_payload;
|
|
222
|
+
if (payload.testMode !== null && payload.testMode !== undefined
|
|
223
|
+
&& payload.testMode !== (mode === "test" ? "1" : "0"))
|
|
224
|
+
return refusal("unsupported-event");
|
|
225
|
+
const identity = { provider: "tpay", providerOccurredAt: occurred };
|
|
226
|
+
let evidence;
|
|
227
|
+
if (payload.eventKind === "blik_alias") {
|
|
228
|
+
const kinds = {
|
|
229
|
+
method_registered: "registered", method_updated: "updated", method_revoked: "revoked", method_expired: "expired",
|
|
230
|
+
};
|
|
231
|
+
const outcome = kinds[webhook.methodLifecycleKind ?? ""];
|
|
232
|
+
if (!outcome)
|
|
233
|
+
return refusal("unsupported-event");
|
|
234
|
+
const lifecycle = occurred !== null ? toNormalizedProviderTpayWebhook(webhook, occurred).methodLifecycle : null;
|
|
235
|
+
evidence = { ...identity, kind: "method", outcome, providerEventId: `${event.provider_event_id}:${outcome}`,
|
|
236
|
+
methodRef: event.payment_provider_id, setupRef: null, intentRef: event.payment_intent_id ?? null,
|
|
237
|
+
...(lifecycle ? { methodLifecycle: lifecycle } : {}) };
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
const outcome = mapTpayOutcome(event.event_type);
|
|
241
|
+
if (!outcome)
|
|
242
|
+
return refusal("unsupported-event");
|
|
243
|
+
const amount = event.amount_minor;
|
|
244
|
+
const reportedCurrency = payload.currency;
|
|
245
|
+
if (reportedCurrency == null && supportedCurrencies.length !== 1)
|
|
246
|
+
return refusal("malformed");
|
|
247
|
+
// Currency binds to the adapter's declared single supported currency because
|
|
248
|
+
// attempts in any other currency are refused before dispatch; this is not a silent default.
|
|
249
|
+
const currency = reportedCurrency ?? supportedCurrencies[0];
|
|
250
|
+
const money = Number.isSafeInteger(amount) && amount > 0 && typeof currency === "string" && /^[A-Za-z]{3}$/u.test(currency)
|
|
251
|
+
? { amountMinor: amount, currency: currency.toUpperCase() } : null;
|
|
252
|
+
const originalAmount = typeof payload.transactionAmount === "string"
|
|
253
|
+
? Math.round(Number(payload.transactionAmount.replace(",", ".")) * 100) : NaN;
|
|
254
|
+
if (outcome === "refunded" && (!money || !Number.isSafeInteger(originalAmount) || originalAmount <= 0))
|
|
255
|
+
return refusal("malformed");
|
|
256
|
+
evidence = { ...identity, kind: "money", providerEventId: `${event.payment_provider_id}:${outcome}`,
|
|
257
|
+
providerPaymentRef: event.payment_provider_id, intentRef: event.payment_intent_id ?? null, attemptRef: null, outcome, money,
|
|
258
|
+
...(outcome === "refunded" && money ? { refund: { kind: "full-reversal", reason: "chargeback",
|
|
259
|
+
operationId: `${event.payment_provider_id}:chargeback`, originalPaymentMoney: { amountMinor: originalAmount, currency: money.currency } } } : {}),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return { ...evidence, fingerprint: createHash("sha256").update(JSON.stringify(evidence)).digest("hex"),
|
|
263
|
+
providerAuditPayload: payload };
|
|
264
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PaymentRecoveryMethod, PaymentRecoveryOperation } from "@openlup/core/payment";
|
|
2
|
+
export type FailureObservationSource = "execution" | "webhook" | "readback";
|
|
3
|
+
export type FailureObservationDisposition = "present" | "absent" | "unreadable" | "read_failed" | "read_timeout";
|
|
4
|
+
/** Protected provider facts, not a customer message or a retry-policy input. */
|
|
5
|
+
export interface PaymentFailureEvidence {
|
|
6
|
+
version: 1;
|
|
7
|
+
source: FailureObservationSource;
|
|
8
|
+
disposition: FailureObservationDisposition;
|
|
9
|
+
refusalVerified: boolean;
|
|
10
|
+
providerPaymentId: string | null;
|
|
11
|
+
providerChargeId: string | null;
|
|
12
|
+
code: string | null;
|
|
13
|
+
declineCode: string | null;
|
|
14
|
+
adviceCode: string | null;
|
|
15
|
+
adviceOrigin: "provider" | "inferred" | "unknown";
|
|
16
|
+
operation: PaymentRecoveryOperation | null;
|
|
17
|
+
method: PaymentRecoveryMethod | null;
|
|
18
|
+
methodSource: "provider" | "execution_contract" | "unknown";
|
|
19
|
+
}
|
|
20
|
+
/** Bounded machine tokens only. Free-form response fields never cross this seam. */
|
|
21
|
+
export declare function evidenceToken(value: unknown): string | null;
|
|
22
|
+
export declare function evidenceRecord(value: unknown): Record<string, unknown>;
|
|
23
|
+
/** Explicit construction prevents accidental payload expansion by object spreading. */
|
|
24
|
+
export declare function serializeFailureEvidence(input: PaymentFailureEvidence): PaymentFailureEvidence;
|
|
25
|
+
/** Persist beside legacy fields; never merge the diagnostic codes into them. */
|
|
26
|
+
export declare function failureEvidencePayload(evidence: PaymentFailureEvidence | null | undefined): Record<string, unknown>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Bounded machine tokens only. Free-form response fields never cross this seam. */
|
|
2
|
+
export function evidenceToken(value) {
|
|
3
|
+
return typeof value === "string" && /^[a-zA-Z0-9_:.-]{1,96}$/.test(value) ? value : null;
|
|
4
|
+
}
|
|
5
|
+
export function evidenceRecord(value) {
|
|
6
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
7
|
+
? value : {};
|
|
8
|
+
}
|
|
9
|
+
/** Explicit construction prevents accidental payload expansion by object spreading. */
|
|
10
|
+
export function serializeFailureEvidence(input) {
|
|
11
|
+
const method = input.method;
|
|
12
|
+
const kind = evidenceToken(method?.kind);
|
|
13
|
+
const recoveryMethodKey = evidenceToken(method?.recoveryMethodKey);
|
|
14
|
+
return {
|
|
15
|
+
version: 1,
|
|
16
|
+
source: input.source,
|
|
17
|
+
disposition: input.disposition,
|
|
18
|
+
refusalVerified: input.refusalVerified,
|
|
19
|
+
providerPaymentId: evidenceToken(input.providerPaymentId),
|
|
20
|
+
providerChargeId: evidenceToken(input.providerChargeId),
|
|
21
|
+
code: evidenceToken(input.code),
|
|
22
|
+
declineCode: evidenceToken(input.declineCode),
|
|
23
|
+
adviceCode: evidenceToken(input.adviceCode),
|
|
24
|
+
adviceOrigin: input.adviceOrigin,
|
|
25
|
+
operation: input.operation,
|
|
26
|
+
method: kind && recoveryMethodKey && method ? { kind, recoveryMethodKey, interaction: method.interaction } : null,
|
|
27
|
+
methodSource: kind && recoveryMethodKey ? input.methodSource : "unknown",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Persist beside legacy fields; never merge the diagnostic codes into them. */
|
|
31
|
+
export function failureEvidencePayload(evidence) {
|
|
32
|
+
return evidence ? { failureEvidence: serializeFailureEvidence(evidence) } : {};
|
|
33
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface TrustedProviderAttemptPreDispatchFailure {
|
|
2
|
+
phase: "oauth" | "transaction_dispatch";
|
|
3
|
+
code: "tpay_oauth_failed" | "tpay_oauth_timeout" | "tpay_oauth_invalid_response" | "tpay_request_encode_failed" | "tpay_request_deadline_exhausted";
|
|
4
|
+
dispatchState: "not_dispatched";
|
|
5
|
+
}
|
|
6
|
+
/** Either proof the provider holds no transaction: the POST was never invoked, or it was and the
|
|
7
|
+
* provider refused the request document. Two closed shapes, so neither absorbs the other. */
|
|
8
|
+
export type TrustedProviderAttemptNoChargeFailure = TrustedProviderAttemptPreDispatchFailure | {
|
|
9
|
+
phase: "response_decode";
|
|
10
|
+
code: "tpay_request_refused";
|
|
11
|
+
dispatchState: "refused";
|
|
12
|
+
};
|
|
13
|
+
export declare class ProviderAttemptPreDispatchError extends Error {
|
|
14
|
+
readonly phase: TrustedProviderAttemptNoChargeFailure["phase"];
|
|
15
|
+
readonly code: TrustedProviderAttemptNoChargeFailure["code"];
|
|
16
|
+
readonly dispatchState: TrustedProviderAttemptNoChargeFailure["dispatchState"];
|
|
17
|
+
paymentAttemptId: string | null;
|
|
18
|
+
retryCount: number;
|
|
19
|
+
constructor(failure: TrustedProviderAttemptNoChargeFailure, paymentAttemptId?: string | null);
|
|
20
|
+
withPaymentAttempt(paymentAttemptId: string): this;
|
|
21
|
+
withRetryCount(retryCount: number): this;
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class ProviderAttemptPreDispatchError extends Error {
|
|
2
|
+
phase;
|
|
3
|
+
code;
|
|
4
|
+
dispatchState;
|
|
5
|
+
paymentAttemptId;
|
|
6
|
+
retryCount = 0;
|
|
7
|
+
constructor(failure, paymentAttemptId = null) {
|
|
8
|
+
const kind = failure.dispatchState === "refused" ? "provider_refused_pre_transaction" : "provider_pre_dispatch_failed";
|
|
9
|
+
super(`${kind}:${failure.phase}:${failure.code}`);
|
|
10
|
+
this.name = "ProviderAttemptPreDispatchError";
|
|
11
|
+
this.phase = failure.phase;
|
|
12
|
+
this.code = failure.code;
|
|
13
|
+
this.dispatchState = failure.dispatchState;
|
|
14
|
+
this.paymentAttemptId = paymentAttemptId;
|
|
15
|
+
}
|
|
16
|
+
withPaymentAttempt(paymentAttemptId) {
|
|
17
|
+
this.paymentAttemptId = paymentAttemptId;
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
withRetryCount(retryCount) {
|
|
21
|
+
this.retryCount = retryCount;
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const TPAY_REQUEST_ID = /^[a-f0-9]{20}$/;
|
|
2
|
+
const LOGGABLE_PROVIDER_ERROR_CODES = new Set([
|
|
3
|
+
"bank_group_does_not_exist",
|
|
4
|
+
"blik_token_not_present",
|
|
5
|
+
"cannot_create_transaction",
|
|
6
|
+
"channel_passed_with_group",
|
|
7
|
+
"channel_unavailable",
|
|
8
|
+
"channel_unavailable_for_source",
|
|
9
|
+
"configuration_error",
|
|
10
|
+
"country_not_valid",
|
|
11
|
+
"crc_not_unique",
|
|
12
|
+
"field_required",
|
|
13
|
+
"general",
|
|
14
|
+
"incorrect_content_type",
|
|
15
|
+
"internal_server_error",
|
|
16
|
+
"invalid_request_body",
|
|
17
|
+
"is_required",
|
|
18
|
+
"no_payment_data",
|
|
19
|
+
"not_array",
|
|
20
|
+
"not_natural_int",
|
|
21
|
+
"not_string",
|
|
22
|
+
"not_valid",
|
|
23
|
+
"partner_cannot_create_transaction",
|
|
24
|
+
"payment_failed",
|
|
25
|
+
"too_low",
|
|
26
|
+
"transaction_blocked",
|
|
27
|
+
"transaction_lock",
|
|
28
|
+
"ts:channel:amount_outside_range",
|
|
29
|
+
"ts:channel:both_channel_and_group_specified",
|
|
30
|
+
"ts:channel:channel_blocked",
|
|
31
|
+
"ts:channel:channel_disabled",
|
|
32
|
+
"ts:channel:channel_disabled_for_pos",
|
|
33
|
+
"ts:channel:collect_prohibited",
|
|
34
|
+
"ts:channel:group_disabled_for_pos",
|
|
35
|
+
"ts:channel:not_available",
|
|
36
|
+
"ts:channel:not_found",
|
|
37
|
+
"ts:channel:source_prohibited",
|
|
38
|
+
"ts:channel:surcharge_prohibited",
|
|
39
|
+
"ts:country:not_valid",
|
|
40
|
+
"ts:merchant:disabled",
|
|
41
|
+
"ts:merchant:is_partner",
|
|
42
|
+
"ts:merchant:not_found",
|
|
43
|
+
"ts:payer:phone:not_valid",
|
|
44
|
+
"ts:pos:not_found",
|
|
45
|
+
"ts:pos:result_url_not_overridable",
|
|
46
|
+
"ts:request:no_ip",
|
|
47
|
+
"ts:transaction:not_inserted",
|
|
48
|
+
"ts:transaction:payer_data_not_inserted",
|
|
49
|
+
"ts:transaction:payer_info_not_inserted",
|
|
50
|
+
"unexpected_field",
|
|
51
|
+
"unsupported_blik_token",
|
|
52
|
+
"url_override_is_disabled",
|
|
53
|
+
"wrong_alias",
|
|
54
|
+
"wrong_autopayment_alias",
|
|
55
|
+
"wrong_model_data_combination",
|
|
56
|
+
]);
|
|
57
|
+
const LOGGABLE_PROVIDER_FIELD_NAMES = new Set([
|
|
58
|
+
"amount",
|
|
59
|
+
"currency",
|
|
60
|
+
"description",
|
|
61
|
+
"hiddenDescription",
|
|
62
|
+
"payer.email",
|
|
63
|
+
"payer.name",
|
|
64
|
+
"payer.phone",
|
|
65
|
+
"payer.address",
|
|
66
|
+
"payer.city",
|
|
67
|
+
"payer.country",
|
|
68
|
+
"payer.postalCode",
|
|
69
|
+
// This list carries FIELD NAMES ONLY - `ProviderAttemptFailureDiagnostic` has
|
|
70
|
+
// no field that holds a provider value - so these two entries log the literal
|
|
71
|
+
// strings, never a user agent or an IP address. Their absence is why the
|
|
72
|
+
// 2026-08-28 BLIK outage logged `fieldNames: []` and stayed unattributed for
|
|
73
|
+
// days: `readTpayHttpFailure` filters Tpay's returned `fieldName` through this
|
|
74
|
+
// set, so an unlisted name is silently dropped.
|
|
75
|
+
"payer.ip",
|
|
76
|
+
"payer.userAgent",
|
|
77
|
+
"pay.groupId",
|
|
78
|
+
"pay.channelId",
|
|
79
|
+
"pay.blikPaymentData.blikToken",
|
|
80
|
+
"pay.blikPaymentData.type",
|
|
81
|
+
"pay.blikPaymentData.aliases.value",
|
|
82
|
+
"pay.blikPaymentData.aliases.type",
|
|
83
|
+
"callbacks.notification.url",
|
|
84
|
+
"callbacks.payerUrls.success",
|
|
85
|
+
"callbacks.payerUrls.error",
|
|
86
|
+
]);
|
|
87
|
+
export function loggableProviderRequestId(value) {
|
|
88
|
+
return typeof value === "string" && TPAY_REQUEST_ID.test(value);
|
|
89
|
+
}
|
|
90
|
+
export function loggableProviderErrorCode(value) {
|
|
91
|
+
return typeof value === "string" && LOGGABLE_PROVIDER_ERROR_CODES.has(value);
|
|
92
|
+
}
|
|
93
|
+
export function loggableProviderFieldName(value) {
|
|
94
|
+
return typeof value === "string" && LOGGABLE_PROVIDER_FIELD_NAMES.has(value);
|
|
95
|
+
}
|