@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,303 @@
|
|
|
1
|
+
import type { ProviderReadPurpose } from "./evidence.js";
|
|
2
|
+
export type ReconciliationProviderKind = string;
|
|
3
|
+
export type ProviderReconciliationStatusKind = "succeeded" | "failed" | "pending" | "unknown";
|
|
4
|
+
export interface ProviderReconciliationStatus {
|
|
5
|
+
status: ProviderReconciliationStatusKind;
|
|
6
|
+
providerStatus: string;
|
|
7
|
+
occurredAt: string | null;
|
|
8
|
+
failureReason: string | null;
|
|
9
|
+
amountMinor: number | null;
|
|
10
|
+
currency: string | null;
|
|
11
|
+
rawPayload: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Who is asking, and therefore what an unresolved provider state MEANS.
|
|
15
|
+
*
|
|
16
|
+
* The same read answers two different questions. Reconciliation — the caller
|
|
17
|
+
* that names no purpose — asks "is this attempt over", long after the buyer
|
|
18
|
+
* left, and an intent still waiting for its first payment method is over. A
|
|
19
|
+
* buyer standing in a live checkout asks "is this attempt settled yet", and the
|
|
20
|
+
* same state means the opposite: nobody has been asked anything.
|
|
21
|
+
*
|
|
22
|
+
* ⛔ Omitting the purpose keeps the reconciliation reading. Terminalizing real
|
|
23
|
+
* abandonment is the more expensive of the two errors to lose, so it is the
|
|
24
|
+
* behaviour a caller gets by default rather than one it must opt into.
|
|
25
|
+
*/
|
|
26
|
+
export interface PaymentProviderReconciliationProvider {
|
|
27
|
+
readPayment(input: {
|
|
28
|
+
providerPaymentId: string;
|
|
29
|
+
attempt?: ClaimedPaymentAttempt;
|
|
30
|
+
purpose?: ProviderReadPurpose;
|
|
31
|
+
}): Promise<ProviderReconciliationStatus>;
|
|
32
|
+
/** Atomically close an incomplete provider object before minting a replacement. */
|
|
33
|
+
closePayment?(input: {
|
|
34
|
+
providerPaymentId: string;
|
|
35
|
+
}): Promise<ProviderReconciliationStatus>;
|
|
36
|
+
/** Exact provider-side absence only; missing local acknowledgement is not
|
|
37
|
+
* absence proof and must never authorize another charge. */
|
|
38
|
+
findPaymentByLocalIntent?(input: {
|
|
39
|
+
attempt: ClaimedPaymentAttempt;
|
|
40
|
+
}): Promise<"found" | "absent">;
|
|
41
|
+
}
|
|
42
|
+
export interface ClaimedPaymentAttempt {
|
|
43
|
+
paymentAttemptId: string;
|
|
44
|
+
paymentIntentId: string;
|
|
45
|
+
paymentId: string;
|
|
46
|
+
orderId: string;
|
|
47
|
+
subscriptionId: string | null;
|
|
48
|
+
subscriptionCycleId: string | null;
|
|
49
|
+
provider: ReconciliationProviderKind;
|
|
50
|
+
providerPaymentId: string | null;
|
|
51
|
+
providerAttemptId: string | null;
|
|
52
|
+
providerSessionId: string | null;
|
|
53
|
+
attemptStatus: string;
|
|
54
|
+
intentStatus: string;
|
|
55
|
+
amountMinor: number;
|
|
56
|
+
currency: string;
|
|
57
|
+
orderMode: "one_time" | "subscription_cycle";
|
|
58
|
+
cycleRetryAttempt: number;
|
|
59
|
+
cycleNextRetryAt: string | null;
|
|
60
|
+
localUpdatedAt: string;
|
|
61
|
+
}
|
|
62
|
+
export type ManualInteractivePreparedAttemptAbsenceEvidence = {
|
|
63
|
+
providerAbsenceConfirmed: true;
|
|
64
|
+
watchdogEvidence: true;
|
|
65
|
+
evidenceCode: "operator_verified_absence" | "operator_documented_absence";
|
|
66
|
+
};
|
|
67
|
+
export interface InteractivePreparedAttemptReopenResult {
|
|
68
|
+
paymentAttemptId: string;
|
|
69
|
+
paymentIntentId: string;
|
|
70
|
+
paymentAttemptStatus: "failed";
|
|
71
|
+
paymentIntentStatus: "failed";
|
|
72
|
+
replayed: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface PaymentProviderReconciliationPort {
|
|
75
|
+
claimPreparedAttempts(input: {
|
|
76
|
+
now: string;
|
|
77
|
+
staleAfterSeconds: number;
|
|
78
|
+
limit: number;
|
|
79
|
+
claimKey: string;
|
|
80
|
+
}): Promise<ClaimedPaymentAttempt[]>;
|
|
81
|
+
claimStaleAttempts(input: {
|
|
82
|
+
now: string;
|
|
83
|
+
staleAfterSeconds: number;
|
|
84
|
+
limit: number;
|
|
85
|
+
claimKey: string;
|
|
86
|
+
}): Promise<ClaimedPaymentAttempt[]>;
|
|
87
|
+
recordEvidence(input: {
|
|
88
|
+
idempotencyKey: string;
|
|
89
|
+
provider: ReconciliationProviderKind;
|
|
90
|
+
providerPaymentId: string | null;
|
|
91
|
+
paymentIntentId: string;
|
|
92
|
+
paymentAttemptId: string;
|
|
93
|
+
localStatus: string;
|
|
94
|
+
providerStatus: string;
|
|
95
|
+
correctionStatus: "observed" | "corrected" | "ignored" | "failed";
|
|
96
|
+
checkedAt: string;
|
|
97
|
+
payload: Record<string, unknown>;
|
|
98
|
+
}): Promise<{
|
|
99
|
+
replayed: boolean;
|
|
100
|
+
}>;
|
|
101
|
+
applyTerminalResult(input: {
|
|
102
|
+
idempotencyKey: string;
|
|
103
|
+
expectedOrderId: string;
|
|
104
|
+
expectedPaymentIntentId: string;
|
|
105
|
+
expectedPaymentAttemptId: string;
|
|
106
|
+
expectedPaymentId: string;
|
|
107
|
+
provider: ReconciliationProviderKind;
|
|
108
|
+
providerPaymentId: string;
|
|
109
|
+
localStatus: string;
|
|
110
|
+
providerStatus: string;
|
|
111
|
+
resultStatus: "succeeded" | "failed";
|
|
112
|
+
occurredAt: string;
|
|
113
|
+
failureReason: string | null;
|
|
114
|
+
/**
|
|
115
|
+
* The class the reading adapter already decided for this refusal, persisted
|
|
116
|
+
* beside the refusal itself on both the attempt and the dunning case the
|
|
117
|
+
* terminal write opens.
|
|
118
|
+
*
|
|
119
|
+
* OPTIONAL, and absence is a real state rather than a default: a caller that
|
|
120
|
+
* omits it writes exactly what it wrote before this field existed, because
|
|
121
|
+
* the RPC coalesces it to NULL and the SQL treats a NULL class as "stamp
|
|
122
|
+
* nothing" rather than "stamp NULL". Two of this port's three consumers
|
|
123
|
+
* (`paymentVerifyNowService`, `checkoutRecoveryPaymentResolver`) resolve a
|
|
124
|
+
* payment without classifying it and are pinned to stay classification-absent.
|
|
125
|
+
*
|
|
126
|
+
* Shape mirrors `applyPaymentResultRpc` deliberately, so the synchronous
|
|
127
|
+
* decline rail and this polling rail hand the control plane the same object.
|
|
128
|
+
*/
|
|
129
|
+
failureClassification?: {
|
|
130
|
+
failureClass: string;
|
|
131
|
+
decidedBy: string;
|
|
132
|
+
} | null;
|
|
133
|
+
checkedAt: string;
|
|
134
|
+
payload: Record<string, unknown>;
|
|
135
|
+
}): Promise<{
|
|
136
|
+
replayed: boolean;
|
|
137
|
+
paymentResult: {
|
|
138
|
+
paymentIntentId: string;
|
|
139
|
+
paymentAttemptId: string;
|
|
140
|
+
paymentId: string;
|
|
141
|
+
orderId: string;
|
|
142
|
+
status: string;
|
|
143
|
+
kind: string;
|
|
144
|
+
replayed: boolean;
|
|
145
|
+
};
|
|
146
|
+
correctionStatus: "corrected" | "ignored";
|
|
147
|
+
subscriptionWebhookDunning: {
|
|
148
|
+
opened: boolean;
|
|
149
|
+
replayed: boolean;
|
|
150
|
+
caseId: string | null;
|
|
151
|
+
} | null;
|
|
152
|
+
}>;
|
|
153
|
+
reopenPreparedAttemptAfterAbsence(input: {
|
|
154
|
+
idempotencyKey: string;
|
|
155
|
+
paymentAttemptId: string;
|
|
156
|
+
expectedPaymentIntentId: string;
|
|
157
|
+
expectedSubscriptionCycleId: string;
|
|
158
|
+
operatorRef: string;
|
|
159
|
+
absenceCheckedAt: string;
|
|
160
|
+
nextRetryAt: string;
|
|
161
|
+
absenceEvidence: Record<string, unknown>;
|
|
162
|
+
}): Promise<{
|
|
163
|
+
replayed: boolean;
|
|
164
|
+
}>;
|
|
165
|
+
/**
|
|
166
|
+
* Optional because the automatic reconciliation worker and its provider
|
|
167
|
+
* neutral fixtures must not acquire an operator-only interactive capability.
|
|
168
|
+
* A caller that selects this rail must require the narrow interface below.
|
|
169
|
+
*/
|
|
170
|
+
reopenInteractivePreparedAttemptAfterAbsence?(input: {
|
|
171
|
+
idempotencyKey: string;
|
|
172
|
+
paymentAttemptId: string;
|
|
173
|
+
expectedPaymentIntentId: string;
|
|
174
|
+
expectedOrderId: string;
|
|
175
|
+
expectedSubscriptionId: string | null;
|
|
176
|
+
expectedSubscriptionCycleId: string | null;
|
|
177
|
+
operatorRef: string;
|
|
178
|
+
absenceCheckedAt: string;
|
|
179
|
+
absenceEvidence: ManualInteractivePreparedAttemptAbsenceEvidence;
|
|
180
|
+
}): Promise<InteractivePreparedAttemptReopenResult>;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Service-role operator boundary for an interactive checkout, distinct from
|
|
184
|
+
* the renewal retry RPC. It terminalizes the local prepared attempt after a
|
|
185
|
+
* human/provider absence confirmation; it never schedules dunning or cron.
|
|
186
|
+
*/
|
|
187
|
+
export interface InteractivePreparedAttemptManualReconciliationPort extends PaymentProviderReconciliationPort {
|
|
188
|
+
reopenInteractivePreparedAttemptAfterAbsence(input: {
|
|
189
|
+
idempotencyKey: string;
|
|
190
|
+
paymentAttemptId: string;
|
|
191
|
+
expectedPaymentIntentId: string;
|
|
192
|
+
expectedOrderId: string;
|
|
193
|
+
expectedSubscriptionId: string | null;
|
|
194
|
+
expectedSubscriptionCycleId: string | null;
|
|
195
|
+
operatorRef: string;
|
|
196
|
+
absenceCheckedAt: string;
|
|
197
|
+
absenceEvidence: ManualInteractivePreparedAttemptAbsenceEvidence;
|
|
198
|
+
}): Promise<InteractivePreparedAttemptReopenResult>;
|
|
199
|
+
}
|
|
200
|
+
export interface PaymentProviderReconciliationWorkerResult {
|
|
201
|
+
ok: boolean;
|
|
202
|
+
checked: number;
|
|
203
|
+
corrected: number;
|
|
204
|
+
succeeded: number;
|
|
205
|
+
failed: number;
|
|
206
|
+
pending: number;
|
|
207
|
+
unknown: number;
|
|
208
|
+
ignored: number;
|
|
209
|
+
dunningOpened: number;
|
|
210
|
+
replayed: number;
|
|
211
|
+
providerCalls: number;
|
|
212
|
+
preparedWithoutProviderAck: number;
|
|
213
|
+
preparedAttemptsReopened: number;
|
|
214
|
+
manualReview: number;
|
|
215
|
+
/**
|
|
216
|
+
* Attempts whose rail has answered non-terminally for longer than that rail
|
|
217
|
+
* declares normal. Counted, never acted on: elapsed time is not evidence
|
|
218
|
+
* about money, so this surfaces a case for a human rather than resolving it.
|
|
219
|
+
*/
|
|
220
|
+
silenceOverdue: number;
|
|
221
|
+
amountCurrencyMismatches: number;
|
|
222
|
+
failures: number;
|
|
223
|
+
skipped: boolean;
|
|
224
|
+
reason?: string;
|
|
225
|
+
}
|
|
226
|
+
export interface PaymentProviderReconciliationWorkerInput {
|
|
227
|
+
port: PaymentProviderReconciliationPort;
|
|
228
|
+
providers: Partial<Record<ReconciliationProviderKind, PaymentProviderReconciliationProvider>>;
|
|
229
|
+
now?: string;
|
|
230
|
+
staleAfterSeconds?: number;
|
|
231
|
+
batchSize?: number;
|
|
232
|
+
claimKey?: string;
|
|
233
|
+
/**
|
|
234
|
+
* When true, prepared attempts whose provider proves absence (see
|
|
235
|
+
* PaymentProviderReconciliationProvider.findPaymentByLocalIntent) and that
|
|
236
|
+
* are at least MIN_PREPARED_ABSENCE_REOPEN_AGE_SECONDS old are automatically
|
|
237
|
+
* reopened for cron retry via the operator RPC. Default false — the
|
|
238
|
+
* stranded-attempt loop stays operator-manual unless the flag is on.
|
|
239
|
+
*/
|
|
240
|
+
autoReopenPreparedAbsence?: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* Published rail capabilities, read to learn how long a non-terminal answer
|
|
243
|
+
* from each rail is still normal. Optional: without it no attempt is ever
|
|
244
|
+
* counted overdue, so omitting it preserves today's behaviour exactly.
|
|
245
|
+
*/
|
|
246
|
+
capabilities?: SilenceWindowLookup;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The narrow slice of the capability registry this worker reads.
|
|
250
|
+
*
|
|
251
|
+
* Declared here rather than importing the whole registry type so the worker
|
|
252
|
+
* depends on the one question it asks - "when does this rail's silence stop
|
|
253
|
+
* being normal" - and not on the shape of every unattended-charge capability.
|
|
254
|
+
*/
|
|
255
|
+
export interface SilenceWindowLookup {
|
|
256
|
+
get(providerKind: string): {
|
|
257
|
+
terminalOutcomeReporting?: {
|
|
258
|
+
silenceBecomesSuspectAfterMinutes: number;
|
|
259
|
+
};
|
|
260
|
+
} | null;
|
|
261
|
+
}
|
|
262
|
+
export interface VerifiableAttemptSnapshot {
|
|
263
|
+
orderId: string;
|
|
264
|
+
orderClientId: string;
|
|
265
|
+
orderMode: string;
|
|
266
|
+
paymentIntentId: string;
|
|
267
|
+
intentStatus: string;
|
|
268
|
+
intentProviderPaymentId: string | null;
|
|
269
|
+
paymentAttemptId: string;
|
|
270
|
+
paymentId: string;
|
|
271
|
+
attemptStatus: string;
|
|
272
|
+
provider: string;
|
|
273
|
+
providerAttemptId: string | null;
|
|
274
|
+
providerSessionId: string | null;
|
|
275
|
+
amountMinor: number;
|
|
276
|
+
currency: string;
|
|
277
|
+
localUpdatedAt: string;
|
|
278
|
+
}
|
|
279
|
+
export interface PaymentVerifyReadPort {
|
|
280
|
+
readVerifiableAttempt(input: {
|
|
281
|
+
orderId: string;
|
|
282
|
+
paymentIntentId: string;
|
|
283
|
+
}): Promise<VerifiableAttemptSnapshot | null>;
|
|
284
|
+
}
|
|
285
|
+
export type PaymentVerifyNowResult = {
|
|
286
|
+
status: string;
|
|
287
|
+
verified: false;
|
|
288
|
+
applied: false;
|
|
289
|
+
providerPaymentId: string | null;
|
|
290
|
+
attempt: ClaimedPaymentAttempt | null;
|
|
291
|
+
} | {
|
|
292
|
+
status: "unavailable" | "pending" | "manual_review";
|
|
293
|
+
verified: boolean;
|
|
294
|
+
applied: false;
|
|
295
|
+
providerPaymentId: string | null;
|
|
296
|
+
attempt: ClaimedPaymentAttempt | null;
|
|
297
|
+
} | {
|
|
298
|
+
status: "succeeded" | "failed";
|
|
299
|
+
verified: true;
|
|
300
|
+
applied: boolean;
|
|
301
|
+
providerPaymentId: string;
|
|
302
|
+
attempt: ClaimedPaymentAttempt;
|
|
303
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { PaymentMethodHandoff } from "./adapter.js";
|
|
2
|
+
import type { ClaimedPaymentAttempt, PaymentProviderReconciliationProvider, ProviderReconciliationStatus } from "./reconciliation.js";
|
|
3
|
+
import type { ProviderReadPurpose } from "./evidence.js";
|
|
4
|
+
export type CheckoutRecoveryPaymentResolution = {
|
|
5
|
+
kind: "paid";
|
|
6
|
+
} | {
|
|
7
|
+
kind: "retry_new";
|
|
8
|
+
} | {
|
|
9
|
+
kind: "resume_existing";
|
|
10
|
+
clientAction: PaymentRecoveryAction;
|
|
11
|
+
} | {
|
|
12
|
+
kind: "awaiting_provider";
|
|
13
|
+
} | {
|
|
14
|
+
kind: "manual_review";
|
|
15
|
+
};
|
|
16
|
+
export interface ProviderPaymentReferenceInput {
|
|
17
|
+
providerAttemptId: string | null;
|
|
18
|
+
providerSessionId: string | null;
|
|
19
|
+
intentProviderPaymentId: string | null;
|
|
20
|
+
}
|
|
21
|
+
export type PaymentRecoveryAction = {
|
|
22
|
+
provider: string;
|
|
23
|
+
} & PaymentMethodHandoff;
|
|
24
|
+
export interface ProviderRecoveryRead {
|
|
25
|
+
status: ProviderReconciliationStatus;
|
|
26
|
+
identityMatches: boolean;
|
|
27
|
+
configuredMoneyMatches: boolean;
|
|
28
|
+
manualReviewRequired: boolean;
|
|
29
|
+
clientAction: PaymentRecoveryAction | null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Recovery is an explicit capability, not an optional method on generic
|
|
33
|
+
* reconciliation providers. One read returns both PSP truth and any transient
|
|
34
|
+
* buyer action, so a redeem never calls the provider twice.
|
|
35
|
+
*/
|
|
36
|
+
export interface PaymentProviderRecoveryProvider extends PaymentProviderReconciliationProvider {
|
|
37
|
+
resolvePaymentReference(input: ProviderPaymentReferenceInput): string | null;
|
|
38
|
+
readRecoveryPayment(input: {
|
|
39
|
+
providerPaymentId: string;
|
|
40
|
+
attempt: ClaimedPaymentAttempt;
|
|
41
|
+
purpose?: ProviderReadPurpose;
|
|
42
|
+
}): Promise<ProviderRecoveryRead>;
|
|
43
|
+
}
|
|
44
|
+
export interface CheckoutRecoveryPaymentResolver {
|
|
45
|
+
resolve(input: {
|
|
46
|
+
orderId: string;
|
|
47
|
+
paymentIntentId: string | null;
|
|
48
|
+
intentStatus?: string | null;
|
|
49
|
+
}): Promise<CheckoutRecoveryPaymentResolution>;
|
|
50
|
+
}
|
|
51
|
+
export interface ActivePaymentActionAuthority {
|
|
52
|
+
orderId: string;
|
|
53
|
+
clientId: string;
|
|
54
|
+
paymentIntentId: string;
|
|
55
|
+
paymentAttemptId: string;
|
|
56
|
+
executionRail: string;
|
|
57
|
+
}
|
|
58
|
+
export interface CheckoutActivePaymentActionResolver {
|
|
59
|
+
readActiveAction(authority: ActivePaymentActionAuthority): Promise<PaymentRecoveryAction | null>;
|
|
60
|
+
}
|
|
61
|
+
export type CheckoutActionReadProvider = Pick<PaymentProviderRecoveryProvider, "resolvePaymentReference" | "readRecoveryPayment">;
|
|
62
|
+
/** Method collection only. Hosts authorize ownership before calling this port.
|
|
63
|
+
* The result never confirms payment, redeems a token or writes a subscription.
|
|
64
|
+
* Handoffs remain transient; unsupported must be shown as an explicit refusal.
|
|
65
|
+
*/
|
|
66
|
+
export interface PaymentRepairMethodPort {
|
|
67
|
+
setupMethod(input: {
|
|
68
|
+
customer: string;
|
|
69
|
+
metadata: Record<string, string>;
|
|
70
|
+
idempotencyKey: string;
|
|
71
|
+
}): Promise<{
|
|
72
|
+
kind: "ready";
|
|
73
|
+
setupRef: string;
|
|
74
|
+
handoff: PaymentMethodHandoff;
|
|
75
|
+
replayed: boolean;
|
|
76
|
+
} | {
|
|
77
|
+
kind: "unsupported";
|
|
78
|
+
}>;
|
|
79
|
+
}
|
|
80
|
+
/** Host-selected durable truth scope. Unsupported schema never opens a store. */
|
|
81
|
+
export interface PaymentServicesOptions {
|
|
82
|
+
schema?: {
|
|
83
|
+
truth: "supported" | "unsupported";
|
|
84
|
+
};
|
|
85
|
+
openTruth?: () => Promise<{
|
|
86
|
+
port: import("./truth.js").PaymentTruthPort;
|
|
87
|
+
close(): Promise<void>;
|
|
88
|
+
}>;
|
|
89
|
+
}
|
|
90
|
+
export interface PaymentServices {
|
|
91
|
+
run<T>(work: (port: import("./truth.js").PaymentTruthPort) => Promise<T>): Promise<T>;
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export type PaymentTruthOutcome = "captured" | "refused" | "indeterminate";
|
|
2
|
+
/**
|
|
3
|
+
* How a refusal looked to the settlement source that reported it.
|
|
4
|
+
*
|
|
5
|
+
* NOT the retry taxonomy. `PaymentFailureClass` in `@openlup/core/payment` is a
|
|
6
|
+
* different vocabulary with no value in common: that one answers "may this be
|
|
7
|
+
* retried, and how", and is what `commerce_payment_attempts.failure_class`
|
|
8
|
+
* persists. This one answers "what did the source say happened" and never leaves
|
|
9
|
+
* this module's reconciliation ledger. They shared a name until this wave, which
|
|
10
|
+
* meant an import could quietly resolve to the wrong five strings.
|
|
11
|
+
*/
|
|
12
|
+
export type PaymentTruthRefusalClass = "authorization_refused" | "insufficient_funds" | "method_unavailable" | "risk_refused" | "unknown_refusal";
|
|
13
|
+
export type PaymentSourceEvidence = {
|
|
14
|
+
sourceKind: "accepted_event" | "reconciliation_read";
|
|
15
|
+
sourceReference: string;
|
|
16
|
+
observedStatus: string;
|
|
17
|
+
observedAt: string;
|
|
18
|
+
payloadFingerprint: string;
|
|
19
|
+
};
|
|
20
|
+
export type PaymentTruthEvent = {
|
|
21
|
+
sourceEventId: string;
|
|
22
|
+
idempotencyKey: string;
|
|
23
|
+
settlementIntentId: string;
|
|
24
|
+
outcome: PaymentTruthOutcome;
|
|
25
|
+
amountMinor: number;
|
|
26
|
+
currency: string;
|
|
27
|
+
occurredAt: string;
|
|
28
|
+
failure: {
|
|
29
|
+
classification: PaymentTruthRefusalClass;
|
|
30
|
+
reason: string | null;
|
|
31
|
+
} | null;
|
|
32
|
+
evidence: PaymentSourceEvidence;
|
|
33
|
+
};
|
|
34
|
+
export type PaymentReconciliationResult = {
|
|
35
|
+
eventId: string;
|
|
36
|
+
idempotencyKey: string;
|
|
37
|
+
evidenceStatus: "observed" | "failed";
|
|
38
|
+
outcome: PaymentTruthOutcome;
|
|
39
|
+
occurredAt: string;
|
|
40
|
+
failure: PaymentTruthEvent["failure"];
|
|
41
|
+
evidence: PaymentSourceEvidence;
|
|
42
|
+
};
|
|
43
|
+
export type PaymentTruthReadback = {
|
|
44
|
+
event: {
|
|
45
|
+
id: string;
|
|
46
|
+
sourceEventId: string;
|
|
47
|
+
fingerprint: string;
|
|
48
|
+
state: "open" | "settled";
|
|
49
|
+
outcome: PaymentTruthOutcome;
|
|
50
|
+
replayed: boolean;
|
|
51
|
+
};
|
|
52
|
+
effects: {
|
|
53
|
+
paymentResultRecorded: boolean;
|
|
54
|
+
dunningOpened: boolean;
|
|
55
|
+
accountingRequested: boolean;
|
|
56
|
+
};
|
|
57
|
+
financial: {
|
|
58
|
+
settlementStatus: string | null;
|
|
59
|
+
orderStatus: string | null;
|
|
60
|
+
subscriptionStatus: string | null;
|
|
61
|
+
accountingStatus: string | null;
|
|
62
|
+
amountMinor: number;
|
|
63
|
+
currency: string;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
export interface PaymentTruthPort {
|
|
67
|
+
ingestEvent(input: PaymentTruthEvent): Promise<PaymentTruthReadback>;
|
|
68
|
+
recordReconciliation(input: PaymentReconciliationResult): Promise<PaymentTruthReadback>;
|
|
69
|
+
sweepOpenEvents(input: {
|
|
70
|
+
now: string;
|
|
71
|
+
limit: number;
|
|
72
|
+
}): Promise<{
|
|
73
|
+
claimed: number;
|
|
74
|
+
settledIgnored: number;
|
|
75
|
+
eventIds: string[];
|
|
76
|
+
}>;
|
|
77
|
+
readEvent(eventId: string): Promise<PaymentTruthReadback | null>;
|
|
78
|
+
}
|
|
79
|
+
export type CanonicalPaymentSource = {
|
|
80
|
+
sourceEventId: string;
|
|
81
|
+
eventKind: string;
|
|
82
|
+
settlementIntentId: string | null;
|
|
83
|
+
amountMinor: number | null;
|
|
84
|
+
currency: string | null;
|
|
85
|
+
occurredAt: string | null;
|
|
86
|
+
payloadFingerprint: string;
|
|
87
|
+
};
|
|
88
|
+
export type PostgresPaymentTruthOptions = {
|
|
89
|
+
dunningTemplateSlug: string;
|
|
90
|
+
recoveryUrlPath: string;
|
|
91
|
+
recoveryTemplateSlug: string;
|
|
92
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PaymentExecutionBaseInput } from "@openlup/core/testing";
|
|
2
|
+
import type { SimulatorResult, ExecutionSubject, ExecutionProviderIdentity, ExecutionEvidence, DurableEventCardinalityExpectations, DurableEventCapability, DurableEventEvidence } from "./contracts/conformance.js";
|
|
3
|
+
/** Framework-free execution proof; durable event evidence is a separate capability. */
|
|
4
|
+
export declare function runExecutionConformance<TInput extends PaymentExecutionBaseInput, TResult extends SimulatorResult>(input: ExecutionProviderIdentity & {
|
|
5
|
+
readonly captured: ExecutionSubject<TInput, TResult>;
|
|
6
|
+
readonly capturedLedger: {
|
|
7
|
+
cardinality(): number;
|
|
8
|
+
};
|
|
9
|
+
readonly refused: ExecutionSubject<TInput, TResult>;
|
|
10
|
+
readonly request: TInput;
|
|
11
|
+
}): Promise<ExecutionEvidence>;
|
|
12
|
+
export declare function runDurableEventConformance(subject: DurableEventCapability, expected: DurableEventCardinalityExpectations): Promise<DurableEventEvidence>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { validatePaymentExecutionContractResult, } from "@openlup/core/testing";
|
|
2
|
+
/** Framework-free execution proof; durable event evidence is a separate capability. */
|
|
3
|
+
export async function runExecutionConformance(input) {
|
|
4
|
+
assertEqual(input.capturedLedger.cardinality(), 0, "initial captured ledger cardinality");
|
|
5
|
+
const captured = await input.captured.execute(input.request);
|
|
6
|
+
const refused = await input.refused.execute(input.request);
|
|
7
|
+
for (const result of [captured, refused]) {
|
|
8
|
+
const validation = validatePaymentExecutionContractResult({
|
|
9
|
+
input: input.request,
|
|
10
|
+
result,
|
|
11
|
+
expectedProvider: input.expectedProvider,
|
|
12
|
+
});
|
|
13
|
+
if (!validation.ok || validation.issues.length) {
|
|
14
|
+
throw new Error(`Invalid simulator payment execution result: ${JSON.stringify(validation.issues)}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
assertEqual(captured.responsePayload.outcome, "captured", "captured subject outcome");
|
|
18
|
+
assertEqual(refused.responsePayload.outcome, "refused", "refused subject outcome");
|
|
19
|
+
const results = [captured, refused];
|
|
20
|
+
const providerCalls = results.filter((result) => result.responsePayload.providerCall === true).length;
|
|
21
|
+
const capturedEffects = input.capturedLedger.cardinality();
|
|
22
|
+
const permanentRefusals = results.filter((result) => result.responsePayload.outcome === "refused"
|
|
23
|
+
&& result.providerDecline?.code === "payment_failed" && result.providerDecline.mandateUnsupported === false).length;
|
|
24
|
+
if (capturedEffects !== 1 || permanentRefusals !== 1) {
|
|
25
|
+
throw new Error("Expected one captured effect and one permanent simulator refusal.");
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
capability: { name: "simulator-execution", durabilityClaim: false },
|
|
29
|
+
executions: results.length,
|
|
30
|
+
providerCalls,
|
|
31
|
+
capturedEffects,
|
|
32
|
+
permanentRefusals,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function runDurableEventConformance(subject, expected) {
|
|
36
|
+
subject.assertSafe();
|
|
37
|
+
let evidence;
|
|
38
|
+
try {
|
|
39
|
+
assertEqual((await subject.reset()).residualRows, 0, "baseline residual rows");
|
|
40
|
+
await subject.seed();
|
|
41
|
+
const baseline = await subject.snapshot();
|
|
42
|
+
assertCoreCounts(baseline.counts, expected.fixture, expected.baseline);
|
|
43
|
+
const aSuccess = await deliver(subject, "outOfOrder", "a-success", "succeeded", "2026-07-29T10:00:00.000Z");
|
|
44
|
+
const afterOutOfOrderSuccess = await subject.snapshot();
|
|
45
|
+
assertEvent(afterOutOfOrderSuccess, aSuccess, "a-success", "outOfOrder", "processed");
|
|
46
|
+
assertCoreCounts(afterOutOfOrderSuccess.counts, expected.fixture, expected.afterOutOfOrderSuccess);
|
|
47
|
+
assertSucceeded(afterOutOfOrderSuccess.aggregates.outOfOrder);
|
|
48
|
+
const aFailure = await deliver(subject, "outOfOrder", "a-failure", "failed", "2026-07-29T09:00:00.000Z");
|
|
49
|
+
const afterOutOfOrderFailure = await subject.snapshot();
|
|
50
|
+
assertEvent(afterOutOfOrderFailure, aFailure, "a-failure", "outOfOrder", "ignored");
|
|
51
|
+
assertCoreCounts(afterOutOfOrderFailure.counts, expected.fixture, expected.afterOutOfOrderFailure);
|
|
52
|
+
assertSucceeded(afterOutOfOrderFailure.aggregates.outOfOrder);
|
|
53
|
+
assertStableAggregate(baseline.aggregates.outOfOrder, afterOutOfOrderFailure.aggregates.outOfOrder);
|
|
54
|
+
const duplicate = await deliver(subject, "outOfOrder", "a-success", "succeeded", "2026-07-29T10:00:00.000Z");
|
|
55
|
+
assertEqual(duplicate.ingest.replayed, true, "duplicate ingest replay");
|
|
56
|
+
assertEqual(duplicate.apply.replayed, true, "duplicate apply replay");
|
|
57
|
+
assertEqual(duplicate.ingest.paymentEventId, aSuccess.ingest.paymentEventId, "duplicate canonical event id");
|
|
58
|
+
const afterDuplicate = await subject.snapshot();
|
|
59
|
+
assertEvent(afterDuplicate, duplicate, "a-success", "outOfOrder", "processed");
|
|
60
|
+
assertCoreCounts(afterDuplicate.counts, expected.fixture, expected.afterDuplicate);
|
|
61
|
+
assertSucceeded(afterDuplicate.aggregates.outOfOrder);
|
|
62
|
+
assertStableAggregate(afterOutOfOrderFailure.aggregates.outOfOrder, afterDuplicate.aggregates.outOfOrder);
|
|
63
|
+
const bFailure = await deliver(subject, "lateSuccess", "b-failure", "failed", "2026-07-29T09:30:00.000Z");
|
|
64
|
+
const afterLateFailure = await subject.snapshot();
|
|
65
|
+
assertEvent(afterLateFailure, bFailure, "b-failure", "lateSuccess", "processed");
|
|
66
|
+
assertCoreCounts(afterLateFailure.counts, expected.fixture, expected.afterLateFailure);
|
|
67
|
+
assertEqual(JSON.stringify(Object.values(afterLateFailure.aggregates.lateSuccess).slice(4)), JSON.stringify(["pending_payment", "failed", "failed", "failed"]), "late failure statuses");
|
|
68
|
+
const bSuccess = await deliver(subject, "lateSuccess", "b-success", "succeeded", "2026-07-29T10:30:00.000Z");
|
|
69
|
+
const final = await subject.snapshot();
|
|
70
|
+
assertEvent(final, bSuccess, "b-success", "lateSuccess", "processed");
|
|
71
|
+
assertCoreCounts(final.counts, expected.fixture, expected.final);
|
|
72
|
+
assertSucceeded(final.aggregates.lateSuccess);
|
|
73
|
+
assertStableAggregate(baseline.aggregates.lateSuccess, final.aggregates.lateSuccess);
|
|
74
|
+
evidence = {
|
|
75
|
+
capability: { name: subject.name, durabilityClaim: true },
|
|
76
|
+
baseline,
|
|
77
|
+
afterOutOfOrderSuccess,
|
|
78
|
+
afterOutOfOrderFailure,
|
|
79
|
+
afterDuplicate,
|
|
80
|
+
afterLateFailure,
|
|
81
|
+
final,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
const cleanup = await subject.reset();
|
|
86
|
+
assertEqual(cleanup.residualRows, 0, "cleanup residual rows");
|
|
87
|
+
}
|
|
88
|
+
if (!evidence)
|
|
89
|
+
throw new Error("Payment-event conformance produced no evidence");
|
|
90
|
+
return { ...evidence, cleanupResidualRows: 0 };
|
|
91
|
+
}
|
|
92
|
+
async function deliver(subject, aggregate, eventKey, resultStatus, occurredAt) {
|
|
93
|
+
const ingest = await subject.ingest({ aggregate, eventKey, resultStatus, occurredAt });
|
|
94
|
+
const apply = await subject.apply({ aggregate, eventKey, paymentEventId: ingest.paymentEventId, resultStatus, occurredAt });
|
|
95
|
+
return { ingest, apply };
|
|
96
|
+
}
|
|
97
|
+
function assertCoreCounts(counts, fixture, writes) {
|
|
98
|
+
for (const [key, value] of Object.entries({ ...fixture, ...writes })) {
|
|
99
|
+
assertEqual(counts[key], value, `${key} count`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function assertEvent(snapshot, receipt, eventKey, aggregate, expectedState) {
|
|
103
|
+
const event = snapshot.events.find((candidate) => candidate.eventKey === eventKey);
|
|
104
|
+
if (!event)
|
|
105
|
+
throw new Error(`Missing durable event ${eventKey}`);
|
|
106
|
+
assertEqual(event.id, receipt.ingest.paymentEventId, `${eventKey} canonical event id`);
|
|
107
|
+
assertEqual(event.paymentIntentId, snapshot.aggregates[aggregate].paymentIntentId, `${eventKey} intent id`);
|
|
108
|
+
assertEqual(event.paymentAttemptId, snapshot.aggregates[aggregate].paymentAttemptId, `${eventKey} attempt id`);
|
|
109
|
+
assertEqual(event.processingStatus, expectedState, `${eventKey} processing status`);
|
|
110
|
+
}
|
|
111
|
+
function assertStableAggregate(before, after) {
|
|
112
|
+
for (const key of ["orderId", "paymentId", "paymentIntentId", "paymentAttemptId"]) {
|
|
113
|
+
assertEqual(after[key], before[key], `stable ${key}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function assertSucceeded(aggregate) {
|
|
117
|
+
assertEqual(aggregate.orderStatus, "paid", "order status");
|
|
118
|
+
assertEqual(aggregate.paymentStatus, "succeeded", "payment status");
|
|
119
|
+
assertEqual(aggregate.intentStatus, "succeeded", "intent status");
|
|
120
|
+
assertEqual(aggregate.attemptStatus, "succeeded", "attempt status");
|
|
121
|
+
}
|
|
122
|
+
function assertEqual(actual, expected, label) {
|
|
123
|
+
if (actual !== expected)
|
|
124
|
+
throw new Error(`${label}: expected ${String(expected)}, received ${String(actual)}`);
|
|
125
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { executePreparedAttemptMechanism } from "./preparedAttempt.js";
|
|
2
|
+
export { paymentSourceFingerprint, paymentPayloadFingerprint, paymentTruthFingerprint, paymentReconciliationFingerprint, assertPaymentTruthEvent } from "./paymentTruth.js";
|
|
3
|
+
export { createVerifiedPaymentIngress } from "./verifiedIngress.js";
|
|
4
|
+
export { readMethodLifecycleEvent } from "./paymentMethodLifecycle.js";
|
|
5
|
+
export { handoffExplicitPaymentRefusal } from "./paymentWebhookPostProcessing.js";
|
|
6
|
+
export { runPaymentProviderReconciliationWorker } from "./paymentProviderReconciliationWorker.js";
|
|
7
|
+
export { verifyPaymentAttemptNow } from "./paymentVerifyNowService.js";
|
|
8
|
+
export { runExecutionConformance, runDurableEventConformance } from "./eventConformanceRunner.js";
|
|
9
|
+
export { runPaymentAdapterConformance } from "./adapterConformance.js";
|
|
10
|
+
export { createPostgresPaymentTruthPort } from "./postgresPaymentTruth.js";
|
|
11
|
+
export { PAYMENT_MIGRATIONS } from "./sql.js";
|
|
12
|
+
export { paymentSchemaDescriptor } from "./schemaCapabilities.js";
|
|
13
|
+
export { createCheckoutActivePaymentActionResolver } from "./checkoutActivePaymentActionResolver.js";
|
|
14
|
+
export { createCheckoutRecoveryPaymentResolver } from "./checkoutRecoveryPaymentResolver.js";
|
|
15
|
+
export { createCommercePaymentVerifyHandler } from "./paymentVerifyNowHandler.js";
|
|
16
|
+
export { createPaymentServices } from "./bindings.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { executePreparedAttemptMechanism } from "./preparedAttempt.js";
|
|
2
|
+
export { paymentSourceFingerprint, paymentPayloadFingerprint, paymentTruthFingerprint, paymentReconciliationFingerprint, assertPaymentTruthEvent } from "./paymentTruth.js";
|
|
3
|
+
export { createVerifiedPaymentIngress } from "./verifiedIngress.js";
|
|
4
|
+
export { readMethodLifecycleEvent } from "./paymentMethodLifecycle.js";
|
|
5
|
+
export { handoffExplicitPaymentRefusal } from "./paymentWebhookPostProcessing.js";
|
|
6
|
+
export { runPaymentProviderReconciliationWorker } from "./paymentProviderReconciliationWorker.js";
|
|
7
|
+
export { verifyPaymentAttemptNow } from "./paymentVerifyNowService.js";
|
|
8
|
+
export { runExecutionConformance, runDurableEventConformance } from "./eventConformanceRunner.js";
|
|
9
|
+
export { runPaymentAdapterConformance } from "./adapterConformance.js";
|
|
10
|
+
export { createPostgresPaymentTruthPort } from "./postgresPaymentTruth.js";
|
|
11
|
+
export { PAYMENT_MIGRATIONS } from "./sql.js";
|
|
12
|
+
export { paymentSchemaDescriptor } from "./schemaCapabilities.js";
|
|
13
|
+
export { createCheckoutActivePaymentActionResolver } from "./checkoutActivePaymentActionResolver.js";
|
|
14
|
+
export { createCheckoutRecoveryPaymentResolver } from "./checkoutRecoveryPaymentResolver.js";
|
|
15
|
+
export { createCommercePaymentVerifyHandler } from "./paymentVerifyNowHandler.js";
|
|
16
|
+
export { createPaymentServices } from "./bindings.js";
|