@openlup/platform 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -231
- package/README.md +172 -6
- package/dist/composition/composeBundle.d.ts +6 -0
- package/dist/composition/composeBundle.js +2 -0
- package/dist/composition/index.js +1 -0
- package/dist/payments/adapterConformance.d.ts +8 -0
- package/dist/payments/adapterConformance.js +109 -0
- package/dist/payments/bindings.d.ts +3 -0
- package/dist/payments/bindings.js +17 -0
- package/dist/payments/checkoutActivePaymentActionResolver.d.ts +7 -0
- package/dist/payments/checkoutActivePaymentActionResolver.js +77 -0
- package/dist/payments/checkoutRecoveryPaymentResolver.d.ts +8 -0
- package/dist/payments/checkoutRecoveryPaymentResolver.js +87 -0
- package/dist/payments/contracts/adapter.d.ts +113 -0
- package/dist/payments/contracts/adapter.js +1 -0
- package/dist/payments/contracts/conformance.d.ts +155 -0
- package/dist/payments/contracts/conformance.js +1 -0
- package/dist/payments/contracts/evidence.d.ts +157 -0
- package/dist/payments/contracts/evidence.js +1 -0
- package/dist/payments/contracts/index.d.ts +10 -0
- package/dist/payments/contracts/index.js +1 -0
- package/dist/payments/contracts/preparedAttempt.d.ts +69 -0
- package/dist/payments/contracts/preparedAttempt.js +1 -0
- package/dist/payments/contracts/reconciliation.d.ts +303 -0
- package/dist/payments/contracts/reconciliation.js +1 -0
- package/dist/payments/contracts/repair.d.ts +92 -0
- package/dist/payments/contracts/repair.js +1 -0
- package/dist/payments/contracts/truth.d.ts +92 -0
- package/dist/payments/contracts/truth.js +1 -0
- package/dist/payments/eventConformanceRunner.d.ts +12 -0
- package/dist/payments/eventConformanceRunner.js +125 -0
- package/dist/payments/index.d.ts +16 -0
- package/dist/payments/index.js +16 -0
- package/dist/payments/paymentAttemptSilenceWindow.d.ts +10 -0
- package/dist/payments/paymentAttemptSilenceWindow.js +19 -0
- package/dist/payments/paymentMethodLifecycle.d.ts +11 -0
- package/dist/payments/paymentMethodLifecycle.js +37 -0
- package/dist/payments/paymentProviderReconciliationAbsenceReopen.d.ts +7 -0
- package/dist/payments/paymentProviderReconciliationAbsenceReopen.js +100 -0
- package/dist/payments/paymentProviderReconciliationEvidence.d.ts +36 -0
- package/dist/payments/paymentProviderReconciliationEvidence.js +91 -0
- package/dist/payments/paymentProviderReconciliationWorker.d.ts +4 -0
- package/dist/payments/paymentProviderReconciliationWorker.js +245 -0
- package/dist/payments/paymentTruth.d.ts +8 -0
- package/dist/payments/paymentTruth.js +125 -0
- package/dist/payments/paymentVerifyNowHandler.d.ts +27 -0
- package/dist/payments/paymentVerifyNowHandler.js +70 -0
- package/dist/payments/paymentVerifyNowService.d.ts +23 -0
- package/dist/payments/paymentVerifyNowService.js +93 -0
- package/dist/payments/paymentWebhookPostProcessing.d.ts +22 -0
- package/dist/payments/paymentWebhookPostProcessing.js +15 -0
- package/dist/payments/postgresPaymentTruth.d.ts +10 -0
- package/dist/payments/postgresPaymentTruth.js +126 -0
- package/dist/payments/preparedAttempt.d.ts +6 -0
- package/dist/payments/preparedAttempt.js +70 -0
- package/dist/payments/preparedAttemptFinalization.d.ts +5 -0
- package/dist/payments/preparedAttemptFinalization.js +12 -0
- package/dist/payments/schemaCapabilities.d.ts +10 -0
- package/dist/payments/schemaCapabilities.js +10 -0
- package/dist/payments/sql.d.ts +20 -0
- package/dist/payments/sql.js +6 -0
- package/dist/payments/stripe/classifyDecline.d.ts +46 -0
- package/dist/payments/stripe/classifyDecline.js +58 -0
- package/dist/payments/stripe/declineFailureHints.d.ts +46 -0
- package/dist/payments/stripe/declineFailureHints.js +87 -0
- package/dist/payments/stripe/donorContracts.d.ts +120 -0
- package/dist/payments/stripe/donorContracts.js +1 -0
- package/dist/payments/stripe/executionDecline.d.ts +53 -0
- package/dist/payments/stripe/executionDecline.js +107 -0
- package/dist/payments/stripe/index.d.ts +16 -0
- package/dist/payments/stripe/index.js +239 -0
- package/dist/payments/stripe/intentMapper.d.ts +26 -0
- package/dist/payments/stripe/intentMapper.js +51 -0
- package/dist/payments/stripe/methodFacts.d.ts +10 -0
- package/dist/payments/stripe/methodFacts.js +12 -0
- package/dist/payments/stripe/paymentFailureEvidence.d.ts +26 -0
- package/dist/payments/stripe/paymentFailureEvidence.js +33 -0
- package/dist/payments/stripe/stripeFailureEvidence.d.ts +41 -0
- package/dist/payments/stripe/stripeFailureEvidence.js +82 -0
- package/dist/payments/stripe/stripeIntentConfirmationEvidence.d.ts +28 -0
- package/dist/payments/stripe/stripeIntentConfirmationEvidence.js +32 -0
- package/dist/payments/stripe/stripePaymentReconciliationProvider.d.ts +35 -0
- package/dist/payments/stripe/stripePaymentReconciliationProvider.js +222 -0
- package/dist/payments/stripe/stripeRefundEvidence.d.ts +8 -0
- package/dist/payments/stripe/stripeRefundEvidence.js +17 -0
- package/dist/payments/stripe/stripeSandboxPaymentExecutionAdapter.d.ts +45 -0
- package/dist/payments/stripe/stripeSandboxPaymentExecutionAdapter.js +220 -0
- package/dist/payments/stripe/stripeWebhookNormalizer.d.ts +14 -0
- package/dist/payments/stripe/stripeWebhookNormalizer.js +243 -0
- package/dist/payments/stripe/webhookSignature.d.ts +14 -0
- package/dist/payments/stripe/webhookSignature.js +43 -0
- package/dist/payments/tpay/blikRecurringMapper.d.ts +37 -0
- package/dist/payments/tpay/blikRecurringMapper.js +24 -0
- package/dist/payments/tpay/capabilities.d.ts +3 -0
- package/dist/payments/tpay/capabilities.js +18 -0
- package/dist/payments/tpay/classifyDecline.d.ts +46 -0
- package/dist/payments/tpay/classifyDecline.js +58 -0
- package/dist/payments/tpay/declineFailureHints.d.ts +72 -0
- package/dist/payments/tpay/declineFailureHints.js +37 -0
- package/dist/payments/tpay/donorContracts.d.ts +140 -0
- package/dist/payments/tpay/donorContracts.js +1 -0
- package/dist/payments/tpay/index.d.ts +43 -0
- package/dist/payments/tpay/index.js +264 -0
- package/dist/payments/tpay/paymentFailureEvidence.d.ts +26 -0
- package/dist/payments/tpay/paymentFailureEvidence.js +33 -0
- package/dist/payments/tpay/preparedProviderAttempt.d.ts +22 -0
- package/dist/payments/tpay/preparedProviderAttempt.js +24 -0
- package/dist/payments/tpay/providerAttemptFailureDiagnostic.d.ts +3 -0
- package/dist/payments/tpay/providerAttemptFailureDiagnostic.js +95 -0
- package/dist/payments/tpay/tpayCreateTransactionInput.d.ts +25 -0
- package/dist/payments/tpay/tpayCreateTransactionInput.js +128 -0
- package/dist/payments/tpay/tpayDeclineEvidence.d.ts +126 -0
- package/dist/payments/tpay/tpayDeclineEvidence.js +177 -0
- package/dist/payments/tpay/tpayDispatchFailure.d.ts +60 -0
- package/dist/payments/tpay/tpayDispatchFailure.js +121 -0
- package/dist/payments/tpay/tpayFailureEvidence.d.ts +20 -0
- package/dist/payments/tpay/tpayFailureEvidence.js +61 -0
- package/dist/payments/tpay/tpayHttpClient.d.ts +52 -0
- package/dist/payments/tpay/tpayHttpClient.js +185 -0
- package/dist/payments/tpay/tpayHttpFailure.d.ts +57 -0
- package/dist/payments/tpay/tpayHttpFailure.js +143 -0
- package/dist/payments/tpay/tpayPaymentExecutionAdapter.d.ts +20 -0
- package/dist/payments/tpay/tpayPaymentExecutionAdapter.js +249 -0
- package/dist/payments/tpay/tpayPaymentReconciliationProvider.d.ts +5 -0
- package/dist/payments/tpay/tpayPaymentReconciliationProvider.js +244 -0
- package/dist/payments/tpay/tpayTransactionPayload.d.ts +120 -0
- package/dist/payments/tpay/tpayTransactionPayload.js +96 -0
- package/dist/payments/tpay/wallClockTime.d.ts +31 -0
- package/dist/payments/tpay/wallClockTime.js +57 -0
- package/dist/payments/tpay/webhookJwsVerifier.d.ts +17 -0
- package/dist/payments/tpay/webhookJwsVerifier.js +51 -0
- package/dist/payments/tpay/webhookNormalizer.d.ts +54 -0
- package/dist/payments/tpay/webhookNormalizer.js +200 -0
- package/dist/payments/verifiedEvidenceProjection.d.ts +12 -0
- package/dist/payments/verifiedEvidenceProjection.js +166 -0
- package/dist/payments/verifiedIngress.d.ts +119 -0
- package/dist/payments/verifiedIngress.js +220 -0
- package/package.json +22 -2
- package/release-gates.json +64 -3
- package/sql/20260810180000_payment_settlement_rail.sql +574 -0
- package/sql/20260815150000_payment_truth_reconciliation.sql +479 -0
- package/sql/20260826100000_late_payment_shifts_the_subscription_cycle.sql +215 -0
- package/src/composition/composeBundle.ts +9 -0
- package/src/composition/index.ts +1 -0
- package/src/payments/adapterConformance.ts +118 -0
- package/src/payments/bindings.ts +17 -0
- package/src/payments/checkoutActivePaymentActionResolver.ts +97 -0
- package/src/payments/checkoutRecoveryPaymentResolver.ts +108 -0
- package/src/payments/contracts/adapter.ts +96 -0
- package/src/payments/contracts/conformance.ts +171 -0
- package/src/payments/contracts/evidence.ts +142 -0
- package/src/payments/contracts/index.ts +54 -0
- package/src/payments/contracts/preparedAttempt.ts +86 -0
- package/src/payments/contracts/reconciliation.ts +296 -0
- package/src/payments/contracts/repair.ts +95 -0
- package/src/payments/contracts/truth.ts +108 -0
- package/src/payments/eventConformanceRunner.ts +149 -0
- package/src/payments/index.ts +21 -0
- package/src/payments/paymentAttemptSilenceWindow.ts +26 -0
- package/src/payments/paymentMethodLifecycle.ts +50 -0
- package/src/payments/paymentProviderReconciliationAbsenceReopen.ts +130 -0
- package/src/payments/paymentProviderReconciliationEvidence.ts +117 -0
- package/src/payments/paymentProviderReconciliationWorker.ts +291 -0
- package/src/payments/paymentTruth.ts +131 -0
- package/src/payments/paymentVerifyNowHandler.ts +94 -0
- package/src/payments/paymentVerifyNowService.ts +125 -0
- package/src/payments/paymentWebhookPostProcessing.ts +36 -0
- package/src/payments/postgresPaymentTruth.ts +149 -0
- package/src/payments/preparedAttempt.ts +90 -0
- package/src/payments/preparedAttemptFinalization.ts +13 -0
- package/src/payments/schemaCapabilities.ts +10 -0
- package/src/payments/sql.ts +6 -0
- package/src/payments/stripe/classifyDecline.ts +62 -0
- package/src/payments/stripe/declineFailureHints.ts +89 -0
- package/src/payments/stripe/donorContracts.ts +87 -0
- package/src/payments/stripe/executionDecline.ts +126 -0
- package/src/payments/stripe/index.ts +235 -0
- package/src/payments/stripe/intentMapper.ts +58 -0
- package/src/payments/stripe/methodFacts.ts +21 -0
- package/src/payments/stripe/paymentFailureEvidence.ts +58 -0
- package/src/payments/stripe/stripeFailureEvidence.ts +115 -0
- package/src/payments/stripe/stripeIntentConfirmationEvidence.ts +34 -0
- package/src/payments/stripe/stripePaymentReconciliationProvider.ts +263 -0
- package/src/payments/stripe/stripeRefundEvidence.ts +19 -0
- package/src/payments/stripe/stripeSandboxPaymentExecutionAdapter.ts +283 -0
- package/src/payments/stripe/stripeWebhookNormalizer.ts +277 -0
- package/src/payments/stripe/webhookSignature.ts +55 -0
- package/src/payments/tpay/blikRecurringMapper.ts +42 -0
- package/src/payments/tpay/capabilities.ts +21 -0
- package/src/payments/tpay/classifyDecline.ts +62 -0
- package/src/payments/tpay/declineFailureHints.ts +90 -0
- package/src/payments/tpay/donorContracts.ts +104 -0
- package/src/payments/tpay/index.ts +257 -0
- package/src/payments/tpay/paymentFailureEvidence.ts +58 -0
- package/src/payments/tpay/preparedProviderAttempt.ts +38 -0
- package/src/payments/tpay/providerAttemptFailureDiagnostic.ts +99 -0
- package/src/payments/tpay/tpayCreateTransactionInput.ts +158 -0
- package/src/payments/tpay/tpayDeclineEvidence.ts +233 -0
- package/src/payments/tpay/tpayDispatchFailure.ts +186 -0
- package/src/payments/tpay/tpayFailureEvidence.ts +75 -0
- package/src/payments/tpay/tpayHttpClient.ts +290 -0
- package/src/payments/tpay/tpayHttpFailure.ts +186 -0
- package/src/payments/tpay/tpayPaymentExecutionAdapter.ts +297 -0
- package/src/payments/tpay/tpayPaymentReconciliationProvider.ts +285 -0
- package/src/payments/tpay/tpayTransactionPayload.ts +207 -0
- package/src/payments/tpay/wallClockTime.ts +57 -0
- package/src/payments/tpay/webhookJwsVerifier.ts +82 -0
- package/src/payments/tpay/webhookNormalizer.ts +279 -0
- package/src/payments/verifiedEvidenceProjection.ts +179 -0
- package/src/payments/verifiedIngress.ts +317 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { failureEvidencePayload } from "./paymentFailureEvidence.js";
|
|
2
|
+
import { declineFromError, declinedExecutionFacts } from "./executionDecline.js";
|
|
3
|
+
import { noIssuerWasAsked } from "./stripeIntentConfirmationEvidence.js";
|
|
4
|
+
/** The one place this adapter spells its own provider identity. */
|
|
5
|
+
export const PROVIDER_KIND = "stripe";
|
|
6
|
+
export function createStripeSandboxPaymentExecutionAdapter({ client, defaultFlow = "one_time_payment", }) {
|
|
7
|
+
return {
|
|
8
|
+
async execute(input) {
|
|
9
|
+
const flow = stripeFlow(input.providerFlow ?? defaultFlow);
|
|
10
|
+
// A reusable-mandate PaymentIntent (setup_future_usage) requires a Stripe
|
|
11
|
+
// Customer. Ensure-or-create one for the client when none was passed.
|
|
12
|
+
let customerRef = input.customerRef;
|
|
13
|
+
if (!customerRef && input.saveForFutureUse && input.clientId && client.ensureCustomer) {
|
|
14
|
+
customerRef = await client.ensureCustomer({ clientId: input.clientId, metadata: { clientId: input.clientId } },
|
|
15
|
+
// A provider attempt is per click; a Stripe Customer is per OpenLup
|
|
16
|
+
// client. A stable key avoids orphan Customers when checkout recovery
|
|
17
|
+
// is reloaded before the first method webhook stores the reference.
|
|
18
|
+
{ idempotencyKey: `openlup:stripe:customer:${input.clientId}` });
|
|
19
|
+
}
|
|
20
|
+
const created = flow === "setup_reusable_method"
|
|
21
|
+
? await client.createSetupIntent({
|
|
22
|
+
customer: customerRef,
|
|
23
|
+
paymentMethod: input.paymentMethodRef,
|
|
24
|
+
usage: "off_session",
|
|
25
|
+
metadata: metadataFor(input, flow),
|
|
26
|
+
}, { idempotencyKey: input.providerIdempotencyKey })
|
|
27
|
+
: await client.createPaymentIntent({
|
|
28
|
+
amount: input.amountMinor,
|
|
29
|
+
currency: input.currency.toLowerCase(),
|
|
30
|
+
customer: customerRef,
|
|
31
|
+
paymentMethod: input.paymentMethodRef,
|
|
32
|
+
offSession: flow === "off_session_payment" ? true : undefined,
|
|
33
|
+
setupFutureUsage: input.saveForFutureUse ? "off_session" : undefined,
|
|
34
|
+
metadata: metadataFor(input, flow),
|
|
35
|
+
}, { idempotencyKey: input.providerIdempotencyKey }).catch((error) => {
|
|
36
|
+
// An off-session charge is confirmed inside this call, so the payer's
|
|
37
|
+
// issuer can refuse it here and nowhere else. That refusal is a FACT,
|
|
38
|
+
// not an ambiguous call: rethrowing it makes the renewal orchestrator
|
|
39
|
+
// record an indeterminate outcome, which applies no result, opens no
|
|
40
|
+
// dunning case and tells the customer nothing. Every other error class
|
|
41
|
+
// — network, auth, config, rate limit — keeps throwing, because after
|
|
42
|
+
// those the charge may still have been accepted and only the
|
|
43
|
+
// fail-closed path is safe.
|
|
44
|
+
const decline = flow === "off_session_payment" ? declineFromError(error) : null;
|
|
45
|
+
if (!decline)
|
|
46
|
+
throw error;
|
|
47
|
+
return decline;
|
|
48
|
+
});
|
|
49
|
+
if ("decline" in created)
|
|
50
|
+
return { ...executionResultBase(input, flow), ...declinedExecutionFacts(created) };
|
|
51
|
+
const intent = created;
|
|
52
|
+
return {
|
|
53
|
+
...executionResultBase(input, flow),
|
|
54
|
+
providerAttemptId: intent.id,
|
|
55
|
+
providerSessionId: intent.id,
|
|
56
|
+
attemptStatus: attemptStatusFromStripe(intent),
|
|
57
|
+
nextActionKind: nextActionKindFromStripe(intent),
|
|
58
|
+
// `clientSecret` is the single canonical exit for the FE to confirm with
|
|
59
|
+
// the provider's browser SDK. It MUST NOT be persisted or duplicated
|
|
60
|
+
// into `responsePayload` (the latter is logged); the consumer reads it
|
|
61
|
+
// once from the top-level result and discards.
|
|
62
|
+
clientSecret: intent.client_secret ?? null,
|
|
63
|
+
customerProviderRef: intent.customer ?? null,
|
|
64
|
+
paymentMethodRef: intent.payment_method ?? null,
|
|
65
|
+
webhookExpected: true,
|
|
66
|
+
recoveryRequired: intent.status === "requires_action" && flow === "off_session_payment",
|
|
67
|
+
responsePayload: {
|
|
68
|
+
providerStatus: intent.status,
|
|
69
|
+
providerAttemptId: intent.id,
|
|
70
|
+
providerSessionIdPresent: Boolean(intent.client_secret),
|
|
71
|
+
latestChargePresent: Boolean(intent.latest_charge),
|
|
72
|
+
customerRefPresent: Boolean(intent.customer),
|
|
73
|
+
paymentMethodRefPresent: Boolean(intent.payment_method),
|
|
74
|
+
...failureEvidencePayload(intent.diagnosticFailureEvidence),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function parseStripePaymentWebhookEvent(rawPayload) {
|
|
81
|
+
const eventId = readString(rawPayload, "id");
|
|
82
|
+
const eventType = readString(rawPayload, "type");
|
|
83
|
+
const data = readObject(rawPayload, "data");
|
|
84
|
+
const object = readObject(data, "object");
|
|
85
|
+
const providerPaymentId = readString(object, "id");
|
|
86
|
+
const amount = readOptionalNumber(object, "amount_received") ?? readOptionalNumber(object, "amount");
|
|
87
|
+
const currency = readOptionalString(object, "currency")?.toUpperCase();
|
|
88
|
+
return {
|
|
89
|
+
provider_event_id: eventId,
|
|
90
|
+
event_type: canonicalStripeEventType(eventType, object),
|
|
91
|
+
payment_provider_id: providerPaymentId,
|
|
92
|
+
amount_minor: amount,
|
|
93
|
+
next_action_kind: nextActionKindFromStripe({
|
|
94
|
+
status: readOptionalString(object, "status") ?? "",
|
|
95
|
+
next_action: readOptionalObject(object, "next_action"),
|
|
96
|
+
}) ?? undefined,
|
|
97
|
+
raw_payload: {
|
|
98
|
+
provider: PROVIDER_KIND,
|
|
99
|
+
eventId,
|
|
100
|
+
eventType,
|
|
101
|
+
providerPaymentId,
|
|
102
|
+
amountMinor: amount,
|
|
103
|
+
currency,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** The facts every result of this adapter carries, whatever the provider answered. */
|
|
108
|
+
function executionResultBase(input, flow) {
|
|
109
|
+
return {
|
|
110
|
+
provider: PROVIDER_KIND,
|
|
111
|
+
requestPayload: {
|
|
112
|
+
providerIdempotencyKey: input.providerIdempotencyKey,
|
|
113
|
+
providerRequestFingerprint: input.providerRequestFingerprint,
|
|
114
|
+
providerFlow: flow,
|
|
115
|
+
amountMinor: input.amountMinor,
|
|
116
|
+
currency: input.currency,
|
|
117
|
+
orderRef: input.orderRef,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function stripeFlow(flow) {
|
|
122
|
+
if (flow === "one_time_payment" || flow === "setup_reusable_method" || flow === "off_session_payment") {
|
|
123
|
+
return flow;
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`Unsupported Stripe provider flow: ${flow}`);
|
|
126
|
+
}
|
|
127
|
+
function metadataFor(input, flow) {
|
|
128
|
+
const metadata = {
|
|
129
|
+
source: "openlup.payment-control",
|
|
130
|
+
paymentIntentId: input.paymentIntentId,
|
|
131
|
+
orderRef: input.orderRef,
|
|
132
|
+
providerRequestFingerprint: input.providerRequestFingerprint,
|
|
133
|
+
providerFlow: flow,
|
|
134
|
+
};
|
|
135
|
+
if (input.clientId)
|
|
136
|
+
metadata.clientId = input.clientId;
|
|
137
|
+
if (input.mode === "subscription_cycle")
|
|
138
|
+
metadata.mode = input.mode;
|
|
139
|
+
return metadata;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* What the durable attempt row is allowed to claim about this execution.
|
|
143
|
+
*
|
|
144
|
+
* Takes the intent, not just its status, because the two states sharing
|
|
145
|
+
* `requires_payment_method` differ only in what else is on the object: a fresh
|
|
146
|
+
* intent the payer never confirmed, and one an issuer has just refused (which
|
|
147
|
+
* carries a payment error and usually a charge).
|
|
148
|
+
*
|
|
149
|
+
* `processing` claims money is moving, and a catch-all used to reach it — so a
|
|
150
|
+
* never-confirmed intent was recorded as a charge in flight, and a payer was
|
|
151
|
+
* told their payment was already processing when no issuer had been asked.
|
|
152
|
+
* `sent_to_provider` is the truth: the request reached the provider, no outcome
|
|
153
|
+
* is known. It belongs to every live-attempt set the platform keeps, so the
|
|
154
|
+
* correction costs no watchdog coverage, and it is fail-closed at the
|
|
155
|
+
* provider-attempt admission gate exactly as `processing` is — admission does
|
|
156
|
+
* not widen by one state.
|
|
157
|
+
*
|
|
158
|
+
* The catch-all stays `processing` on purpose: an unrecognised provider status is
|
|
159
|
+
* not evidence that nothing happened.
|
|
160
|
+
*/
|
|
161
|
+
function attemptStatusFromStripe(intent) {
|
|
162
|
+
if (intent.status === "requires_action" || intent.status === "requires_confirmation") {
|
|
163
|
+
return "requires_action";
|
|
164
|
+
}
|
|
165
|
+
if (noIssuerWasAsked(intent))
|
|
166
|
+
return "sent_to_provider";
|
|
167
|
+
return "processing";
|
|
168
|
+
}
|
|
169
|
+
function nextActionKindFromStripe(intent) {
|
|
170
|
+
if (intent.status !== "requires_action" && intent.status !== "requires_confirmation")
|
|
171
|
+
return null;
|
|
172
|
+
if (intent.next_action?.type === "redirect_to_url")
|
|
173
|
+
return "redirect";
|
|
174
|
+
if (intent.next_action?.type === "use_stripe_sdk")
|
|
175
|
+
return "3ds_challenge";
|
|
176
|
+
return "sca_required";
|
|
177
|
+
}
|
|
178
|
+
function canonicalStripeEventType(stripeEventType, object) {
|
|
179
|
+
if (stripeEventType === "payment_intent.succeeded")
|
|
180
|
+
return "payment.succeeded";
|
|
181
|
+
if (stripeEventType === "payment_intent.payment_failed" || stripeEventType === "payment_intent.canceled") {
|
|
182
|
+
return "payment.failed";
|
|
183
|
+
}
|
|
184
|
+
if (stripeEventType === "payment_intent.requires_action")
|
|
185
|
+
return "payment.requires_action";
|
|
186
|
+
if (stripeEventType === "charge.refunded")
|
|
187
|
+
return "payment.refunded";
|
|
188
|
+
if (stripeEventType.startsWith("charge.dispute."))
|
|
189
|
+
return "payment.disputed";
|
|
190
|
+
const status = readOptionalString(object, "status");
|
|
191
|
+
if (status === "requires_action")
|
|
192
|
+
return "payment.requires_action";
|
|
193
|
+
throw new Error(`Unsupported Stripe webhook event: ${stripeEventType}`);
|
|
194
|
+
}
|
|
195
|
+
function readObject(value, key) {
|
|
196
|
+
const raw = value[key];
|
|
197
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
198
|
+
throw new Error(`Stripe payload missing object ${key}`);
|
|
199
|
+
}
|
|
200
|
+
return raw;
|
|
201
|
+
}
|
|
202
|
+
function readOptionalObject(value, key) {
|
|
203
|
+
const raw = value[key];
|
|
204
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
205
|
+
}
|
|
206
|
+
function readString(value, key) {
|
|
207
|
+
const raw = value[key];
|
|
208
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
209
|
+
throw new Error(`Stripe payload missing string ${key}`);
|
|
210
|
+
}
|
|
211
|
+
return raw;
|
|
212
|
+
}
|
|
213
|
+
function readOptionalString(value, key) {
|
|
214
|
+
const raw = value[key];
|
|
215
|
+
return typeof raw === "string" && raw.length > 0 ? raw : undefined;
|
|
216
|
+
}
|
|
217
|
+
function readOptionalNumber(value, key) {
|
|
218
|
+
const raw = value[key];
|
|
219
|
+
return typeof raw === "number" ? raw : undefined;
|
|
220
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ProviderWebhookWithMethodLifecycle } from "./methodFacts.js";
|
|
2
|
+
/**
|
|
3
|
+
* Convert a verified raw Stripe webhook payload into the plural handler's
|
|
4
|
+
* `NormalizedProviderPaymentWebhook` shape.
|
|
5
|
+
*
|
|
6
|
+
* Reusable method extraction (Wave C): populated for `setup_intent.succeeded`
|
|
7
|
+
* always, and for `payment_intent.succeeded` when `setup_future_usage` was
|
|
8
|
+
* requested at PaymentIntent creation. Requires `metadata.clientId` to be set
|
|
9
|
+
* (the adapter does this when the runtime caller passes `clientId` through
|
|
10
|
+
* `PaymentExecutionInput`). When metadata is missing, `reusableMethod` stays
|
|
11
|
+
* null and the plural handler skips the method-ref upsert — safe degradation
|
|
12
|
+
* rather than crashing the webhook.
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeStripeWebhookEvent(rawPayload: Record<string, unknown>): ProviderWebhookWithMethodLifecycle;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { stripeWebhookPayload } from "./stripeFailureEvidence.js";
|
|
2
|
+
import { stripeRefundEvidence } from "./stripeRefundEvidence.js";
|
|
3
|
+
import { expiryInstantFromMonthYear } from "@openlup/core/payment";
|
|
4
|
+
import { methodFactSnapshotKeys } from "./methodFacts.js";
|
|
5
|
+
const PROVIDER_KIND = "stripe";
|
|
6
|
+
const METHOD_LIFECYCLE_EVENTS = {
|
|
7
|
+
"payment_method.attached": { kind: "method_registered", canonical: "setup.succeeded" },
|
|
8
|
+
"payment_method.automatically_updated": { kind: "method_updated", canonical: "setup.succeeded" },
|
|
9
|
+
"payment_method.detached": { kind: "method_revoked", canonical: "setup.failed" },
|
|
10
|
+
};
|
|
11
|
+
const SUPPORTED_EVENT_TYPES = new Set([
|
|
12
|
+
"payment.requires_action",
|
|
13
|
+
"payment.succeeded",
|
|
14
|
+
"payment.failed",
|
|
15
|
+
"payment.refunded",
|
|
16
|
+
"payment.disputed",
|
|
17
|
+
"setup.succeeded",
|
|
18
|
+
"setup.failed",
|
|
19
|
+
"setup.requires_action",
|
|
20
|
+
]);
|
|
21
|
+
/**
|
|
22
|
+
* Convert a verified raw Stripe webhook payload into the plural handler's
|
|
23
|
+
* `NormalizedProviderPaymentWebhook` shape.
|
|
24
|
+
*
|
|
25
|
+
* Reusable method extraction (Wave C): populated for `setup_intent.succeeded`
|
|
26
|
+
* always, and for `payment_intent.succeeded` when `setup_future_usage` was
|
|
27
|
+
* requested at PaymentIntent creation. Requires `metadata.clientId` to be set
|
|
28
|
+
* (the adapter does this when the runtime caller passes `clientId` through
|
|
29
|
+
* `PaymentExecutionInput`). When metadata is missing, `reusableMethod` stays
|
|
30
|
+
* null and the plural handler skips the method-ref upsert — safe degradation
|
|
31
|
+
* rather than crashing the webhook.
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeStripeWebhookEvent(rawPayload) {
|
|
34
|
+
const providerEventId = readString(rawPayload, "id");
|
|
35
|
+
const eventType = readString(rawPayload, "type");
|
|
36
|
+
const data = readObject(rawPayload, "data");
|
|
37
|
+
const object = readObject(data, "object");
|
|
38
|
+
const canonicalEventType = mapStripeEventType(eventType, object);
|
|
39
|
+
const providerPaymentId = resolveLinkagePaymentId(eventType, object);
|
|
40
|
+
// Payment-control compares this against the intent's `amount_cents` (the order
|
|
41
|
+
// total) as a tamper guard, so we want the PaymentIntent's authorized `amount`,
|
|
42
|
+
// not `amount_received`. On a declined/canceled PI Stripe reports
|
|
43
|
+
// `amount_received: 0` while `amount` still holds the full total — and `??`
|
|
44
|
+
// does NOT treat `0` as nullish, so reading `amount_received` first yielded a
|
|
45
|
+
// `0` that tripped the amount-mismatch guard and left every failed event
|
|
46
|
+
// unreconciled (intent stuck `processing`, inventory holds leaked). Prefer
|
|
47
|
+
// `amount`; fall back to `amount_received` only when `amount` is absent.
|
|
48
|
+
const amountMinor = readOptionalNumber(object, "amount") ??
|
|
49
|
+
readOptionalNumber(object, "amount_received") ??
|
|
50
|
+
null;
|
|
51
|
+
const currency = readOptionalString(object, "currency")?.toUpperCase() ?? null;
|
|
52
|
+
const occurredAt = readOccurredAt(rawPayload);
|
|
53
|
+
const reusableMethod = extractReusableMethod(canonicalEventType, object);
|
|
54
|
+
const refund = stripeRefundEvidence(eventType, object);
|
|
55
|
+
return {
|
|
56
|
+
provider: PROVIDER_KIND,
|
|
57
|
+
providerEventId,
|
|
58
|
+
eventType: canonicalEventType,
|
|
59
|
+
providerPaymentId,
|
|
60
|
+
paymentIntentId: null,
|
|
61
|
+
paymentAttemptId: null,
|
|
62
|
+
amountMinor,
|
|
63
|
+
currency,
|
|
64
|
+
occurredAt,
|
|
65
|
+
refundKind: refund.refundKind,
|
|
66
|
+
rawPayload: {
|
|
67
|
+
...stripeWebhookPayload({ eventId: providerEventId, eventType, providerPaymentId, amountMinor, currency }, object),
|
|
68
|
+
...(refund.snapshot ? { stripeRefundSnapshot: refund.snapshot } : {}),
|
|
69
|
+
},
|
|
70
|
+
reusableMethod,
|
|
71
|
+
methodLifecycle: extractMethodLifecycle(eventType, object, providerEventId, occurredAt),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** The neutral transition this delivery reports, or null when it reports none. */
|
|
75
|
+
function extractMethodLifecycle(rawEventType, object, providerEventId, occurredAt) {
|
|
76
|
+
const mapping = METHOD_LIFECYCLE_EVENTS[rawEventType];
|
|
77
|
+
// On a method-family event the webhook object IS the method, so its own id is
|
|
78
|
+
// the reference the stored row is keyed by.
|
|
79
|
+
const providerMethodRef = readOptionalString(object, "id");
|
|
80
|
+
if (!mapping || !providerMethodRef)
|
|
81
|
+
return null;
|
|
82
|
+
const replacement = readMethodFacts(object);
|
|
83
|
+
return { kind: mapping.kind, providerKind: PROVIDER_KIND, providerMethodRef, providerEventId, occurredAt, replacement };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Scheme label, trailing digits and expiry, from whichever of the three shapes
|
|
87
|
+
* the payload carries them in: the method object itself at the top level, an
|
|
88
|
+
* expanded method one level down, or a charge-shaped object's method details.
|
|
89
|
+
* A payload carrying none of them yields null, not a row of empty facts.
|
|
90
|
+
*/
|
|
91
|
+
function readMethodFacts(object) {
|
|
92
|
+
const card = readOptionalObject(object, "card")
|
|
93
|
+
?? readOptionalObject(readOptionalObject(object, "payment_method") ?? {}, "card")
|
|
94
|
+
?? readOptionalObject(readOptionalObject(object, "payment_method_details") ?? {}, "card");
|
|
95
|
+
if (!card)
|
|
96
|
+
return null;
|
|
97
|
+
const schemeLabel = readOptionalString(card, "brand") ?? null;
|
|
98
|
+
const lastDigits = readOptionalString(card, "last4") ?? null;
|
|
99
|
+
const expiresAt = expiryInstantFromMonthYear(readOptionalNumber(card, "exp_month") ?? null, readOptionalNumber(card, "exp_year") ?? null);
|
|
100
|
+
if (!schemeLabel && !lastDigits && !expiresAt)
|
|
101
|
+
return null;
|
|
102
|
+
return { schemeLabel, lastDigits, expiresAt };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolve the provider payment id used to LINK this event to the local payment
|
|
106
|
+
* attempt. Payment-control matches inbound events to an attempt by
|
|
107
|
+
* `commerce_payment_attempts.provider_attempt_id`, which holds the Stripe
|
|
108
|
+
* PaymentIntent id (`pi_…`).
|
|
109
|
+
*
|
|
110
|
+
* For `payment_intent.*` events the webhook object IS the PaymentIntent, so its
|
|
111
|
+
* `id` is already the linkage key. For `charge.*` (incl. `charge.dispute.*`)
|
|
112
|
+
* the object is a Charge (`ch_…`) or Dispute (`dp_…`) whose own id never
|
|
113
|
+
* matches an attempt — but it carries `payment_intent` pointing back at the PI.
|
|
114
|
+
* Using that field is what lets `charge.refunded` / `charge.dispute.created`
|
|
115
|
+
* resolve to the order and drive `applyPaymentResult`; without it refunds and
|
|
116
|
+
* disputes were ingested but silently never applied. Falls back to the object
|
|
117
|
+
* id when `payment_intent` is absent (defensive — keeps the event recordable).
|
|
118
|
+
*/
|
|
119
|
+
function resolveLinkagePaymentId(stripeEventType, object) {
|
|
120
|
+
if (stripeEventType.startsWith("charge.")) {
|
|
121
|
+
const paymentIntentId = readOptionalString(object, "payment_intent");
|
|
122
|
+
if (paymentIntentId)
|
|
123
|
+
return paymentIntentId;
|
|
124
|
+
}
|
|
125
|
+
return readString(object, "id");
|
|
126
|
+
}
|
|
127
|
+
function mapStripeEventType(stripeEventType, object) {
|
|
128
|
+
if (stripeEventType === "payment_intent.succeeded")
|
|
129
|
+
return "payment.succeeded";
|
|
130
|
+
if (stripeEventType === "payment_intent.payment_failed")
|
|
131
|
+
return "payment.failed";
|
|
132
|
+
if (stripeEventType === "payment_intent.canceled")
|
|
133
|
+
return "payment.failed";
|
|
134
|
+
if (stripeEventType === "payment_intent.requires_action")
|
|
135
|
+
return "payment.requires_action";
|
|
136
|
+
if (stripeEventType === "setup_intent.succeeded")
|
|
137
|
+
return "setup.succeeded";
|
|
138
|
+
if (stripeEventType === "setup_intent.setup_failed")
|
|
139
|
+
return "setup.failed";
|
|
140
|
+
if (stripeEventType === "setup_intent.requires_action")
|
|
141
|
+
return "setup.requires_action";
|
|
142
|
+
if (stripeEventType === "charge.refunded")
|
|
143
|
+
return "payment.refunded";
|
|
144
|
+
if (stripeEventType.startsWith("charge.dispute."))
|
|
145
|
+
return "payment.disputed";
|
|
146
|
+
const methodLifecycle = METHOD_LIFECYCLE_EVENTS[stripeEventType];
|
|
147
|
+
if (methodLifecycle)
|
|
148
|
+
return methodLifecycle.canonical;
|
|
149
|
+
const status = readOptionalString(object, "status");
|
|
150
|
+
if (status === "requires_action")
|
|
151
|
+
return "payment.requires_action";
|
|
152
|
+
const guess = `payment.${stripeEventType}`;
|
|
153
|
+
if (SUPPORTED_EVENT_TYPES.has(guess))
|
|
154
|
+
return guess;
|
|
155
|
+
throw new Error(`Unsupported Stripe webhook event: ${stripeEventType}`);
|
|
156
|
+
}
|
|
157
|
+
function extractReusableMethod(canonicalEventType, object) {
|
|
158
|
+
const isSetupSucceeded = canonicalEventType === "setup.succeeded";
|
|
159
|
+
const isPaymentSucceededWithSaveCard = canonicalEventType === "payment.succeeded" &&
|
|
160
|
+
readOptionalString(object, "setup_future_usage") === "off_session";
|
|
161
|
+
if (!isSetupSucceeded && !isPaymentSucceededWithSaveCard)
|
|
162
|
+
return null;
|
|
163
|
+
const customer = readOptionalString(object, "customer");
|
|
164
|
+
// The method arrives as a bare id, or as an expanded object when the payload
|
|
165
|
+
// also carries its facts; both address the same stored method.
|
|
166
|
+
const paymentMethod = readOptionalString(object, "payment_method")
|
|
167
|
+
?? readOptionalString(readOptionalObject(object, "payment_method") ?? {}, "id");
|
|
168
|
+
const metadata = readOptionalObject(object, "metadata");
|
|
169
|
+
const clientId = metadata ? readOptionalString(metadata, "clientId") : undefined;
|
|
170
|
+
const subscriptionId = metadata ? readOptionalString(metadata, "subscriptionId") : undefined;
|
|
171
|
+
const mode = metadata ? readOptionalString(metadata, "mode") : undefined;
|
|
172
|
+
const recoveryCaseId = metadata ? readOptionalString(metadata, "recoveryCaseId") : undefined;
|
|
173
|
+
const metadataSource = metadata ? readOptionalString(metadata, "source") : undefined;
|
|
174
|
+
if (!customer || !paymentMethod || !clientId)
|
|
175
|
+
return null;
|
|
176
|
+
return {
|
|
177
|
+
clientId,
|
|
178
|
+
// W11.7 Wave D-4a — recovery setup-method flow tags `subscriptionId` in
|
|
179
|
+
// SetupIntent metadata so this upsert binds the new method to the
|
|
180
|
+
// subscription the dunning case targets. One-time + initial save-card
|
|
181
|
+
// (Wave C) flows leave the field null because no subscription exists yet.
|
|
182
|
+
subscriptionId: subscriptionId ?? null,
|
|
183
|
+
providerCustomerRef: customer,
|
|
184
|
+
providerMethodRef: paymentMethod,
|
|
185
|
+
providerMandateRef: null,
|
|
186
|
+
methodKind: "card",
|
|
187
|
+
status: "active",
|
|
188
|
+
consentSnapshot: {
|
|
189
|
+
source: "stripe_webhook",
|
|
190
|
+
eventType: canonicalEventType,
|
|
191
|
+
checkoutMode: mode ?? null,
|
|
192
|
+
recoveryCaseId: recoveryCaseId ?? null,
|
|
193
|
+
metadataSource: metadataSource ?? null,
|
|
194
|
+
// Additive facts only, and only when the payload actually carried them.
|
|
195
|
+
// The setup event carries the method's id and nothing else, so this is
|
|
196
|
+
// usually empty there and filled later by the method-family delivery.
|
|
197
|
+
...methodFactSnapshotKeys(readMethodFacts(object)),
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function readOccurredAt(rawPayload) {
|
|
202
|
+
const created = rawPayload.created;
|
|
203
|
+
if (typeof created === "number" && Number.isFinite(created)) {
|
|
204
|
+
return new Date(created * 1000).toISOString();
|
|
205
|
+
}
|
|
206
|
+
if (typeof created === "string") {
|
|
207
|
+
const parsed = new Date(created);
|
|
208
|
+
if (!Number.isNaN(parsed.getTime()))
|
|
209
|
+
return parsed.toISOString();
|
|
210
|
+
}
|
|
211
|
+
// Stripe always sends `created` on event payloads; fall back defensively if
|
|
212
|
+
// missing rather than refusing the event — downstream RPC still records the
|
|
213
|
+
// signature-verified facts.
|
|
214
|
+
return new Date(0).toISOString();
|
|
215
|
+
}
|
|
216
|
+
function readObject(value, key) {
|
|
217
|
+
const raw = value[key];
|
|
218
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
219
|
+
throw new Error(`Stripe payload missing object ${key}`);
|
|
220
|
+
}
|
|
221
|
+
return raw;
|
|
222
|
+
}
|
|
223
|
+
function readOptionalObject(value, key) {
|
|
224
|
+
const raw = value[key];
|
|
225
|
+
return raw && typeof raw === "object" && !Array.isArray(raw)
|
|
226
|
+
? raw
|
|
227
|
+
: null;
|
|
228
|
+
}
|
|
229
|
+
function readString(value, key) {
|
|
230
|
+
const raw = value[key];
|
|
231
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
232
|
+
throw new Error(`Stripe payload missing string ${key}`);
|
|
233
|
+
}
|
|
234
|
+
return raw;
|
|
235
|
+
}
|
|
236
|
+
function readOptionalString(value, key) {
|
|
237
|
+
const raw = value[key];
|
|
238
|
+
return typeof raw === "string" && raw.length > 0 ? raw : undefined;
|
|
239
|
+
}
|
|
240
|
+
function readOptionalNumber(value, key) {
|
|
241
|
+
const raw = value[key];
|
|
242
|
+
return typeof raw === "number" ? raw : undefined;
|
|
243
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const STRIPE_TIMESTAMP_TOLERANCE_SECONDS = 300;
|
|
2
|
+
export declare function verifyStripeSignature(input: {
|
|
3
|
+
rawBody: string;
|
|
4
|
+
signatureHeader: string | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* One or many endpoint secrets to accept. Multi-secret support lets a single
|
|
7
|
+
* route accept signatures from both the dashboard webhook endpoint and a
|
|
8
|
+
* local `stripe listen` CLI tunnel; the signature is accepted if ANY secret
|
|
9
|
+
* verifies. An empty array (or missing single secret) fails closed.
|
|
10
|
+
*/
|
|
11
|
+
endpointSecret: string | readonly string[];
|
|
12
|
+
/** Injectable clock (UTC seconds) for deterministic tests; defaults to now. */
|
|
13
|
+
now?: () => number;
|
|
14
|
+
}): boolean;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
export const STRIPE_TIMESTAMP_TOLERANCE_SECONDS = 300;
|
|
3
|
+
export function verifyStripeSignature(input) {
|
|
4
|
+
const parts = Object.fromEntries((input.signatureHeader ?? "")
|
|
5
|
+
.split(",")
|
|
6
|
+
.map((part) => part.split("="))
|
|
7
|
+
.filter(([key, value]) => key && value));
|
|
8
|
+
const timestamp = parts.t;
|
|
9
|
+
const signature = parts.v1;
|
|
10
|
+
if (!timestamp || !signature)
|
|
11
|
+
return false;
|
|
12
|
+
// Reject replays outside the tolerance window before doing the HMAC work. A
|
|
13
|
+
// valid signature on a stale timestamp (captured-and-replayed event) must not
|
|
14
|
+
// pass; the future-skew bound tolerates small clock drift between Stripe and us.
|
|
15
|
+
const timestampSec = Number(timestamp);
|
|
16
|
+
if (!Number.isFinite(timestampSec))
|
|
17
|
+
return false;
|
|
18
|
+
const now = input.now ?? (() => Math.floor(Date.now() / 1000));
|
|
19
|
+
const ageSec = now() - timestampSec;
|
|
20
|
+
if (ageSec > STRIPE_TIMESTAMP_TOLERANCE_SECONDS)
|
|
21
|
+
return false;
|
|
22
|
+
if (ageSec < -STRIPE_TIMESTAMP_TOLERANCE_SECONDS)
|
|
23
|
+
return false;
|
|
24
|
+
const secrets = Array.isArray(input.endpointSecret)
|
|
25
|
+
? input.endpointSecret
|
|
26
|
+
: input.endpointSecret
|
|
27
|
+
? [input.endpointSecret]
|
|
28
|
+
: [];
|
|
29
|
+
return secrets.some((secret) => {
|
|
30
|
+
if (!secret)
|
|
31
|
+
return false;
|
|
32
|
+
const expected = hmac(`${timestamp}.${input.rawBody}`, secret);
|
|
33
|
+
return timingSafeStringEqual(signature, expected);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function hmac(value, secret) {
|
|
37
|
+
return createHmac("sha256", secret).update(value).digest("hex");
|
|
38
|
+
}
|
|
39
|
+
function timingSafeStringEqual(left, right) {
|
|
40
|
+
const leftBuffer = Buffer.from(left);
|
|
41
|
+
const rightBuffer = Buffer.from(right);
|
|
42
|
+
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface TpayBlikAliasRegistrationInput {
|
|
2
|
+
orderRef: string;
|
|
3
|
+
amountMinor: number;
|
|
4
|
+
currency: string;
|
|
5
|
+
customerEmail: string;
|
|
6
|
+
staticWebhookUrl: string;
|
|
7
|
+
consentRef: string;
|
|
8
|
+
}
|
|
9
|
+
export interface TpayBlikAliasChargeInput {
|
|
10
|
+
orderRef: string;
|
|
11
|
+
payId: string;
|
|
12
|
+
amountMinor: number;
|
|
13
|
+
currency: string;
|
|
14
|
+
staticWebhookUrl: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function buildTpayBlikAliasRegistrationDraft(input: TpayBlikAliasRegistrationInput): {
|
|
17
|
+
provider: string;
|
|
18
|
+
requestKind: string;
|
|
19
|
+
orderRef: string;
|
|
20
|
+
amount: number;
|
|
21
|
+
currency: string;
|
|
22
|
+
customer: {
|
|
23
|
+
email: string;
|
|
24
|
+
};
|
|
25
|
+
webhookUrl: string;
|
|
26
|
+
consentRef: string;
|
|
27
|
+
fallbackRequiredWhenAliasUnavailable: boolean;
|
|
28
|
+
};
|
|
29
|
+
export declare function buildTpayBlikAliasChargeDraft(input: TpayBlikAliasChargeInput): {
|
|
30
|
+
provider: string;
|
|
31
|
+
requestKind: string;
|
|
32
|
+
orderRef: string;
|
|
33
|
+
payId: string;
|
|
34
|
+
amount: number;
|
|
35
|
+
currency: string;
|
|
36
|
+
webhookUrl: string;
|
|
37
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function buildTpayBlikAliasRegistrationDraft(input) {
|
|
2
|
+
return {
|
|
3
|
+
provider: "tpay",
|
|
4
|
+
requestKind: "blik_alias_registration",
|
|
5
|
+
orderRef: input.orderRef,
|
|
6
|
+
amount: input.amountMinor,
|
|
7
|
+
currency: input.currency,
|
|
8
|
+
customer: { email: input.customerEmail },
|
|
9
|
+
webhookUrl: input.staticWebhookUrl,
|
|
10
|
+
consentRef: input.consentRef,
|
|
11
|
+
fallbackRequiredWhenAliasUnavailable: true,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function buildTpayBlikAliasChargeDraft(input) {
|
|
15
|
+
return {
|
|
16
|
+
provider: "tpay",
|
|
17
|
+
requestKind: "blik_alias_charge",
|
|
18
|
+
orderRef: input.orderRef,
|
|
19
|
+
payId: input.payId,
|
|
20
|
+
amount: input.amountMinor,
|
|
21
|
+
currency: input.currency,
|
|
22
|
+
webhookUrl: input.staticWebhookUrl,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PaymentAdapterCapabilities } from "../contracts/index.js";
|
|
2
|
+
/** Bank-redirect, one-time-code and unattended Model O attempts are supported with host payer/method input; account availability remains verification_pending. */
|
|
3
|
+
export declare const tpayPaymentCapabilities: Readonly<PaymentAdapterCapabilities>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Bank-redirect, one-time-code and unattended Model O attempts are supported with host payer/method input; account availability remains verification_pending. */
|
|
2
|
+
export const tpayPaymentCapabilities = Object.freeze({
|
|
3
|
+
providerKind: "tpay",
|
|
4
|
+
captureFlows: Object.freeze([
|
|
5
|
+
Object.freeze({ kind: "bank_redirect", handoff: "redirect", methodFamilies: Object.freeze(["bank-redirect"]) }),
|
|
6
|
+
Object.freeze({ kind: "one_time_code", handoff: "customer-authorization", methodFamilies: Object.freeze(["one-time-code"]) }),
|
|
7
|
+
]),
|
|
8
|
+
requiresStoredMandateEvidence: true,
|
|
9
|
+
assessMandate: snapshot => snapshot.recurringModel === "O" ? { chargeable: true }
|
|
10
|
+
: { chargeable: false, blockReason: "mandate_model_unsupported_for_unattended_charge" },
|
|
11
|
+
unattendedChargeFlow: "recurring_charge",
|
|
12
|
+
payerContext: Object.freeze({ requiresPayerBlock: true, customerRefFallsBackToContactEmail: false }),
|
|
13
|
+
methodHealth: Object.freeze({ requiresCustomerRef: false, requiredMethodKind: "blik_payid", requiresPayerContact: true }),
|
|
14
|
+
terminalOutcomeReporting: Object.freeze({ kind: "attempt_records", silenceBecomesSuspectAfterMinutes: 360 }),
|
|
15
|
+
mandateUpsertIsSubscriptionScoped: true,
|
|
16
|
+
supportedCurrencies: Object.freeze(["PLN"]), unattended: "supported", readback: "supported", close: "unsupported",
|
|
17
|
+
exactAbsence: "unsupported", refundObservation: "supported", refundCreate: "unsupported", authorizationCaptureSplit: "unsupported",
|
|
18
|
+
});
|