@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,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-field maxima Tpay enforces on `payer.*`, read from the provider's own SDK
|
|
3
|
+
* rather than inferred. In `tpay-com/tpay-openapi-php`,
|
|
4
|
+
* `src/Model/Objects/Transactions/Payer.php` binds every accepted wire key to a
|
|
5
|
+
* field model, and each model declares the `$maxLength` the API applies:
|
|
6
|
+
*
|
|
7
|
+
* email `src/Model/Fields/Person/Email.php` 255
|
|
8
|
+
* name `src/Model/Fields/Payer/Name.php` 255
|
|
9
|
+
* ip `src/Model/Fields/Payer/IP.php` 255
|
|
10
|
+
* userAgent `src/Model/Fields/Payer/UserAgent.php` 255
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ The unit is BYTES, not characters: `src/Model/Fields/FieldValidator.php`
|
|
13
|
+
* measures with PHP `strlen()`, which counts bytes. A 255-CHARACTER name in an
|
|
14
|
+
* accented or non-Latin script can be far more than 255 bytes in UTF-8 and would
|
|
15
|
+
* still be refused, so the clamp below counts bytes.
|
|
16
|
+
*
|
|
17
|
+
* An over-long field is refused synchronously with HTTP 400 `not_valid`. On BLIK
|
|
18
|
+
* the code then never reaches the bank and the buyer sees no prompt at all: the
|
|
19
|
+
* Facebook iOS in-app WebView user agent is 271 characters, which cost four
|
|
20
|
+
* orders and two cancelled subscriptions from 2026-08-28.
|
|
21
|
+
*/
|
|
22
|
+
const TPAY_PAYER_MAX_BYTES = {
|
|
23
|
+
email: 255,
|
|
24
|
+
name: 255,
|
|
25
|
+
ip: 255,
|
|
26
|
+
userAgent: 255,
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Fits one payer field inside the provider's documented maximum.
|
|
30
|
+
*
|
|
31
|
+
* ⛔ Truncate, never omit, and for every one of the four keys. `Field.php` runs
|
|
32
|
+
* a `pattern` check only for models that declare one and none of these four
|
|
33
|
+
* does, so a truncated value stays structurally valid; `getRequiredFields()`
|
|
34
|
+
* makes `email` and `name` mandatory, so dropping either only buys a different
|
|
35
|
+
* 400; and `ts:request:no_ip` is a real provider error, so dropping `ip` trades
|
|
36
|
+
* one refusal for another. Truncation also keeps the prefix that a fraud review
|
|
37
|
+
* or a support ticket can still read.
|
|
38
|
+
*
|
|
39
|
+
* Lives here, in the single function that serialises the wire body, so no payer
|
|
40
|
+
* producer can bypass it.
|
|
41
|
+
*/
|
|
42
|
+
function payerFieldWithinLimit(key, value) {
|
|
43
|
+
const maxBytes = TPAY_PAYER_MAX_BYTES[key];
|
|
44
|
+
const bytes = new TextEncoder().encode(value);
|
|
45
|
+
if (bytes.length <= maxBytes)
|
|
46
|
+
return value;
|
|
47
|
+
let end = maxBytes;
|
|
48
|
+
// Never cut inside a code point: UTF-8 continuation bytes are 0b10xxxxxx, so
|
|
49
|
+
// walk back off them rather than emitting a replacement character.
|
|
50
|
+
while (end > 0 && (bytes[end] & 0xc0) === 0x80)
|
|
51
|
+
end -= 1;
|
|
52
|
+
return new TextDecoder().decode(bytes.subarray(0, end));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Builds the `POST /transactions` wire payload.
|
|
56
|
+
*
|
|
57
|
+
* Every BLIK-specific field lives under `pay.blikPaymentData`; `aliases` carries
|
|
58
|
+
* the mandate. `refuseNoPayId` sits beside `aliases` (not inside it), while
|
|
59
|
+
* `recommendedAuthLevel` and `noDelay` sit inside the alias object as siblings of
|
|
60
|
+
* `autopayment` — matching Tpay's SDK object model and verified against
|
|
61
|
+
* production on 2026-07-20.
|
|
62
|
+
*/
|
|
63
|
+
export function buildTpayTransactionPayload(input) {
|
|
64
|
+
const pay = {};
|
|
65
|
+
if (input.groupId)
|
|
66
|
+
pay.groupId = input.groupId;
|
|
67
|
+
if (input.channelId)
|
|
68
|
+
pay.channelId = input.channelId;
|
|
69
|
+
if (input.blikToken || input.alias) {
|
|
70
|
+
pay.blikPaymentData = {
|
|
71
|
+
...(input.blikPaymentType ? { type: input.blikPaymentType } : {}),
|
|
72
|
+
...(input.blikToken ? { blikToken: input.blikToken } : {}),
|
|
73
|
+
...(input.refuseNoPayId ? { refuseNoPayId: true } : {}),
|
|
74
|
+
...(input.alias ? { aliases: input.alias } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
amount: input.amount,
|
|
79
|
+
currency: input.currency,
|
|
80
|
+
description: input.description,
|
|
81
|
+
hiddenDescription: input.hiddenDescription,
|
|
82
|
+
payer: {
|
|
83
|
+
email: payerFieldWithinLimit("email", input.payer.email),
|
|
84
|
+
name: payerFieldWithinLimit("name", input.payer.name),
|
|
85
|
+
...(input.payer.ip ? { ip: payerFieldWithinLimit("ip", input.payer.ip) } : {}),
|
|
86
|
+
...(input.payer.userAgent
|
|
87
|
+
? { userAgent: payerFieldWithinLimit("userAgent", input.payer.userAgent) }
|
|
88
|
+
: {}),
|
|
89
|
+
},
|
|
90
|
+
...(Object.keys(pay).length > 0 ? { pay } : {}),
|
|
91
|
+
callbacks: {
|
|
92
|
+
notification: { url: input.notificationUrl },
|
|
93
|
+
payerUrls: { success: input.successUrl, error: input.errorUrl },
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a timestamp from a rail that does not say what clock it is on.
|
|
3
|
+
*
|
|
4
|
+
* A value carrying `Z` or `±HH:mm` is already an instant and is honoured as
|
|
5
|
+
* written. A value without one is a WALL CLOCK — a label on someone's local
|
|
6
|
+
* time — and turning it into an instant needs the zone that clock runs on.
|
|
7
|
+
* `Date.parse` resolves such a value against the HOST zone, which is a silent
|
|
8
|
+
* trap: it reads correct on a laptop set to the rail's own zone and skews by
|
|
9
|
+
* the offset on the UTC runtime that serves production. Callers therefore pass
|
|
10
|
+
* the zone explicitly; there is no default, because a wrong default is exactly
|
|
11
|
+
* the bug this module exists to remove.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* The UTC instant of a zone-less `YYYY-MM-DDTHH:mm[:ss]` read in `timeZone`.
|
|
15
|
+
*
|
|
16
|
+
* Two passes, because the offset is a function of the answer being computed:
|
|
17
|
+
* the first prices the naive reading, the second re-prices it at the instant
|
|
18
|
+
* that produced. That is what makes the conversion hold on both sides of a DST
|
|
19
|
+
* transition rather than only in the half-year it was written in — a single
|
|
20
|
+
* pass reads the hour after a spring-forward an hour early. A local time the
|
|
21
|
+
* transition skips or repeats resolves through the offset in force at the first
|
|
22
|
+
* pass; no wall clock can separate those two readings.
|
|
23
|
+
*/
|
|
24
|
+
export declare function wallClockToUtc(wallClock: string, timeZone: string): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* An ISO UTC instant for `value`, or `null` when it does not carry one. A
|
|
27
|
+
* zone-less value is read as a wall clock in `timeZone`; a value stating its
|
|
28
|
+
* own offset keeps it. Never invents an instant — the absence of a readable
|
|
29
|
+
* timestamp must stay distinguishable from a timestamp.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseInstant(value: unknown, timeZone: string): string | null;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a timestamp from a rail that does not say what clock it is on.
|
|
3
|
+
*
|
|
4
|
+
* A value carrying `Z` or `±HH:mm` is already an instant and is honoured as
|
|
5
|
+
* written. A value without one is a WALL CLOCK — a label on someone's local
|
|
6
|
+
* time — and turning it into an instant needs the zone that clock runs on.
|
|
7
|
+
* `Date.parse` resolves such a value against the HOST zone, which is a silent
|
|
8
|
+
* trap: it reads correct on a laptop set to the rail's own zone and skews by
|
|
9
|
+
* the offset on the UTC runtime that serves production. Callers therefore pass
|
|
10
|
+
* the zone explicitly; there is no default, because a wrong default is exactly
|
|
11
|
+
* the bug this module exists to remove.
|
|
12
|
+
*/
|
|
13
|
+
/** A date-time with no `Z` and no `±HH:mm`, i.e. a bare wall-clock label. */
|
|
14
|
+
const ZONELESS_DATE_TIME = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)$/;
|
|
15
|
+
/** Milliseconds `timeZone` is ahead of UTC at `at` (negative when west of it). */
|
|
16
|
+
function zoneOffsetMs(at, timeZone) {
|
|
17
|
+
const zone = new Intl.DateTimeFormat("en", { timeZone, timeZoneName: "longOffset" })
|
|
18
|
+
.formatToParts(at).find((part) => part.type === "timeZoneName")?.value;
|
|
19
|
+
const match = /^GMT([+-])(\d{1,2}):(\d{2})$/.exec(zone ?? "");
|
|
20
|
+
if (!match)
|
|
21
|
+
return 0;
|
|
22
|
+
const [, sign, hours, minutes] = match;
|
|
23
|
+
return (sign === "-" ? -1 : 1) * (Number(hours) * 60 + Number(minutes)) * 60_000;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The UTC instant of a zone-less `YYYY-MM-DDTHH:mm[:ss]` read in `timeZone`.
|
|
27
|
+
*
|
|
28
|
+
* Two passes, because the offset is a function of the answer being computed:
|
|
29
|
+
* the first prices the naive reading, the second re-prices it at the instant
|
|
30
|
+
* that produced. That is what makes the conversion hold on both sides of a DST
|
|
31
|
+
* transition rather than only in the half-year it was written in — a single
|
|
32
|
+
* pass reads the hour after a spring-forward an hour early. A local time the
|
|
33
|
+
* transition skips or repeats resolves through the offset in force at the first
|
|
34
|
+
* pass; no wall clock can separate those two readings.
|
|
35
|
+
*/
|
|
36
|
+
export function wallClockToUtc(wallClock, timeZone) {
|
|
37
|
+
const asIfUtc = Date.parse(`${wallClock}Z`);
|
|
38
|
+
if (!Number.isFinite(asIfUtc))
|
|
39
|
+
return null;
|
|
40
|
+
const firstPass = asIfUtc - zoneOffsetMs(new Date(asIfUtc), timeZone);
|
|
41
|
+
return new Date(asIfUtc - zoneOffsetMs(new Date(firstPass), timeZone)).toISOString();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* An ISO UTC instant for `value`, or `null` when it does not carry one. A
|
|
45
|
+
* zone-less value is read as a wall clock in `timeZone`; a value stating its
|
|
46
|
+
* own offset keeps it. Never invents an instant — the absence of a readable
|
|
47
|
+
* timestamp must stay distinguishable from a timestamp.
|
|
48
|
+
*/
|
|
49
|
+
export function parseInstant(value, timeZone) {
|
|
50
|
+
if (typeof value !== "string" || !value.trim())
|
|
51
|
+
return null;
|
|
52
|
+
const trimmed = value.trim();
|
|
53
|
+
const zoneless = ZONELESS_DATE_TIME.exec(trimmed);
|
|
54
|
+
if (zoneless)
|
|
55
|
+
return wallClockToUtc(`${zoneless[1]}T${zoneless[2]}`, timeZone);
|
|
56
|
+
return Number.isFinite(Date.parse(trimmed)) ? new Date(trimmed).toISOString() : null;
|
|
57
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface TpayJwsCertificateResolver {
|
|
2
|
+
(url: string): Promise<string>;
|
|
3
|
+
}
|
|
4
|
+
export interface TpayJwsVerificationInput {
|
|
5
|
+
rawBody: string;
|
|
6
|
+
signatureHeader: string | undefined;
|
|
7
|
+
resolveCertificate: TpayJwsCertificateResolver;
|
|
8
|
+
rootCertificatePem: string;
|
|
9
|
+
allowedX5uPrefix?: string;
|
|
10
|
+
verifyCertificateChain?: (signingCertificatePem: string, rootCertificatePem: string) => boolean;
|
|
11
|
+
verifyDetachedSignature?: (input: {
|
|
12
|
+
signingInput: string;
|
|
13
|
+
signature: Buffer;
|
|
14
|
+
signingCertificatePem: string;
|
|
15
|
+
}) => boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function verifyTpayJwsSignature({ rawBody, signatureHeader, resolveCertificate, rootCertificatePem, allowedX5uPrefix, verifyCertificateChain: verifyChain, verifyDetachedSignature: verifyDetached, }: TpayJwsVerificationInput): Promise<boolean>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { verify as verifySignature, X509Certificate } from "node:crypto";
|
|
2
|
+
export async function verifyTpayJwsSignature({ rawBody, signatureHeader, resolveCertificate, rootCertificatePem, allowedX5uPrefix = "https://secure.sandbox.tpay.com/", verifyCertificateChain: verifyChain = verifyCertificateChain, verifyDetachedSignature: verifyDetached = defaultVerifyDetachedSignature, }) {
|
|
3
|
+
if (!signatureHeader || !rootCertificatePem)
|
|
4
|
+
return false;
|
|
5
|
+
const parts = signatureHeader.split(".");
|
|
6
|
+
if (parts.length < 3 || !parts[0] || !parts[2])
|
|
7
|
+
return false;
|
|
8
|
+
const header = parseJwsHeader(parts[0]);
|
|
9
|
+
if (!header?.x5u || !header.x5u.startsWith(allowedX5uPrefix))
|
|
10
|
+
return false;
|
|
11
|
+
const signingCertificatePem = await resolveCertificate(header.x5u);
|
|
12
|
+
if (!verifyChain(signingCertificatePem, rootCertificatePem))
|
|
13
|
+
return false;
|
|
14
|
+
const signedPayload = `${parts[0]}.${base64Url(Buffer.from(rawBody, "utf8"))}`;
|
|
15
|
+
const signature = base64UrlDecode(parts[2]);
|
|
16
|
+
return verifyDetached({ signingInput: signedPayload, signature, signingCertificatePem });
|
|
17
|
+
}
|
|
18
|
+
function parseJwsHeader(encoded) {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(base64UrlDecode(encoded).toString("utf8"));
|
|
21
|
+
if (!parsed || typeof parsed !== "object")
|
|
22
|
+
return null;
|
|
23
|
+
const x5u = parsed.x5u;
|
|
24
|
+
return typeof x5u === "string" ? { x5u } : null;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function verifyCertificateChain(signingCertificatePem, rootCertificatePem) {
|
|
31
|
+
try {
|
|
32
|
+
const signing = new X509Certificate(signingCertificatePem);
|
|
33
|
+
const root = new X509Certificate(rootCertificatePem);
|
|
34
|
+
return signing.verify(root.publicKey);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function defaultVerifyDetachedSignature(input) {
|
|
41
|
+
const publicKey = new X509Certificate(input.signingCertificatePem).publicKey;
|
|
42
|
+
return verifySignature("RSA-SHA256", Buffer.from(input.signingInput), publicKey, input.signature);
|
|
43
|
+
}
|
|
44
|
+
function base64Url(value) {
|
|
45
|
+
return value.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
46
|
+
}
|
|
47
|
+
function base64UrlDecode(value) {
|
|
48
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
49
|
+
const padded = normalized.padEnd(normalized.length + ((4 - normalized.length % 4) % 4), "=");
|
|
50
|
+
return Buffer.from(padded, "base64");
|
|
51
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PaymentMethodLifecycleEvent, PaymentMethodLifecycleEventKind } from "@openlup/core/payment";
|
|
2
|
+
declare const PROVIDER_KIND = "tpay";
|
|
3
|
+
type TpayCanonicalPaymentEvent = {
|
|
4
|
+
provider_event_id: string;
|
|
5
|
+
event_type: "payment.succeeded" | "payment.failed" | "payment.refunded" | "payment.requires_action" | "payment.disputed" | "setup.succeeded" | "setup.failed" | "setup.requires_action";
|
|
6
|
+
payment_provider_id: string;
|
|
7
|
+
payment_intent_id?: string | null;
|
|
8
|
+
amount_minor?: number;
|
|
9
|
+
raw_payload: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
export interface NormalizedTpayMethodRef {
|
|
12
|
+
clientId: string | null;
|
|
13
|
+
subscriptionId?: string | null;
|
|
14
|
+
providerMethodRef: string;
|
|
15
|
+
methodKind: "blik_payid";
|
|
16
|
+
status: "active" | "inactive";
|
|
17
|
+
consentSnapshot: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
export interface NormalizedTpayWebhook {
|
|
20
|
+
event: TpayCanonicalPaymentEvent;
|
|
21
|
+
methodRef: NormalizedTpayMethodRef | null;
|
|
22
|
+
/**
|
|
23
|
+
* The neutral transition this delivery reports about the stored mandate. The
|
|
24
|
+
* instant it happened is not known at this layer, so the full neutral event
|
|
25
|
+
* is assembled by the provider-webhook mapper below.
|
|
26
|
+
*/
|
|
27
|
+
methodLifecycleKind: PaymentMethodLifecycleEventKind | null;
|
|
28
|
+
responseBody: "TRUE";
|
|
29
|
+
}
|
|
30
|
+
type NormalizedTpayProviderWebhook = {
|
|
31
|
+
provider: typeof PROVIDER_KIND;
|
|
32
|
+
providerEventId: string;
|
|
33
|
+
eventType: TpayCanonicalPaymentEvent["event_type"];
|
|
34
|
+
providerPaymentId: string;
|
|
35
|
+
paymentIntentId?: string | null;
|
|
36
|
+
amountMinor: number | null;
|
|
37
|
+
currency: string | null;
|
|
38
|
+
occurredAt: string;
|
|
39
|
+
rawPayload: Record<string, unknown>;
|
|
40
|
+
reusableMethod: {
|
|
41
|
+
clientId: string;
|
|
42
|
+
subscriptionId: string | null;
|
|
43
|
+
providerCustomerRef: null;
|
|
44
|
+
providerMethodRef: string;
|
|
45
|
+
providerMandateRef: null;
|
|
46
|
+
methodKind: "blik_payid";
|
|
47
|
+
status: "active" | "inactive";
|
|
48
|
+
consentSnapshot: Record<string, unknown>;
|
|
49
|
+
} | null;
|
|
50
|
+
methodLifecycle: PaymentMethodLifecycleEvent | null;
|
|
51
|
+
};
|
|
52
|
+
export declare function normalizeTpayWebhook(rawBody: string): NormalizedTpayWebhook;
|
|
53
|
+
export declare function toNormalizedProviderTpayWebhook(webhook: NormalizedTpayWebhook, occurredAt: string): NormalizedTpayProviderWebhook;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const PROVIDER_KIND = "tpay";
|
|
2
|
+
/**
|
|
3
|
+
* Alias-rail event names, mapped to the neutral lifecycle vocabulary.
|
|
4
|
+
*
|
|
5
|
+
* ⛔ `ALIAS_UPDATE` is a ROTATION, not a death: the payer's standing consent is
|
|
6
|
+
* unchanged and only the alias behind it moves. Classifying it with the two
|
|
7
|
+
* terminal names would strip a healthy subscription of the mandate it renews
|
|
8
|
+
* on. That is why the `active` flag below already treats register and update
|
|
9
|
+
* alike, and why this table must keep them apart from unregister and expiry.
|
|
10
|
+
*/
|
|
11
|
+
const ALIAS_LIFECYCLE_KINDS = {
|
|
12
|
+
ALIAS_REGISTER: "method_registered",
|
|
13
|
+
ALIAS_UPDATE: "method_updated",
|
|
14
|
+
ALIAS_UNREGISTER: "method_revoked",
|
|
15
|
+
ALIAS_EXPIRED: "method_expired",
|
|
16
|
+
};
|
|
17
|
+
export function normalizeTpayWebhook(rawBody) {
|
|
18
|
+
const payload = parsePayload(rawBody);
|
|
19
|
+
const aliasEvent = payload.event;
|
|
20
|
+
if (aliasEvent) {
|
|
21
|
+
return normalizeAliasWebhook(payload);
|
|
22
|
+
}
|
|
23
|
+
return normalizeTransactionWebhook(payload);
|
|
24
|
+
}
|
|
25
|
+
export function toNormalizedProviderTpayWebhook(webhook, occurredAt) {
|
|
26
|
+
return {
|
|
27
|
+
provider: PROVIDER_KIND,
|
|
28
|
+
providerEventId: webhook.event.provider_event_id,
|
|
29
|
+
eventType: webhook.event.event_type,
|
|
30
|
+
providerPaymentId: webhook.event.payment_provider_id,
|
|
31
|
+
paymentIntentId: webhook.event.payment_intent_id ?? null,
|
|
32
|
+
amountMinor: webhook.event.amount_minor ?? null,
|
|
33
|
+
currency: readCurrency(webhook.event.raw_payload),
|
|
34
|
+
occurredAt,
|
|
35
|
+
rawPayload: webhook.event.raw_payload,
|
|
36
|
+
reusableMethod: toReusableMethod(webhook.methodRef),
|
|
37
|
+
// The alias value IS the stored method reference on this rail. The rail
|
|
38
|
+
// publishes no scheme label or digits with an alias transition, and its
|
|
39
|
+
// expiry field is deliberately not read here (see the wave plan): a
|
|
40
|
+
// transition on this rail is a state change, not a facts refresh.
|
|
41
|
+
methodLifecycle: webhook.methodLifecycleKind
|
|
42
|
+
? {
|
|
43
|
+
kind: webhook.methodLifecycleKind,
|
|
44
|
+
providerKind: PROVIDER_KIND,
|
|
45
|
+
providerMethodRef: webhook.event.payment_provider_id,
|
|
46
|
+
providerEventId: webhook.event.provider_event_id,
|
|
47
|
+
occurredAt,
|
|
48
|
+
replacement: null,
|
|
49
|
+
}
|
|
50
|
+
: null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function normalizeTransactionWebhook(payload) {
|
|
54
|
+
const transactionTitle = required(payload, "tr_id");
|
|
55
|
+
const rawStatus = required(payload, "tr_status");
|
|
56
|
+
// Tpay production sends `tr_status` UPPERCASE ("TRUE"/"CHARGEBACK"); a
|
|
57
|
+
// case-sensitive compare mislabels every real success as payment.failed
|
|
58
|
+
// (caught live 2026-07-13 via a production payment-link). Classify
|
|
59
|
+
// case-insensitively; keep the raw value in raw_payload for observability.
|
|
60
|
+
const status = rawStatus.toLowerCase();
|
|
61
|
+
const amountMinor = amountToMinor(payload.tr_paid || payload.tr_amount);
|
|
62
|
+
const paymentIntentId = parseUuid(payload.tr_crc);
|
|
63
|
+
return {
|
|
64
|
+
event: {
|
|
65
|
+
// Only chargebacks split identity; every other status keeps its legacy keys.
|
|
66
|
+
provider_event_id: status === "chargeback" ? `${transactionTitle}:chargeback` : transactionTitle,
|
|
67
|
+
event_type: status === "chargeback" ? "payment.refunded" : status === "true" ? "payment.succeeded" : "payment.failed",
|
|
68
|
+
payment_provider_id: transactionTitle,
|
|
69
|
+
payment_intent_id: paymentIntentId,
|
|
70
|
+
amount_minor: amountMinor,
|
|
71
|
+
raw_payload: {
|
|
72
|
+
provider: PROVIDER_KIND,
|
|
73
|
+
eventKind: "transaction",
|
|
74
|
+
transactionTitle,
|
|
75
|
+
hiddenDescription: payload.tr_crc ?? null,
|
|
76
|
+
transactionAmount: payload.tr_amount ?? null,
|
|
77
|
+
paidAmount: payload.tr_paid ?? null,
|
|
78
|
+
// Null, not the settlement default: this records what the provider SAID,
|
|
79
|
+
// and a currency it did not send is unknown. The whole path was already
|
|
80
|
+
// built for that - `NormalizedTpayProviderWebhook.currency` is typed
|
|
81
|
+
// `string | null` and `readCurrency` below returns null for an absent or
|
|
82
|
+
// blank field - so the old `?? "PLN"` was manufacturing an operand rather
|
|
83
|
+
// than filling a gap.
|
|
84
|
+
//
|
|
85
|
+
// It does not weaken the SQL cross-check either. `commerce_payment_control_
|
|
86
|
+
// apply_result` guards `v_event.currency IS NOT NULL AND v_event.currency <>
|
|
87
|
+
// v_intent.currency`; a fabricated code cannot detect a real mismatch, it can
|
|
88
|
+
// only compare equal by luck in a PLN deployment or raise a *false*
|
|
89
|
+
// `payment_control_result_currency_mismatch` in one that settles elsewhere and
|
|
90
|
+
// refuse a legitimate payment. Skipping a comparison nobody can make beats
|
|
91
|
+
// making one against an invented value.
|
|
92
|
+
currency: payload.tr_currency ?? null,
|
|
93
|
+
status: rawStatus,
|
|
94
|
+
testMode: payload.test_mode ?? null,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
methodRef: null,
|
|
98
|
+
methodLifecycleKind: null,
|
|
99
|
+
responseBody: "TRUE",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function normalizeAliasWebhook(payload) {
|
|
103
|
+
const aliasValue = required(payload, "msg_value[value]");
|
|
104
|
+
// Tpay sends alias event names UPPERCASE; normalize defensively so the
|
|
105
|
+
// classification can never be defeated by casing (see tr_status above).
|
|
106
|
+
const event = required(payload, "event").toUpperCase();
|
|
107
|
+
const active = event === "ALIAS_REGISTER" || event === "ALIAS_UPDATE";
|
|
108
|
+
const paymentIntentId = paymentIntentIdFromAlias(aliasValue);
|
|
109
|
+
return {
|
|
110
|
+
event: {
|
|
111
|
+
provider_event_id: `${event}:${aliasValue}`,
|
|
112
|
+
event_type: active ? "setup.succeeded" : "setup.failed",
|
|
113
|
+
payment_provider_id: aliasValue,
|
|
114
|
+
payment_intent_id: paymentIntentId,
|
|
115
|
+
raw_payload: {
|
|
116
|
+
provider: PROVIDER_KIND,
|
|
117
|
+
eventKind: "blik_alias",
|
|
118
|
+
event,
|
|
119
|
+
paymentIntentId,
|
|
120
|
+
aliasValuePresent: true,
|
|
121
|
+
aliasType: payload["msg_value[type]"] ?? null,
|
|
122
|
+
aliasExpirationDatePresent: Boolean(payload["msg_value[expirationDate]"]),
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
methodRef: {
|
|
126
|
+
clientId: payload.client_id ?? null,
|
|
127
|
+
subscriptionId: payload.subscription_id ?? null,
|
|
128
|
+
providerMethodRef: aliasValue,
|
|
129
|
+
methodKind: "blik_payid",
|
|
130
|
+
status: active ? "active" : "inactive",
|
|
131
|
+
consentSnapshot: { source: "tpay.alias.webhook", event },
|
|
132
|
+
},
|
|
133
|
+
methodLifecycleKind: ALIAS_LIFECYCLE_KINDS[event] ?? null,
|
|
134
|
+
responseBody: "TRUE",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function toReusableMethod(methodRef) {
|
|
138
|
+
if (!methodRef?.clientId)
|
|
139
|
+
return null;
|
|
140
|
+
return {
|
|
141
|
+
clientId: methodRef.clientId,
|
|
142
|
+
subscriptionId: methodRef.subscriptionId ?? null,
|
|
143
|
+
providerCustomerRef: null,
|
|
144
|
+
providerMethodRef: methodRef.providerMethodRef,
|
|
145
|
+
providerMandateRef: null,
|
|
146
|
+
methodKind: methodRef.methodKind,
|
|
147
|
+
status: methodRef.status,
|
|
148
|
+
consentSnapshot: methodRef.consentSnapshot,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function readCurrency(payload) {
|
|
152
|
+
const currency = payload.currency;
|
|
153
|
+
return typeof currency === "string" && currency.trim() ? currency : null;
|
|
154
|
+
}
|
|
155
|
+
function parsePayload(rawBody) {
|
|
156
|
+
const trimmed = rawBody.trim();
|
|
157
|
+
if (trimmed.startsWith("{")) {
|
|
158
|
+
const parsed = JSON.parse(trimmed);
|
|
159
|
+
return flattenJson(parsed);
|
|
160
|
+
}
|
|
161
|
+
const params = new URLSearchParams(rawBody);
|
|
162
|
+
return Object.fromEntries(Array.from(params.entries()));
|
|
163
|
+
}
|
|
164
|
+
function flattenJson(value, prefix = "") {
|
|
165
|
+
const out = {};
|
|
166
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
167
|
+
const name = prefix ? `${prefix}[${key}]` : key;
|
|
168
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
169
|
+
Object.assign(out, flattenJson(raw, name));
|
|
170
|
+
}
|
|
171
|
+
else if (raw !== undefined && raw !== null) {
|
|
172
|
+
out[name] = String(raw);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
function required(payload, key) {
|
|
178
|
+
const value = payload[key];
|
|
179
|
+
if (!value)
|
|
180
|
+
throw new Error(`tpay_webhook_missing_${key}`);
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
function amountToMinor(value) {
|
|
184
|
+
if (!value)
|
|
185
|
+
return undefined;
|
|
186
|
+
const numeric = Number(value.replace(",", "."));
|
|
187
|
+
return Number.isFinite(numeric) ? Math.round(numeric * 100) : undefined;
|
|
188
|
+
}
|
|
189
|
+
function paymentIntentIdFromAlias(value) {
|
|
190
|
+
const match = /^openlup_([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/i.exec(value);
|
|
191
|
+
return match ? match[1].toLowerCase() : null;
|
|
192
|
+
}
|
|
193
|
+
function parseUuid(value) {
|
|
194
|
+
if (!value)
|
|
195
|
+
return null;
|
|
196
|
+
const trimmed = value.trim();
|
|
197
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(trimmed)
|
|
198
|
+
? trimmed.toLowerCase()
|
|
199
|
+
: null;
|
|
200
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PaymentVerifyNormalizePort, RefundEvidencePolicy, VerifiedMethodEvidence, VerifiedPaymentEvidence } from "./contracts/evidence.js";
|
|
2
|
+
import type { ProviderWebhookWithMethodLifecycle } from "./paymentMethodLifecycle.js";
|
|
3
|
+
/** New boundary module: verified adapter envelopes into the unchanged donor carrier.
|
|
4
|
+
* Original bytes go only to verification. Store adapter-redacted audit facts plus
|
|
5
|
+
* nested normalized evidence; typed facts override overlapping reader keys.
|
|
6
|
+
* Neither audit shape nor receipt time changes adapter identity/fingerprint.
|
|
7
|
+
* Incomplete refunds are ingested and acknowledged, but require-evidence prevents apply.
|
|
8
|
+
*/
|
|
9
|
+
export declare function projectVerifiedEvidence(evidence: VerifiedPaymentEvidence | VerifiedMethodEvidence, input: Parameters<PaymentVerifyNormalizePort["verifyAndNormalize"]>[0], policy: RefundEvidencePolicy, methodOwner?: {
|
|
10
|
+
clientId: string;
|
|
11
|
+
subscriptionId?: string | null;
|
|
12
|
+
}): ProviderWebhookWithMethodLifecycle;
|