@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,166 @@
|
|
|
1
|
+
/** New boundary module: verified adapter envelopes into the unchanged donor carrier.
|
|
2
|
+
* Original bytes go only to verification. Store adapter-redacted audit facts plus
|
|
3
|
+
* nested normalized evidence; typed facts override overlapping reader keys.
|
|
4
|
+
* Neither audit shape nor receipt time changes adapter identity/fingerprint.
|
|
5
|
+
* Incomplete refunds are ingested and acknowledged, but require-evidence prevents apply.
|
|
6
|
+
*/
|
|
7
|
+
export function projectVerifiedEvidence(evidence, input, policy, methodOwner) {
|
|
8
|
+
const payload = auditPayload(evidence);
|
|
9
|
+
const base = {
|
|
10
|
+
provider: evidence.provider, providerEventId: evidence.providerEventId,
|
|
11
|
+
occurredAt: input.receivedAt, rawPayload: payload,
|
|
12
|
+
paymentIntentId: evidence.intentRef,
|
|
13
|
+
};
|
|
14
|
+
switch (evidence.kind) {
|
|
15
|
+
case "money": {
|
|
16
|
+
// The donor treats absent money as unasserted and skips parity checks.
|
|
17
|
+
// Settling adapter evidence must instead use its existing mismatch refusal.
|
|
18
|
+
if ((evidence.outcome === "succeeded" || evidence.outcome === "refunded" || evidence.outcome === "disputed")
|
|
19
|
+
&& (evidence.money === null || !Number.isSafeInteger(evidence.money.amountMinor)
|
|
20
|
+
|| evidence.money.amountMinor <= 0 || !evidence.money.currency.trim())) {
|
|
21
|
+
throw new Error("payment_control_result_amount_mismatch");
|
|
22
|
+
}
|
|
23
|
+
const reversal = evidence.refund;
|
|
24
|
+
const refundKind = evidence.outcome === "refunded"
|
|
25
|
+
? (reversal ? projectRefund(reversal, evidence) : null) ?? (policy === "legacy-assume-full" ? "full" : null)
|
|
26
|
+
: undefined;
|
|
27
|
+
return {
|
|
28
|
+
...base, eventType: moneyEvent(evidence.outcome, refundKind ? reversal : undefined),
|
|
29
|
+
providerPaymentId: evidence.providerPaymentRef, paymentAttemptId: evidence.attemptRef,
|
|
30
|
+
amountMinor: evidence.money?.amountMinor ?? null, currency: evidence.money?.currency ?? null,
|
|
31
|
+
refundKind,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
case "method":
|
|
35
|
+
return {
|
|
36
|
+
...base, eventType: methodEvent(evidence.outcome),
|
|
37
|
+
// Pinned donor normalizer :59 calls resolveLinkagePaymentId :146–154:
|
|
38
|
+
// setup/method objects use their own object id as the carrier reference.
|
|
39
|
+
providerPaymentId: evidence.setupRef ?? evidence.methodRef,
|
|
40
|
+
methodLifecycle: lifecycle(evidence.methodLifecycle),
|
|
41
|
+
...(evidence.reusableMethod && methodOwner ? { reusableMethod: {
|
|
42
|
+
clientId: methodOwner.clientId, subscriptionId: methodOwner.subscriptionId,
|
|
43
|
+
providerCustomerRef: evidence.reusableMethod.providerCustomerRef,
|
|
44
|
+
providerMethodRef: evidence.reusableMethod.providerMethodRef,
|
|
45
|
+
providerMandateRef: evidence.reusableMethod.providerMandateRef,
|
|
46
|
+
methodKind: evidence.reusableMethod.methodKind, status: evidence.reusableMethod.status,
|
|
47
|
+
} } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return assertNever(evidence);
|
|
51
|
+
}
|
|
52
|
+
function moneyEvent(outcome, refund) {
|
|
53
|
+
switch (outcome) {
|
|
54
|
+
case "succeeded": return "payment.succeeded";
|
|
55
|
+
case "failed": return "payment.failed";
|
|
56
|
+
case "pending": return "payment.requires_action";
|
|
57
|
+
case "refunded": return refund?.kind === "full-reversal" && refund.reason === "chargeback" ? "payment.disputed" : "payment.refunded";
|
|
58
|
+
case "disputed": return "payment.disputed";
|
|
59
|
+
}
|
|
60
|
+
return assertNever(outcome);
|
|
61
|
+
}
|
|
62
|
+
function methodEvent(outcome) {
|
|
63
|
+
switch (outcome) {
|
|
64
|
+
case "registered":
|
|
65
|
+
case "updated": return "setup.succeeded";
|
|
66
|
+
case "revoked":
|
|
67
|
+
case "expired":
|
|
68
|
+
case "suspended":
|
|
69
|
+
case "failed": return "setup.failed";
|
|
70
|
+
case "requires-action": return "setup.requires_action";
|
|
71
|
+
}
|
|
72
|
+
return assertNever(outcome);
|
|
73
|
+
}
|
|
74
|
+
function projectRefund(refund, evidence) {
|
|
75
|
+
const original = refund.originalPaymentMoney;
|
|
76
|
+
if (!refund.operationId?.trim() || !evidence.providerPaymentRef.trim()
|
|
77
|
+
|| !Number.isSafeInteger(original?.amountMinor) || original.amountMinor <= 0
|
|
78
|
+
|| !original.currency?.trim() || !evidence.money
|
|
79
|
+
|| evidence.money.amountMinor !== original.amountMinor || evidence.money.currency !== original.currency) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
switch (refund.kind) {
|
|
83
|
+
case "full-reversal": return "full";
|
|
84
|
+
case "cumulative-snapshot":
|
|
85
|
+
case "event-delta": {
|
|
86
|
+
const money = refund.refundedMoney;
|
|
87
|
+
if (!Number.isSafeInteger(money?.amountMinor) || money.amountMinor <= 0
|
|
88
|
+
|| money.amountMinor > original.amountMinor || money.currency !== original.currency) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return money.amountMinor < original.amountMinor ? "partial" : "full";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return assertNever(refund);
|
|
95
|
+
}
|
|
96
|
+
function assertNever(value) {
|
|
97
|
+
throw new Error(`payment_evidence_unmapped:${String(value)}`);
|
|
98
|
+
}
|
|
99
|
+
function auditPayload(evidence) {
|
|
100
|
+
// Explicit typed fields only: unknown envelope properties never enter storage.
|
|
101
|
+
const normalized = {
|
|
102
|
+
provider: evidence.provider, providerEventId: evidence.providerEventId,
|
|
103
|
+
fingerprint: evidence.fingerprint,
|
|
104
|
+
providerOccurredAt: evidence.providerOccurredAt, kind: evidence.kind,
|
|
105
|
+
intentRef: evidence.intentRef, outcome: evidence.outcome,
|
|
106
|
+
...(evidence.kind === "money" ? {
|
|
107
|
+
providerPaymentRef: evidence.providerPaymentRef, attemptRef: evidence.attemptRef,
|
|
108
|
+
money: evidence.money && money(evidence.money),
|
|
109
|
+
refusal: evidence.refusal && { code: evidence.refusal.code,
|
|
110
|
+
hints: [...evidence.refusal.hints], adviceCode: evidence.refusal.adviceCode },
|
|
111
|
+
refund: evidence.refund && refund(evidence.refund),
|
|
112
|
+
} : {
|
|
113
|
+
methodRef: evidence.methodRef, setupRef: evidence.setupRef,
|
|
114
|
+
methodLifecycle: lifecycle(evidence.methodLifecycle),
|
|
115
|
+
reusableMethod: evidence.reusableMethod && {
|
|
116
|
+
providerCustomerRef: evidence.reusableMethod.providerCustomerRef,
|
|
117
|
+
providerMethodRef: evidence.reusableMethod.providerMethodRef,
|
|
118
|
+
providerMandateRef: evidence.reusableMethod.providerMandateRef,
|
|
119
|
+
methodKind: evidence.reusableMethod.methodKind, status: evidence.reusableMethod.status,
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
};
|
|
123
|
+
const payload = { ...evidence.providerAuditPayload };
|
|
124
|
+
if (evidence.kind === "money") {
|
|
125
|
+
// Host readers use the provider object reference and original payment money.
|
|
126
|
+
payload.providerPaymentId = evidence.providerPaymentRef;
|
|
127
|
+
payload.amountMinor = evidence.money?.amountMinor ?? null;
|
|
128
|
+
payload.currency = evidence.money?.currency ?? null;
|
|
129
|
+
// Snapshot reader namespaces are adapter-owned. Their charge identity has no
|
|
130
|
+
// typed home; their cumulative amount must come from the typed observation.
|
|
131
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
132
|
+
if (value && typeof value === "object" && !Array.isArray(value) && "chargeId" in value) {
|
|
133
|
+
payload[key] = { ...value, amountRefundedMinor: evidence.refund?.kind === "cumulative-snapshot"
|
|
134
|
+
? evidence.refund.refundedMoney?.amountMinor ?? null : null };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const failure = payload.failureEvidence;
|
|
138
|
+
if (failure && typeof failure === "object" && !Array.isArray(failure)) {
|
|
139
|
+
payload.failureEvidence = { ...failure, providerPaymentId: evidence.providerPaymentRef,
|
|
140
|
+
code: evidence.refusal?.code ?? null, adviceCode: evidence.refusal?.adviceCode ?? null };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
payload.openlupEvidence = normalized;
|
|
144
|
+
return payload;
|
|
145
|
+
}
|
|
146
|
+
function money(value) {
|
|
147
|
+
return { amountMinor: value.amountMinor, currency: value.currency };
|
|
148
|
+
}
|
|
149
|
+
function refund(value) {
|
|
150
|
+
const base = { operationId: value.operationId, originalPaymentMoney: value.originalPaymentMoney && money(value.originalPaymentMoney) };
|
|
151
|
+
switch (value.kind) {
|
|
152
|
+
case "full-reversal": return { ...base, kind: value.kind, reason: value.reason };
|
|
153
|
+
case "cumulative-snapshot":
|
|
154
|
+
case "event-delta":
|
|
155
|
+
return { ...base, kind: value.kind, refundedMoney: value.refundedMoney && money(value.refundedMoney) };
|
|
156
|
+
}
|
|
157
|
+
return assertNever(value);
|
|
158
|
+
}
|
|
159
|
+
function lifecycle(value) {
|
|
160
|
+
return value && { kind: value.kind, providerKind: value.providerKind,
|
|
161
|
+
providerMethodRef: value.providerMethodRef, providerEventId: value.providerEventId,
|
|
162
|
+
occurredAt: value.occurredAt, replacement: value.replacement && {
|
|
163
|
+
schemeLabel: value.replacement.schemeLabel, lastDigits: value.replacement.lastDigits,
|
|
164
|
+
expiresAt: value.replacement.expiresAt,
|
|
165
|
+
} };
|
|
166
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { PaymentIngressPort, PaymentVerifyNormalizePort, RefundEvidencePolicy, VerifiedMethodEvidence } from "./contracts/evidence.js";
|
|
2
|
+
declare const PAYMENT_WEBHOOK_EVENT_TYPES: readonly ["payment.succeeded", "payment.failed", "payment.refunded", "payment.requires_action", "payment.disputed", "setup.succeeded", "setup.failed", "setup.requires_action"];
|
|
3
|
+
type PaymentWebhookEventType = (typeof PAYMENT_WEBHOOK_EVENT_TYPES)[number];
|
|
4
|
+
export type ProviderPaymentWebhookKind = PaymentWebhookEventType;
|
|
5
|
+
export interface NormalizedProviderPaymentWebhook {
|
|
6
|
+
provider: string;
|
|
7
|
+
providerEventId: string;
|
|
8
|
+
eventType: ProviderPaymentWebhookKind;
|
|
9
|
+
providerPaymentId: string;
|
|
10
|
+
paymentIntentId?: string | null;
|
|
11
|
+
paymentAttemptId?: string | null;
|
|
12
|
+
amountMinor?: number | null;
|
|
13
|
+
currency?: string | null;
|
|
14
|
+
occurredAt: string;
|
|
15
|
+
refundKind?: "full" | "partial" | null;
|
|
16
|
+
rawPayload: Record<string, unknown>;
|
|
17
|
+
reusableMethod?: {
|
|
18
|
+
clientId: string;
|
|
19
|
+
subscriptionId?: string | null;
|
|
20
|
+
providerCustomerRef?: string | null;
|
|
21
|
+
providerMethodRef: string;
|
|
22
|
+
providerMandateRef?: string | null;
|
|
23
|
+
methodKind: string;
|
|
24
|
+
status: "pending_verification" | "active" | "inactive" | "expired" | "revoked";
|
|
25
|
+
consentSnapshot?: Record<string, unknown>;
|
|
26
|
+
} | null;
|
|
27
|
+
}
|
|
28
|
+
export interface PaymentWebhookControlPort {
|
|
29
|
+
ingestPaymentEvent(input: NormalizedProviderPaymentWebhook): Promise<{
|
|
30
|
+
paymentEventId: string;
|
|
31
|
+
paymentIntentId: string | null;
|
|
32
|
+
paymentAttemptId: string | null;
|
|
33
|
+
replayed: boolean;
|
|
34
|
+
occurredAt?: string;
|
|
35
|
+
}>;
|
|
36
|
+
applyPaymentResult?(input: {
|
|
37
|
+
idempotencyKey: string;
|
|
38
|
+
paymentIntentId: string;
|
|
39
|
+
paymentEventId: string;
|
|
40
|
+
resultStatus: "succeeded" | "failed" | "refunded" | "partially_refunded" | "disputed";
|
|
41
|
+
occurredAt: string;
|
|
42
|
+
failureReason: string | null;
|
|
43
|
+
}): Promise<{
|
|
44
|
+
replayed: boolean;
|
|
45
|
+
}>;
|
|
46
|
+
confirmSubscriptionActivation?(input: {
|
|
47
|
+
idempotencyKey: string;
|
|
48
|
+
paymentIntentId: string;
|
|
49
|
+
occurredAt: string;
|
|
50
|
+
methodRef: string | null;
|
|
51
|
+
methodKind: string | null;
|
|
52
|
+
}): Promise<{
|
|
53
|
+
confirmed: boolean;
|
|
54
|
+
status: string | null;
|
|
55
|
+
}>;
|
|
56
|
+
recoverPaidSubscriptionActivationWithCard?(input: {
|
|
57
|
+
idempotencyKey: string;
|
|
58
|
+
subscriptionId: string;
|
|
59
|
+
paymentMethodRef: string;
|
|
60
|
+
occurredAt: string;
|
|
61
|
+
}): Promise<{
|
|
62
|
+
recovered: boolean;
|
|
63
|
+
replayed: boolean;
|
|
64
|
+
reason: string | null;
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* Closes the ledger row of a `setup.*` event once the full setup pipeline
|
|
68
|
+
* resolved. Setup events carry no money result, so applyPaymentResult (the
|
|
69
|
+
* only other processed_at writer) never runs for them.
|
|
70
|
+
*/
|
|
71
|
+
markSetupEventProcessed?(input: {
|
|
72
|
+
paymentEventId: string;
|
|
73
|
+
}): Promise<{
|
|
74
|
+
updated: boolean;
|
|
75
|
+
}>;
|
|
76
|
+
}
|
|
77
|
+
export interface PaymentWebhookMethodRefPort {
|
|
78
|
+
upsertFromWebhook(input: NormalizedProviderPaymentWebhook): Promise<{
|
|
79
|
+
replayed: boolean;
|
|
80
|
+
}>;
|
|
81
|
+
}
|
|
82
|
+
export type PaymentWebhookAfterProcessed = (input: {
|
|
83
|
+
event: NormalizedProviderPaymentWebhook;
|
|
84
|
+
ingested: {
|
|
85
|
+
paymentEventId: string;
|
|
86
|
+
paymentIntentId: string | null;
|
|
87
|
+
paymentAttemptId: string | null;
|
|
88
|
+
replayed: boolean;
|
|
89
|
+
};
|
|
90
|
+
resultStatus: "succeeded" | "failed" | "refunded" | "partially_refunded" | "disputed" | null;
|
|
91
|
+
}) => Promise<void>;
|
|
92
|
+
export interface PaymentWebhookProcessingPorts {
|
|
93
|
+
paymentControlPort: PaymentWebhookControlPort;
|
|
94
|
+
methodRefPort?: PaymentWebhookMethodRefPort;
|
|
95
|
+
afterProcessed?: PaymentWebhookAfterProcessed;
|
|
96
|
+
}
|
|
97
|
+
type OperationalEventRecorder = (event: {
|
|
98
|
+
name: string;
|
|
99
|
+
domain: string;
|
|
100
|
+
surface: string;
|
|
101
|
+
details: Record<string, unknown>;
|
|
102
|
+
}) => void;
|
|
103
|
+
/** Verified ingress boundary; persistence and transport response writing belong to the host. */
|
|
104
|
+
export declare function createVerifiedPaymentIngress({ provider, verifyPort, paymentControlPort, methodRefPort, afterProcessed, createPorts, resolveMethodOwner, refundEvidencePolicy, operationalEvents, }: {
|
|
105
|
+
provider: string;
|
|
106
|
+
verifyPort: PaymentVerifyNormalizePort;
|
|
107
|
+
paymentControlPort: PaymentWebhookControlPort;
|
|
108
|
+
methodRefPort?: PaymentWebhookMethodRefPort;
|
|
109
|
+
afterProcessed?: PaymentWebhookAfterProcessed;
|
|
110
|
+
createPorts?: (event: NormalizedProviderPaymentWebhook) => PaymentWebhookProcessingPorts | Promise<PaymentWebhookProcessingPorts>;
|
|
111
|
+
/** Host-owned identity; adapter metadata cannot supply client/subscription authority. */
|
|
112
|
+
resolveMethodOwner?: (evidence: VerifiedMethodEvidence) => Promise<{
|
|
113
|
+
clientId: string;
|
|
114
|
+
subscriptionId?: string | null;
|
|
115
|
+
} | null>;
|
|
116
|
+
refundEvidencePolicy?: RefundEvidencePolicy;
|
|
117
|
+
operationalEvents?: OperationalEventRecorder;
|
|
118
|
+
}): PaymentIngressPort;
|
|
119
|
+
export {};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { projectVerifiedEvidence } from "./verifiedEvidenceProjection.js";
|
|
2
|
+
const PAYMENT_WEBHOOK_EVENT_TYPES = [
|
|
3
|
+
"payment.succeeded",
|
|
4
|
+
"payment.failed",
|
|
5
|
+
"payment.refunded",
|
|
6
|
+
"payment.requires_action",
|
|
7
|
+
"payment.disputed",
|
|
8
|
+
"setup.succeeded",
|
|
9
|
+
"setup.failed",
|
|
10
|
+
"setup.requires_action",
|
|
11
|
+
];
|
|
12
|
+
/** Verified ingress boundary; persistence and transport response writing belong to the host. */
|
|
13
|
+
export function createVerifiedPaymentIngress({ provider, verifyPort, paymentControlPort, methodRefPort, afterProcessed, createPorts, resolveMethodOwner, refundEvidencePolicy = "require-evidence", operationalEvents = () => { }, }) {
|
|
14
|
+
return { async receive(input) {
|
|
15
|
+
let evidence;
|
|
16
|
+
try {
|
|
17
|
+
evidence = await verifyPort.verifyAndNormalize(input);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return { status: 400, data: { error: "Invalid provider webhook signature" } };
|
|
21
|
+
}
|
|
22
|
+
if (evidence.kind === "refusal") {
|
|
23
|
+
return evidence.reason === "unsupported-event"
|
|
24
|
+
? { status: 200, data: { provider, acknowledged: true, skipped: "unsupported_event" } }
|
|
25
|
+
: { status: 400, data: { error: "Invalid provider webhook signature" } };
|
|
26
|
+
}
|
|
27
|
+
const verifiedEvidence = evidence;
|
|
28
|
+
let event;
|
|
29
|
+
const res = { status: 503, data: {} };
|
|
30
|
+
const sendSuccess = (target, data) => { target.status = 200; target.data = data; };
|
|
31
|
+
const sendBffError = (target, code, error) => {
|
|
32
|
+
target.status = code === "BAD_REQUEST" ? 400 : code === "CONFLICT" ? 409 : 503;
|
|
33
|
+
target.data = { error };
|
|
34
|
+
};
|
|
35
|
+
await processEvent();
|
|
36
|
+
return res;
|
|
37
|
+
async function processEvent() {
|
|
38
|
+
try {
|
|
39
|
+
try {
|
|
40
|
+
event = projectVerifiedEvidence(verifiedEvidence, input, refundEvidencePolicy);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof Error && error.message === "payment_control_result_amount_mismatch")
|
|
44
|
+
throw error;
|
|
45
|
+
sendBffError(res, "BAD_REQUEST", "Invalid provider webhook evidence");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (event.provider !== provider) {
|
|
49
|
+
sendBffError(res, "BAD_REQUEST", "Provider webhook route mismatch");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (verifiedEvidence.kind === "method" && verifiedEvidence.reusableMethod && resolveMethodOwner) {
|
|
53
|
+
const owner = await resolveMethodOwner(verifiedEvidence);
|
|
54
|
+
event = projectVerifiedEvidence(verifiedEvidence, input, refundEvidencePolicy, owner ?? undefined);
|
|
55
|
+
}
|
|
56
|
+
const ports = createPorts
|
|
57
|
+
? await createPorts(event)
|
|
58
|
+
: { paymentControlPort: paymentControlPort, methodRefPort, afterProcessed };
|
|
59
|
+
if (!ports.paymentControlPort)
|
|
60
|
+
throw new Error("Provider webhook port unavailable");
|
|
61
|
+
const ingested = await ports.paymentControlPort.ingestPaymentEvent(event);
|
|
62
|
+
if (ingested.occurredAt)
|
|
63
|
+
event = { ...event, occurredAt: ingested.occurredAt };
|
|
64
|
+
const resultStatus = mapResultStatus(event, refundEvidencePolicy);
|
|
65
|
+
if (resultStatus && ingested.paymentIntentId && ports.paymentControlPort.applyPaymentResult) {
|
|
66
|
+
await ports.paymentControlPort.applyPaymentResult({
|
|
67
|
+
idempotencyKey: `provider-webhook:${provider}:${event.providerEventId}:apply`,
|
|
68
|
+
paymentIntentId: ingested.paymentIntentId,
|
|
69
|
+
paymentEventId: ingested.paymentEventId,
|
|
70
|
+
resultStatus,
|
|
71
|
+
occurredAt: event.occurredAt,
|
|
72
|
+
failureReason: event.eventType === "payment.failed" ? "provider_webhook_failed" : null,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
else if (isMoneyMoved(resultStatus) && !ingested.paymentIntentId && !ingested.replayed) {
|
|
76
|
+
// Operator policy: payments should only originate at the storefront
|
|
77
|
+
// checkout. A money-moved provider event (succeeded / refunded /
|
|
78
|
+
// disputed) that matched no local intent is an out-of-band payment
|
|
79
|
+
// (provider panel payment-link, dashboard link/invoice, wallet, card
|
|
80
|
+
// terminal). It is recorded in inbound_provider_events and ACKed, but
|
|
81
|
+
// there is no order to settle, invoice, or fulfill — the safe no-op above
|
|
82
|
+
// is intentional and preserved. Emit a vendor-neutral, non-paging
|
|
83
|
+
// reconciliation signal so the money is never silently lost.
|
|
84
|
+
// Fires once per unique provider event (`!replayed`): the ingest RPC is
|
|
85
|
+
// idempotent on (provider, provider_event_id), so PSP re-delivery is a
|
|
86
|
+
// natural throttle.
|
|
87
|
+
operationalEvents({
|
|
88
|
+
name: "provider_payment_unmatched_out_of_band",
|
|
89
|
+
domain: "payment",
|
|
90
|
+
surface: "webhook",
|
|
91
|
+
details: {
|
|
92
|
+
provider,
|
|
93
|
+
providerPaymentId: event.providerPaymentId,
|
|
94
|
+
eventType: event.eventType,
|
|
95
|
+
resultStatus,
|
|
96
|
+
amountMinor: event.amountMinor ?? null,
|
|
97
|
+
currency: event.currency ?? null,
|
|
98
|
+
occurredAt: event.occurredAt,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (ports.methodRefPort) {
|
|
103
|
+
await ports.methodRefPort.upsertFromWebhook(event);
|
|
104
|
+
}
|
|
105
|
+
if (ports.afterProcessed) {
|
|
106
|
+
await ports.afterProcessed({ event, ingested, resultStatus });
|
|
107
|
+
}
|
|
108
|
+
// Reached only when every setup mutation above resolved (a throw 5xx's →
|
|
109
|
+
// provider redelivers into a still-'received' row). Runs on replays too,
|
|
110
|
+
// so redelivery self-heals rows stuck from before this closer existed.
|
|
111
|
+
if (resultStatus === null && event.eventType.startsWith("setup.") && ports.paymentControlPort.markSetupEventProcessed) {
|
|
112
|
+
await ports.paymentControlPort.markSetupEventProcessed({ paymentEventId: ingested.paymentEventId });
|
|
113
|
+
}
|
|
114
|
+
sendSuccess(res, {
|
|
115
|
+
provider,
|
|
116
|
+
providerEventId: event.providerEventId,
|
|
117
|
+
paymentEventId: ingested.paymentEventId,
|
|
118
|
+
replayed: ingested.replayed,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
+
// A genuine idempotency conflict (same key, different payload) is a client
|
|
124
|
+
// 409, not a transient 5xx — a 503 would make the provider retry a request
|
|
125
|
+
// that can never succeed. The method-ref RPC raises this on a fingerprint
|
|
126
|
+
// mismatch (SQLSTATE 23505).
|
|
127
|
+
if (/payment_method_ref_idempotency_conflict|commerce_idempotency_conflict|\b23505\b/.test(message)) {
|
|
128
|
+
sendBffError(res, "CONFLICT", "Webhook idempotency conflict");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// A `succeeded` result that arrives AFTER the provisional-subscription
|
|
132
|
+
// sweep has already terminalized the intent (cancelled/refunded/…) is
|
|
133
|
+
// rejected by `apply_result` with `payment_control_result_terminal_intent`
|
|
134
|
+
// (ERRCODE 22023). The money question is already settled from the control
|
|
135
|
+
// plane's perspective, so this is a "no work left to do" outcome, not a
|
|
136
|
+
// transient upstream failure — ACK 200 so the provider stops
|
|
137
|
+
// retrying for days (retry-storm / charge-without-fulfillment risk). We
|
|
138
|
+
// match the RPC's exception text specifically, NOT the bare 22023 code,
|
|
139
|
+
// because 22023 is also raised for genuine mismatches (amount / currency /
|
|
140
|
+
// intent-not-found) that must still surface as 503.
|
|
141
|
+
if (event && /payment_control_result_terminal_intent/.test(message)) {
|
|
142
|
+
operationalEvents({
|
|
143
|
+
name: "provider_payment_result_after_terminal_intent",
|
|
144
|
+
domain: "payment",
|
|
145
|
+
surface: "webhook",
|
|
146
|
+
details: {
|
|
147
|
+
provider,
|
|
148
|
+
providerEventId: event.providerEventId,
|
|
149
|
+
providerPaymentId: event.providerPaymentId,
|
|
150
|
+
eventType: event.eventType,
|
|
151
|
+
occurredAt: event.occurredAt,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
sendSuccess(res, {
|
|
155
|
+
provider,
|
|
156
|
+
providerEventId: event.providerEventId,
|
|
157
|
+
acknowledged: true,
|
|
158
|
+
skipped: "result_terminal_intent",
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// The event cannot be attributed to the intent's exact active attempt:
|
|
163
|
+
// either it named another attempt (the slow-webhook shape and possible
|
|
164
|
+
// double charge) or ingest could not correlate it at all. Neither answer
|
|
165
|
+
// changes on redelivery, so ACK 200 instead of creating an endless retry
|
|
166
|
+
// loop. The durable inbound event and this reason route operator recovery.
|
|
167
|
+
// Match named reasons only, never the shared 22023 SQLSTATE.
|
|
168
|
+
if (event && /payment_control_result_attempt_(?:mismatch|unresolved)/.test(message)) {
|
|
169
|
+
const reason = message.includes("payment_control_result_attempt_unresolved")
|
|
170
|
+
? "attempt_unresolved"
|
|
171
|
+
: "attempt_mismatch";
|
|
172
|
+
operationalEvents({
|
|
173
|
+
name: "provider_payment_result_attempt_mismatch",
|
|
174
|
+
domain: "payment",
|
|
175
|
+
surface: "webhook",
|
|
176
|
+
// `providerPaymentId`, not `providerEventId`, is the operator's handle:
|
|
177
|
+
// the detail allowlist carries the former and drops the latter, and it
|
|
178
|
+
// is the value `ingest_event` correlated the attempt from.
|
|
179
|
+
details: {
|
|
180
|
+
provider,
|
|
181
|
+
providerPaymentId: event.providerPaymentId,
|
|
182
|
+
eventType: event.eventType,
|
|
183
|
+
resultStatus: mapResultStatus(event, refundEvidencePolicy),
|
|
184
|
+
occurredAt: event.occurredAt,
|
|
185
|
+
reason,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
sendSuccess(res, {
|
|
189
|
+
provider,
|
|
190
|
+
providerEventId: event.providerEventId,
|
|
191
|
+
acknowledged: true,
|
|
192
|
+
skipped: "result_attempt_mismatch",
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Provider webhook processing failed");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} };
|
|
200
|
+
}
|
|
201
|
+
function mapResultStatus(event, refundEvidencePolicy) {
|
|
202
|
+
if (event.eventType === "payment.succeeded")
|
|
203
|
+
return "succeeded";
|
|
204
|
+
if (event.eventType === "payment.failed")
|
|
205
|
+
return "failed";
|
|
206
|
+
if (event.eventType === "payment.refunded") {
|
|
207
|
+
if (refundEvidencePolicy === "require-evidence" && (event.refundKind == null || !event.providerPaymentId.trim() || !Number.isSafeInteger(event.amountMinor) || (event.amountMinor ?? 0) <= 0 || !event.currency?.trim()))
|
|
208
|
+
return null;
|
|
209
|
+
return event.refundKind === "partial" ? "partially_refunded" : "refunded";
|
|
210
|
+
}
|
|
211
|
+
if (event.eventType === "payment.disputed")
|
|
212
|
+
return "disputed";
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
// "Money moved" for reconciliation purposes: a settled charge, a refund, or a
|
|
216
|
+
// chargeback/dispute. `failed` (no money moved) and `null` (setup / non-money
|
|
217
|
+
// events) are excluded — an unmatched failed/setup event needs no reconciliation.
|
|
218
|
+
function isMoneyMoved(resultStatus) {
|
|
219
|
+
return resultStatus !== null && resultStatus !== "failed";
|
|
220
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReadinessReport, SchemaObservation } from "@openlup/core/readiness";
|
|
1
2
|
import type { BffResponder } from "../bff/index.js";
|
|
2
3
|
import type { CustomerAddressBookPort, CustomerAddressMutationPort, CustomerAddressesDeps } from "./addressPorts.js";
|
|
3
4
|
import type { CustomerAddressSchemas } from "./addressSchemas.js";
|
|
@@ -16,6 +17,10 @@ type Store = {
|
|
|
16
17
|
export interface ShippingBindingDependencies {
|
|
17
18
|
dataKind: string;
|
|
18
19
|
createManagedStore?: () => Store | null;
|
|
20
|
+
/** Core report for the selected shipping contribution, observed by the host. */
|
|
21
|
+
nativeRateReadiness?: ReadinessReport;
|
|
22
|
+
/** Same-database Core observation of the applied shipping seed and required objects. */
|
|
23
|
+
nativeRateSchema?: SchemaObservation;
|
|
19
24
|
nativeStore?: Omit<Store, "addressMutationPort"> | null;
|
|
20
25
|
responder: BffResponder;
|
|
21
26
|
selection?: {
|
|
@@ -5,7 +5,17 @@ import { createDeliveryOptionsHandler, createPickupPointSearchHandler, createPic
|
|
|
5
5
|
export function createShippingBindings(deps) {
|
|
6
6
|
const native = deps.dataKind === "postgres";
|
|
7
7
|
const addressWrites = native ? "shipping_native_address_writes_unsupported" : null;
|
|
8
|
-
const
|
|
8
|
+
const nativeRateReady = typeof deps.nativeStore?.shippingRateDataPort?.getShippingFlatMinor === "function"
|
|
9
|
+
&& typeof deps.nativeStore?.shippingRateDataPort?.setShippingFlatMinor === "function"
|
|
10
|
+
&& typeof deps.nativeStore?.shippingRateDataPort?.recordPromotionAudit === "function"
|
|
11
|
+
&& deps.nativeRateReadiness?.state === "ready"
|
|
12
|
+
&& typeof deps.nativeRateReadiness.candidate?.database === "string"
|
|
13
|
+
&& deps.nativeRateReadiness.candidate.database.length > 0
|
|
14
|
+
&& deps.nativeRateSchema?.database === deps.nativeRateReadiness.candidate.database
|
|
15
|
+
&& deps.nativeRateSchema.version === "20261021200000_shipping_flat_rate_seed"
|
|
16
|
+
&& ["public.commerce_settings", "public.commerce_settings.value_minor", "public.commerce_settings.updated_at"].every(name => deps.nativeRateSchema?.objects?.some(entry => entry.present === true && entry.object?.name === name
|
|
17
|
+
&& entry.object.kind === (name === "public.commerce_settings" ? "table" : "column")));
|
|
18
|
+
const flatRateSave = native && !nativeRateReady ? "shipping_native_flat_rate_save_unsupported" : null;
|
|
9
19
|
const refused = (reason) => ({
|
|
10
20
|
store: {}, handlers: { options: null, search: null, validate: null, addresses: null, rate: null },
|
|
11
21
|
readiness: { ready: false, reason }, unsupported: { addressWrites, flatRateSave },
|
|
@@ -14,7 +24,7 @@ export function createShippingBindings(deps) {
|
|
|
14
24
|
return refused("shipping_store_incompatible");
|
|
15
25
|
if (native && deps.addresses?.requireWrites)
|
|
16
26
|
return refused(addressWrites);
|
|
17
|
-
if (native && deps.rate)
|
|
27
|
+
if (native && deps.rate && flatRateSave)
|
|
18
28
|
return refused(flatRateSave);
|
|
19
29
|
const selected = native ? deps.nativeStore : deps.createManagedStore?.();
|
|
20
30
|
if ((deps.addresses || deps.rate) && !selected)
|
package/dist/shipping/index.d.ts
CHANGED
|
@@ -18,3 +18,17 @@ export type { AdminShippingRateHandlerDeps, ShippingRateDataPort, ShippingRateHa
|
|
|
18
18
|
export { createShippingBindings, type ShippingBindingDependencies, type ShippingBindings } from "./bindings.js";
|
|
19
19
|
export type { ShippingTerms, ShippingPolicyEntry, ShippingPolicy, ShippingSettings } from "./contracts/threshold.js";
|
|
20
20
|
export { shippingTermsSchema, shippingPolicySchema, parseShippingTerms, hasShippingTerms, normalizeShippingPolicy, resolveShippingPolicy, resolveThresholdShipping } from "./thresholdPolicy.js";
|
|
21
|
+
/** Sparse canonical projections, never an install chain. */
|
|
22
|
+
export declare const SHIPPING_MIGRATIONS: readonly [{
|
|
23
|
+
readonly id: "20260807140000_read_parity_catalog";
|
|
24
|
+
readonly catalogPath: "db/platform/migrations/20260807140000_read_parity_catalog.sql";
|
|
25
|
+
readonly position: 1;
|
|
26
|
+
readonly sha256: "fcf3827028c1651ae42a7c7d7472153825a3121c936d58a3d105befc7dece9ea";
|
|
27
|
+
readonly url: URL;
|
|
28
|
+
}, {
|
|
29
|
+
readonly id: "20261021200000_shipping_flat_rate_seed";
|
|
30
|
+
readonly catalogPath: "db/platform/migrations/20261021200000_shipping_flat_rate_seed.sql";
|
|
31
|
+
readonly position: 72;
|
|
32
|
+
readonly sha256: "34f8696f515a2cc072395db5a3de2d9b09643d13236ee3851d0d78123f907eae";
|
|
33
|
+
readonly url: URL;
|
|
34
|
+
}];
|
package/dist/shipping/index.js
CHANGED
|
@@ -8,3 +8,8 @@ export { readAddresses, readOrdererProfiles, addressUsedByActiveSubscription, ad
|
|
|
8
8
|
export { createAdminShippingRateHandler } from "./adminShippingRateHandler.js";
|
|
9
9
|
export { createShippingBindings } from "./bindings.js";
|
|
10
10
|
export { shippingTermsSchema, shippingPolicySchema, parseShippingTerms, hasShippingTerms, normalizeShippingPolicy, resolveShippingPolicy, resolveThresholdShipping } from "./thresholdPolicy.js";
|
|
11
|
+
/** Sparse canonical projections, never an install chain. */
|
|
12
|
+
export const SHIPPING_MIGRATIONS = [
|
|
13
|
+
{ id: "20260807140000_read_parity_catalog", catalogPath: "db/platform/migrations/20260807140000_read_parity_catalog.sql", position: 1, sha256: "fcf3827028c1651ae42a7c7d7472153825a3121c936d58a3d105befc7dece9ea", url: new URL("../../sql/20260807140000_read_parity_catalog.sql", import.meta.url) },
|
|
14
|
+
{ id: "20261021200000_shipping_flat_rate_seed", catalogPath: "db/platform/migrations/20261021200000_shipping_flat_rate_seed.sql", position: 72, sha256: "34f8696f515a2cc072395db5a3de2d9b09643d13236ee3851d0d78123f907eae", url: new URL("../../sql/20261021200000_shipping_flat_rate_seed.sql", import.meta.url) },
|
|
15
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlup/platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Runnable server substrate: Node HTTP runtime, BFF dispatch, scheduler, storage, data gateways and bundle composition; development preview.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"api:update": "npm run build && node --experimental-strip-types ./scripts/api-contract.ts --update"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@openlup/core": "0.
|
|
34
|
+
"@openlup/core": "0.21.0",
|
|
35
35
|
"pg": "^8.22.0",
|
|
36
36
|
"@aws-sdk/client-s3": "^3.1144.0",
|
|
37
37
|
"@types/pg": "^8.20.0"
|
|
@@ -175,6 +175,26 @@
|
|
|
175
175
|
"types": "./dist/shipping/contracts/index.d.ts",
|
|
176
176
|
"import": "./dist/shipping/contracts/index.js",
|
|
177
177
|
"default": "./dist/shipping/contracts/index.js"
|
|
178
|
+
},
|
|
179
|
+
"./payments": {
|
|
180
|
+
"types": "./dist/payments/index.d.ts",
|
|
181
|
+
"import": "./dist/payments/index.js",
|
|
182
|
+
"default": "./dist/payments/index.js"
|
|
183
|
+
},
|
|
184
|
+
"./payments/contracts": {
|
|
185
|
+
"types": "./dist/payments/contracts/index.d.ts",
|
|
186
|
+
"import": "./dist/payments/contracts/index.js",
|
|
187
|
+
"default": "./dist/payments/contracts/index.js"
|
|
188
|
+
},
|
|
189
|
+
"./payments/stripe": {
|
|
190
|
+
"types": "./dist/payments/stripe/index.d.ts",
|
|
191
|
+
"import": "./dist/payments/stripe/index.js",
|
|
192
|
+
"default": "./dist/payments/stripe/index.js"
|
|
193
|
+
},
|
|
194
|
+
"./payments/tpay": {
|
|
195
|
+
"types": "./dist/payments/tpay/index.d.ts",
|
|
196
|
+
"import": "./dist/payments/tpay/index.js",
|
|
197
|
+
"default": "./dist/payments/tpay/index.js"
|
|
178
198
|
}
|
|
179
199
|
},
|
|
180
200
|
"repository": {
|