@alphawebai/hs-prism 0.1.0-rc.40 → 0.1.0-rc.42
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/dist/src/alphaweb/contracts.d.ts +1 -0
- package/dist/src/alphaweb/contracts.js +3 -1
- package/dist/src/alphaweb/error.d.ts +3 -0
- package/dist/src/alphaweb/error.js +3 -0
- package/dist/src/alphaweb/manifest.d.ts +3 -1
- package/dist/src/alphaweb/manifest.js +3 -1
- package/dist/src/alphaweb/paypal-approved-order.d.ts +49 -0
- package/dist/src/alphaweb/paypal-approved-order.js +326 -0
- package/dist/src/alphaweb/paypal-connector.d.ts +8 -1
- package/dist/src/alphaweb/paypal-connector.js +216 -15
- package/dist/src/alphaweb/stripe-connector.d.ts +5 -1
- package/dist/src/alphaweb/stripe-connector.js +123 -2
- package/dist/src/alphaweb/types.d.ts +97 -2
- package/dist/src/payments/generated/libconnector_service_ffi.so +0 -0
- package/package.json +1 -1
|
@@ -3,4 +3,5 @@ export type { AlphaWebPrismCompatibilityManifest } from './manifest';
|
|
|
3
3
|
export { PrismOperationError, normalizePrismError } from './error';
|
|
4
4
|
export { assertSafeProviderMoney, fromProviderMinorAmount, getCurrencyRule, toProviderMinorAmount, } from './money';
|
|
5
5
|
export type { CurrencyRule } from './money';
|
|
6
|
+
export { paypalQuotesMatch } from './paypal-approved-order';
|
|
6
7
|
export type * from './types';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.toProviderMinorAmount = exports.getCurrencyRule = exports.fromProviderMinorAmount = exports.assertSafeProviderMoney = exports.normalizePrismError = exports.PrismOperationError = exports.ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST = void 0;
|
|
3
|
+
exports.paypalQuotesMatch = exports.toProviderMinorAmount = exports.getCurrencyRule = exports.fromProviderMinorAmount = exports.assertSafeProviderMoney = exports.normalizePrismError = exports.PrismOperationError = exports.ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST = void 0;
|
|
4
4
|
var manifest_1 = require("./manifest");
|
|
5
5
|
Object.defineProperty(exports, "ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST", { enumerable: true, get: function () { return manifest_1.ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST; } });
|
|
6
6
|
var error_1 = require("./error");
|
|
@@ -11,4 +11,6 @@ Object.defineProperty(exports, "assertSafeProviderMoney", { enumerable: true, ge
|
|
|
11
11
|
Object.defineProperty(exports, "fromProviderMinorAmount", { enumerable: true, get: function () { return money_1.fromProviderMinorAmount; } });
|
|
12
12
|
Object.defineProperty(exports, "getCurrencyRule", { enumerable: true, get: function () { return money_1.getCurrencyRule; } });
|
|
13
13
|
Object.defineProperty(exports, "toProviderMinorAmount", { enumerable: true, get: function () { return money_1.toProviderMinorAmount; } });
|
|
14
|
+
var paypal_approved_order_1 = require("./paypal-approved-order");
|
|
15
|
+
Object.defineProperty(exports, "paypalQuotesMatch", { enumerable: true, get: function () { return paypal_approved_order_1.paypalQuotesMatch; } });
|
|
14
16
|
//# sourceMappingURL=contracts.js.map
|
|
@@ -2,6 +2,9 @@ export interface PrismOperationErrorShape {
|
|
|
2
2
|
category: 'configuration' | 'authentication' | 'validation' | 'customer_action' | 'declined' | 'rate_limited' | 'network' | 'provider_server' | 'conflict' | 'unknown';
|
|
3
3
|
providerCode?: string;
|
|
4
4
|
providerRequestId?: string;
|
|
5
|
+
providerHttpStatus?: number;
|
|
6
|
+
/** Provider issue identifiers only; never descriptions, field values, or raw responses. */
|
|
7
|
+
providerIssueCodes?: readonly string[];
|
|
5
8
|
retryable: boolean;
|
|
6
9
|
outcome: 'definitely_not_applied' | 'applied' | 'ambiguous';
|
|
7
10
|
safeMessage: string;
|
|
@@ -101,6 +101,9 @@ function normalizePrismError(error) {
|
|
|
101
101
|
category,
|
|
102
102
|
...(code ? { providerCode: code } : {}),
|
|
103
103
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
104
|
+
...(Number.isInteger(status) && status >= 100 && status <= 599
|
|
105
|
+
? { providerHttpStatus: status }
|
|
106
|
+
: {}),
|
|
104
107
|
retryable,
|
|
105
108
|
outcome,
|
|
106
109
|
safeMessage,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare const ALPHAWEB_STRIPE_WEBHOOK_EVENTS: readonly ["charge.dispute.closed", "charge.dispute.created", "charge.dispute.funds_reinstated", "charge.dispute.funds_withdrawn", "charge.dispute.updated", "mandate.updated", "payment_intent.amount_capturable_updated", "payment_intent.canceled", "payment_intent.payment_failed", "payment_intent.processing", "payment_intent.requires_action", "payment_intent.succeeded", "payment_method.attached", "payment_method.automatically_updated", "payment_method.detached", "payment_method.updated", "refund.created", "refund.failed", "refund.updated", "setup_intent.canceled", "setup_intent.requires_action", "setup_intent.setup_failed", "setup_intent.succeeded"];
|
|
2
2
|
export declare const ALPHAWEB_PAYPAL_WEBHOOK_EVENTS: readonly ["CHECKOUT.ORDER.APPROVED", "CHECKOUT.ORDER.COMPLETED", "CHECKOUT.ORDER.PROCESSED", "CHECKOUT.PAYMENT-APPROVAL.REVERSED", "CUSTOMER.DISPUTE.CREATED", "CUSTOMER.DISPUTE.RESOLVED", "CUSTOMER.DISPUTE.UPDATED", "PAYMENT.AUTHORIZATION.CREATED", "PAYMENT.AUTHORIZATION.VOIDED", "PAYMENT.CAPTURE.COMPLETED", "PAYMENT.CAPTURE.DECLINED", "PAYMENT.CAPTURE.PENDING", "PAYMENT.CAPTURE.REFUNDED", "PAYMENT.CAPTURE.REVERSED", "PAYMENT.REFUND.FAILED", "PAYMENT.REFUND.PENDING", "RISK.DISPUTE.CREATED", "VAULT.PAYMENT-TOKEN.CREATED", "VAULT.PAYMENT-TOKEN.DELETED", "VAULT.PAYMENT-TOKEN.DELETION-INITIATED"];
|
|
3
3
|
export declare const ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST: Readonly<{
|
|
4
|
-
readonly release: "0.1.0-rc.
|
|
4
|
+
readonly release: "0.1.0-rc.42";
|
|
5
5
|
readonly upstreamCommit: "b07af7d9dac271de729548e74217ddcf895c602b";
|
|
6
6
|
readonly medusaRange: ">=2.17.2 <2.18.0";
|
|
7
7
|
readonly stripeApiVersion: "2026-06-24.dahlia";
|
|
@@ -24,6 +24,7 @@ export declare const ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST: Readonly<{
|
|
|
24
24
|
readonly connectorCapabilities: {
|
|
25
25
|
readonly stripe: {
|
|
26
26
|
readonly browserSession: "stripe_payment_element";
|
|
27
|
+
readonly manualAuthorizationCheckout: "v1";
|
|
27
28
|
readonly oneTimePayment: true;
|
|
28
29
|
readonly futureUse: true;
|
|
29
30
|
readonly repeatPayment: true;
|
|
@@ -34,6 +35,7 @@ export declare const ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST: Readonly<{
|
|
|
34
35
|
};
|
|
35
36
|
readonly paypal: {
|
|
36
37
|
readonly browserSession: "paypal_checkout";
|
|
38
|
+
readonly approvedOrderCheckout: "v1";
|
|
37
39
|
readonly oneTimePayment: true;
|
|
38
40
|
readonly futureUse: true;
|
|
39
41
|
readonly repeatPayment: true;
|
|
@@ -49,7 +49,7 @@ exports.ALPHAWEB_PAYPAL_WEBHOOK_EVENTS = Object.freeze([
|
|
|
49
49
|
'VAULT.PAYMENT-TOKEN.DELETION-INITIATED',
|
|
50
50
|
]);
|
|
51
51
|
exports.ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST = Object.freeze({
|
|
52
|
-
release: '0.1.0-rc.
|
|
52
|
+
release: '0.1.0-rc.42',
|
|
53
53
|
upstreamCommit: 'b07af7d9dac271de729548e74217ddcf895c602b',
|
|
54
54
|
medusaRange: '>=2.17.2 <2.18.0',
|
|
55
55
|
stripeApiVersion: '2026-06-24.dahlia',
|
|
@@ -72,6 +72,7 @@ exports.ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST = Object.freeze({
|
|
|
72
72
|
connectorCapabilities: {
|
|
73
73
|
stripe: {
|
|
74
74
|
browserSession: 'stripe_payment_element',
|
|
75
|
+
manualAuthorizationCheckout: 'v1',
|
|
75
76
|
oneTimePayment: true,
|
|
76
77
|
futureUse: true,
|
|
77
78
|
repeatPayment: true,
|
|
@@ -82,6 +83,7 @@ exports.ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST = Object.freeze({
|
|
|
82
83
|
},
|
|
83
84
|
paypal: {
|
|
84
85
|
browserSession: 'paypal_checkout',
|
|
86
|
+
approvedOrderCheckout: 'v1',
|
|
85
87
|
oneTimePayment: true,
|
|
86
88
|
futureUse: true,
|
|
87
89
|
repeatPayment: true,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { PrismOperationError } from './error';
|
|
2
|
+
import type { PrismPayPalApprovedOrder, PrismPayPalConnectorOptions, PrismPayPalOrderOwner, PrismPayPalOrderQuote } from './types';
|
|
3
|
+
export declare function approvedOrderError(code: string, outcome?: 'ambiguous' | 'definitely_not_applied'): PrismOperationError;
|
|
4
|
+
export declare function paypalQuotePayload(quote: PrismPayPalOrderQuote): {
|
|
5
|
+
items?: {
|
|
6
|
+
billing_plan?: {
|
|
7
|
+
name: string;
|
|
8
|
+
billing_cycles: {
|
|
9
|
+
tenure_type: string;
|
|
10
|
+
pricing_scheme: {
|
|
11
|
+
price: {
|
|
12
|
+
currency_code: string;
|
|
13
|
+
value: string;
|
|
14
|
+
};
|
|
15
|
+
pricing_model: string;
|
|
16
|
+
};
|
|
17
|
+
frequency: {
|
|
18
|
+
interval_unit: string;
|
|
19
|
+
interval_count: number;
|
|
20
|
+
};
|
|
21
|
+
total_cycles: number;
|
|
22
|
+
sequence: number;
|
|
23
|
+
}[];
|
|
24
|
+
} | undefined;
|
|
25
|
+
tax?: {
|
|
26
|
+
currency_code: string;
|
|
27
|
+
value: string;
|
|
28
|
+
} | undefined;
|
|
29
|
+
category?: "DIGITAL_GOODS" | "PHYSICAL_GOODS" | "DONATION" | undefined;
|
|
30
|
+
sku?: string | undefined;
|
|
31
|
+
name: string;
|
|
32
|
+
quantity: string;
|
|
33
|
+
unit_amount: {
|
|
34
|
+
currency_code: string;
|
|
35
|
+
value: string;
|
|
36
|
+
};
|
|
37
|
+
}[] | undefined;
|
|
38
|
+
amount: {
|
|
39
|
+
breakdown?: Record<string, {
|
|
40
|
+
currency_code: string;
|
|
41
|
+
value: string;
|
|
42
|
+
}> | undefined;
|
|
43
|
+
currency_code: string;
|
|
44
|
+
value: string;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
export declare function paypalQuotesMatch(left: PrismPayPalOrderQuote, right: PrismPayPalOrderQuote): boolean;
|
|
48
|
+
export declare function assertRecurringDisclosurePreserved(previous: PrismPayPalOrderQuote, next: PrismPayPalOrderQuote): void;
|
|
49
|
+
export declare function readApprovedOrder(payload: unknown, owner: PrismPayPalOrderOwner, options: Readonly<PrismPayPalConnectorOptions>): PrismPayPalApprovedOrder;
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.approvedOrderError = approvedOrderError;
|
|
4
|
+
exports.paypalQuotePayload = paypalQuotePayload;
|
|
5
|
+
exports.paypalQuotesMatch = paypalQuotesMatch;
|
|
6
|
+
exports.assertRecurringDisclosurePreserved = assertRecurringDisclosurePreserved;
|
|
7
|
+
exports.readApprovedOrder = readApprovedOrder;
|
|
8
|
+
const node_util_1 = require("node:util");
|
|
9
|
+
const error_1 = require("./error");
|
|
10
|
+
const money_1 = require("./money");
|
|
11
|
+
const breakdownFields = {
|
|
12
|
+
itemTotalMinor: 'item_total',
|
|
13
|
+
taxTotalMinor: 'tax_total',
|
|
14
|
+
shippingMinor: 'shipping',
|
|
15
|
+
handlingMinor: 'handling',
|
|
16
|
+
insuranceMinor: 'insurance',
|
|
17
|
+
shippingDiscountMinor: 'shipping_discount',
|
|
18
|
+
discountMinor: 'discount',
|
|
19
|
+
};
|
|
20
|
+
function record(value) {
|
|
21
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
22
|
+
? value
|
|
23
|
+
: {};
|
|
24
|
+
}
|
|
25
|
+
function text(value) {
|
|
26
|
+
return typeof value === 'string' ? value : '';
|
|
27
|
+
}
|
|
28
|
+
function approvedOrderError(code, outcome = 'ambiguous') {
|
|
29
|
+
return new error_1.PrismOperationError({
|
|
30
|
+
category: 'conflict',
|
|
31
|
+
providerCode: code,
|
|
32
|
+
retryable: false,
|
|
33
|
+
outcome,
|
|
34
|
+
safeMessage: 'The PayPal order must be verified before payment can continue.',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function moneyFrom(value, currency) {
|
|
38
|
+
const amount = record(value);
|
|
39
|
+
const code = text(amount.currency_code).toLowerCase();
|
|
40
|
+
if (!code || (currency && currency !== code) || typeof amount.value !== 'string') {
|
|
41
|
+
throw approvedOrderError('PRISM_PAYPAL_ORDER_MONEY');
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
currency: code,
|
|
45
|
+
amountMinor: (0, money_1.toProviderMinorAmount)({ medusaAmount: amount.value, currency: code }).toString(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function providerMoney(amountMinor, currency) {
|
|
49
|
+
return {
|
|
50
|
+
currency_code: currency.toUpperCase(),
|
|
51
|
+
value: (0, money_1.fromProviderMinorAmount)({ amountMinor, currency }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function minor(value, currency) {
|
|
55
|
+
(0, money_1.fromProviderMinorAmount)({ amountMinor: value, currency });
|
|
56
|
+
return BigInt(value);
|
|
57
|
+
}
|
|
58
|
+
function recurringPlan(agreement, currency) {
|
|
59
|
+
const limits = { day: 365, week: 52, month: 12, year: 1 };
|
|
60
|
+
const money = (0, money_1.assertSafeProviderMoney)(agreement.recurringMoney);
|
|
61
|
+
if (money.currency !== currency ||
|
|
62
|
+
!agreement.planName.trim() ||
|
|
63
|
+
agreement.planName.length > 127 ||
|
|
64
|
+
!Number.isSafeInteger(agreement.intervalCount) ||
|
|
65
|
+
agreement.intervalCount < 1 ||
|
|
66
|
+
!(agreement.intervalUnit in limits) ||
|
|
67
|
+
agreement.intervalCount > limits[agreement.intervalUnit] ||
|
|
68
|
+
(agreement.totalPayments !== null &&
|
|
69
|
+
(!Number.isSafeInteger(agreement.totalPayments) ||
|
|
70
|
+
agreement.totalPayments < 2 ||
|
|
71
|
+
agreement.totalPayments > 999))) {
|
|
72
|
+
throw approvedOrderError('PRISM_PAYPAL_RECURRING_AGREEMENT', 'definitely_not_applied');
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
name: agreement.planName,
|
|
76
|
+
billing_cycles: [
|
|
77
|
+
{
|
|
78
|
+
tenure_type: 'REGULAR',
|
|
79
|
+
pricing_scheme: {
|
|
80
|
+
price: providerMoney(agreement.recurringMoney.amountMinor, currency),
|
|
81
|
+
pricing_model: 'FIXED',
|
|
82
|
+
},
|
|
83
|
+
frequency: {
|
|
84
|
+
interval_unit: agreement.intervalUnit.toUpperCase(),
|
|
85
|
+
interval_count: agreement.intervalCount,
|
|
86
|
+
},
|
|
87
|
+
total_cycles: agreement.totalPayments ?? 0,
|
|
88
|
+
sequence: 1,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function recurringFrom(value, currency) {
|
|
94
|
+
const plan = record(value);
|
|
95
|
+
const cycles = Array.isArray(plan.billing_cycles) ? plan.billing_cycles : [];
|
|
96
|
+
const cycle = record(cycles[0]);
|
|
97
|
+
const frequency = record(cycle.frequency);
|
|
98
|
+
const pricing = record(cycle.pricing_scheme);
|
|
99
|
+
const unit = text(frequency.interval_unit).toLowerCase();
|
|
100
|
+
if (cycles.length !== 1 ||
|
|
101
|
+
cycle.tenure_type !== 'REGULAR' ||
|
|
102
|
+
cycle.sequence !== 1 ||
|
|
103
|
+
pricing.pricing_model !== 'FIXED' ||
|
|
104
|
+
(unit !== 'day' && unit !== 'week' && unit !== 'month' && unit !== 'year')) {
|
|
105
|
+
throw approvedOrderError('PRISM_PAYPAL_RECURRING_AGREEMENT');
|
|
106
|
+
}
|
|
107
|
+
const agreement = {
|
|
108
|
+
planName: text(plan.name),
|
|
109
|
+
intervalUnit: unit,
|
|
110
|
+
intervalCount: Number(frequency.interval_count),
|
|
111
|
+
totalPayments: cycle.total_cycles === 0 ? null : Number(cycle.total_cycles),
|
|
112
|
+
recurringMoney: moneyFrom(pricing.price, currency),
|
|
113
|
+
};
|
|
114
|
+
recurringPlan(agreement, currency);
|
|
115
|
+
return agreement;
|
|
116
|
+
}
|
|
117
|
+
function buildQuotePayload(quote) {
|
|
118
|
+
const currency = (0, money_1.assertSafeProviderMoney)(quote.money).currency;
|
|
119
|
+
const breakdown = {};
|
|
120
|
+
let total = 0n;
|
|
121
|
+
for (const [key, field] of Object.entries(breakdownFields)) {
|
|
122
|
+
const value = quote.breakdown?.[key];
|
|
123
|
+
if (value === undefined)
|
|
124
|
+
continue;
|
|
125
|
+
const amount = minor(value, currency);
|
|
126
|
+
total += field === 'discount' || field === 'shipping_discount' ? -amount : amount;
|
|
127
|
+
breakdown[field] = providerMoney(value, currency);
|
|
128
|
+
}
|
|
129
|
+
if (quote.breakdown && total !== BigInt(quote.money.amountMinor)) {
|
|
130
|
+
throw approvedOrderError('PRISM_PAYPAL_QUOTE_TOTAL', 'definitely_not_applied');
|
|
131
|
+
}
|
|
132
|
+
let itemTotal = 0n;
|
|
133
|
+
let taxTotal = 0n;
|
|
134
|
+
const items = quote.items?.map((item) => {
|
|
135
|
+
if (!item.name.trim() ||
|
|
136
|
+
item.name.length > 127 ||
|
|
137
|
+
(item.sku !== undefined && (!item.sku || item.sku.length > 127)) ||
|
|
138
|
+
(item.category !== undefined &&
|
|
139
|
+
!['DIGITAL_GOODS', 'PHYSICAL_GOODS', 'DONATION'].includes(item.category)) ||
|
|
140
|
+
!Number.isSafeInteger(item.quantity) ||
|
|
141
|
+
item.quantity < 1) {
|
|
142
|
+
throw approvedOrderError('PRISM_PAYPAL_QUOTE_ITEM', 'definitely_not_applied');
|
|
143
|
+
}
|
|
144
|
+
itemTotal += minor(item.unitAmountMinor, currency) * BigInt(item.quantity);
|
|
145
|
+
taxTotal += minor(item.taxMinor ?? '0', currency) * BigInt(item.quantity);
|
|
146
|
+
return {
|
|
147
|
+
name: item.name,
|
|
148
|
+
quantity: String(item.quantity),
|
|
149
|
+
unit_amount: providerMoney(item.unitAmountMinor, currency),
|
|
150
|
+
...(item.sku ? { sku: item.sku } : {}),
|
|
151
|
+
...(item.category ? { category: item.category } : {}),
|
|
152
|
+
...(item.taxMinor === undefined ? {} : { tax: providerMoney(item.taxMinor, currency) }),
|
|
153
|
+
...(item.recurringAgreement
|
|
154
|
+
? { billing_plan: recurringPlan(item.recurringAgreement, currency) }
|
|
155
|
+
: {}),
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
if (items &&
|
|
159
|
+
(!items.length ||
|
|
160
|
+
quote.breakdown?.itemTotalMinor === undefined ||
|
|
161
|
+
itemTotal !== BigInt(quote.breakdown.itemTotalMinor) ||
|
|
162
|
+
(quote.items?.some((item) => item.taxMinor !== undefined) &&
|
|
163
|
+
(quote.breakdown.taxTotalMinor === undefined ||
|
|
164
|
+
taxTotal !== BigInt(quote.breakdown.taxTotalMinor))))) {
|
|
165
|
+
throw approvedOrderError('PRISM_PAYPAL_QUOTE_ITEMS_TOTAL', 'definitely_not_applied');
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
amount: {
|
|
169
|
+
...providerMoney(quote.money.amountMinor, currency),
|
|
170
|
+
...(quote.breakdown ? { breakdown } : {}),
|
|
171
|
+
},
|
|
172
|
+
...(items ? { items } : {}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function paypalQuotePayload(quote) {
|
|
176
|
+
try {
|
|
177
|
+
return buildQuotePayload(quote);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error instanceof error_1.PrismOperationError)
|
|
181
|
+
throw error;
|
|
182
|
+
throw approvedOrderError('PRISM_PAYPAL_QUOTE_INVALID', 'definitely_not_applied');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function paypalQuotesMatch(left, right) {
|
|
186
|
+
return (0, node_util_1.isDeepStrictEqual)(paypalQuotePayload(left), paypalQuotePayload(right));
|
|
187
|
+
}
|
|
188
|
+
function assertRecurringDisclosurePreserved(previous, next) {
|
|
189
|
+
const agreements = (quote) => quote.items
|
|
190
|
+
?.filter((item) => item.recurringAgreement)
|
|
191
|
+
.map((item) => ({ quantity: item.quantity, agreement: item.recurringAgreement })) ?? [];
|
|
192
|
+
if (!(0, node_util_1.isDeepStrictEqual)(agreements(previous), agreements(next))) {
|
|
193
|
+
throw approvedOrderError('PRISM_PAYPAL_RECURRING_AGREEMENT_CHANGED', 'definitely_not_applied');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function readApprovedOrder(payload, owner, options) {
|
|
197
|
+
const order = record(payload);
|
|
198
|
+
const units = Array.isArray(order.purchase_units) ? order.purchase_units : [];
|
|
199
|
+
const unit = record(units[0]);
|
|
200
|
+
const payee = record(unit.payee);
|
|
201
|
+
if (owner.credentialBindingId !== options.credentialBindingId ||
|
|
202
|
+
owner.providerAccountId !== options.providerAccountId ||
|
|
203
|
+
owner.livemode !== (options.environment === 'live') ||
|
|
204
|
+
order.id !== owner.connectorPaymentId ||
|
|
205
|
+
order.intent !== 'CAPTURE' ||
|
|
206
|
+
units.length !== 1 ||
|
|
207
|
+
unit.reference_id !== owner.rootOperationId ||
|
|
208
|
+
unit.invoice_id !== owner.rootOperationId ||
|
|
209
|
+
(owner.expectedCustomId !== undefined && unit.custom_id !== owner.expectedCustomId) ||
|
|
210
|
+
(options.payerId !== undefined && payee.merchant_id !== options.payerId)) {
|
|
211
|
+
throw approvedOrderError('PRISM_PAYPAL_ORDER_OWNER');
|
|
212
|
+
}
|
|
213
|
+
const money = moneyFrom(unit.amount);
|
|
214
|
+
const suppliedBreakdown = record(unit.amount).breakdown;
|
|
215
|
+
const rawBreakdown = record(suppliedBreakdown);
|
|
216
|
+
if ((suppliedBreakdown !== undefined &&
|
|
217
|
+
(suppliedBreakdown === null ||
|
|
218
|
+
typeof suppliedBreakdown !== 'object' ||
|
|
219
|
+
Array.isArray(suppliedBreakdown))) ||
|
|
220
|
+
Object.keys(rawBreakdown).some((field) => !Object.values(breakdownFields).some((known) => known === field)) ||
|
|
221
|
+
(unit.items !== undefined && !Array.isArray(unit.items))) {
|
|
222
|
+
throw approvedOrderError('PRISM_PAYPAL_ORDER_RESPONSE');
|
|
223
|
+
}
|
|
224
|
+
const breakdown = {};
|
|
225
|
+
for (const [key, field] of Object.entries(breakdownFields)) {
|
|
226
|
+
if (rawBreakdown[field] !== undefined)
|
|
227
|
+
breakdown[key] = moneyFrom(rawBreakdown[field], money.currency).amountMinor;
|
|
228
|
+
}
|
|
229
|
+
const items = Array.isArray(unit.items)
|
|
230
|
+
? unit.items.map((value) => {
|
|
231
|
+
const item = record(value);
|
|
232
|
+
return {
|
|
233
|
+
name: text(item.name),
|
|
234
|
+
quantity: Number(item.quantity),
|
|
235
|
+
unitAmountMinor: moneyFrom(item.unit_amount, money.currency).amountMinor,
|
|
236
|
+
...(item.sku === undefined ? {} : { sku: text(item.sku) }),
|
|
237
|
+
...(item.category === undefined
|
|
238
|
+
? {}
|
|
239
|
+
: { category: text(item.category) }),
|
|
240
|
+
...(item.tax === undefined
|
|
241
|
+
? {}
|
|
242
|
+
: { taxMinor: moneyFrom(item.tax, money.currency).amountMinor }),
|
|
243
|
+
...(item.billing_plan === undefined
|
|
244
|
+
? {}
|
|
245
|
+
: { recurringAgreement: recurringFrom(item.billing_plan, money.currency) }),
|
|
246
|
+
};
|
|
247
|
+
})
|
|
248
|
+
: undefined;
|
|
249
|
+
const quote = {
|
|
250
|
+
money,
|
|
251
|
+
...(Object.keys(rawBreakdown).length ? { breakdown } : {}),
|
|
252
|
+
...(items ? { items } : {}),
|
|
253
|
+
};
|
|
254
|
+
paypalQuotePayload(quote);
|
|
255
|
+
const payments = record(unit.payments);
|
|
256
|
+
if ((unit.payments !== undefined &&
|
|
257
|
+
(unit.payments === null ||
|
|
258
|
+
typeof unit.payments !== 'object' ||
|
|
259
|
+
Array.isArray(unit.payments))) ||
|
|
260
|
+
(payments.captures !== undefined && !Array.isArray(payments.captures)) ||
|
|
261
|
+
(payments.authorizations !== undefined && !Array.isArray(payments.authorizations))) {
|
|
262
|
+
throw approvedOrderError('PRISM_PAYPAL_ORDER_RESPONSE');
|
|
263
|
+
}
|
|
264
|
+
const captures = Array.isArray(payments.captures) ? payments.captures : [];
|
|
265
|
+
const authorizations = Array.isArray(payments.authorizations) ? payments.authorizations : [];
|
|
266
|
+
if (captures.length > 1)
|
|
267
|
+
throw approvedOrderError('PRISM_PAYPAL_MULTIPLE_CAPTURES');
|
|
268
|
+
const statuses = [
|
|
269
|
+
'CREATED',
|
|
270
|
+
'SAVED',
|
|
271
|
+
'PAYER_ACTION_REQUIRED',
|
|
272
|
+
'APPROVED',
|
|
273
|
+
'VOIDED',
|
|
274
|
+
'COMPLETED',
|
|
275
|
+
];
|
|
276
|
+
const orderStatus = statuses.find((status) => status === order.status) ?? 'UNKNOWN';
|
|
277
|
+
let capture;
|
|
278
|
+
if (captures.length) {
|
|
279
|
+
const data = record(captures[0]);
|
|
280
|
+
const captureMoney = moneyFrom(data.amount, money.currency);
|
|
281
|
+
if (!text(data.id) ||
|
|
282
|
+
(data.invoice_id !== undefined && data.invoice_id !== owner.rootOperationId) ||
|
|
283
|
+
captureMoney.amountMinor !== money.amountMinor) {
|
|
284
|
+
throw approvedOrderError('PRISM_PAYPAL_CAPTURE_MISMATCH');
|
|
285
|
+
}
|
|
286
|
+
const captureStatuses = {
|
|
287
|
+
COMPLETED: 'captured',
|
|
288
|
+
PENDING: 'pending',
|
|
289
|
+
DECLINED: 'failed',
|
|
290
|
+
FAILED: 'failed',
|
|
291
|
+
DENIED: 'failed',
|
|
292
|
+
REFUNDED: 'refunded',
|
|
293
|
+
PARTIALLY_REFUNDED: 'partially_refunded',
|
|
294
|
+
};
|
|
295
|
+
capture = {
|
|
296
|
+
connectorCaptureId: text(data.id),
|
|
297
|
+
money: captureMoney,
|
|
298
|
+
status: captureStatuses[data.status] ?? 'unknown',
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
connectorPaymentId: owner.connectorPaymentId,
|
|
303
|
+
rootOperationId: owner.rootOperationId,
|
|
304
|
+
quote,
|
|
305
|
+
orderStatus,
|
|
306
|
+
hasAuthorization: authorizations.length > 0,
|
|
307
|
+
hasCapture: captures.length > 0,
|
|
308
|
+
...(capture
|
|
309
|
+
? {
|
|
310
|
+
capture,
|
|
311
|
+
connectorMetadata: JSON.stringify({
|
|
312
|
+
authorize_id: null,
|
|
313
|
+
capture_id: capture.connectorCaptureId,
|
|
314
|
+
incremental_authorization_id: null,
|
|
315
|
+
psync_flow: 'CAPTURE',
|
|
316
|
+
next_action: null,
|
|
317
|
+
order_id: null,
|
|
318
|
+
}),
|
|
319
|
+
}
|
|
320
|
+
: {}),
|
|
321
|
+
...(typeof order.update_time === 'string' && Number.isFinite(Date.parse(order.update_time))
|
|
322
|
+
? { providerUpdatedAt: order.update_time }
|
|
323
|
+
: {}),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
//# sourceMappingURL=paypal-approved-order.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { types } from '../payments/generated/proto';
|
|
2
|
-
import type { PrismAuthorizeApprovedPaymentInput, PrismCancelInput, PrismCaptureInput, PrismCreateWebhookEndpointInput, PrismDeleteWebhookEndpointInput, PrismDisputeResult, PrismEstablishReusableReferenceInput, PrismFindPaymentInput, PrismFindRefundInput, PrismMerchantOperation, PrismPayPalConnectorOptions, PrismPayPalConnectorV1, PrismPayPalOrderLifecycle, PrismPayPalRuntimeVerification, PrismPayPalRuntimeVerificationInput, PrismPaymentResult, PrismRefundInput, PrismRefundResult, PrismRetrieveDisputeInput, PrismRetrieveOrderLifecycleInput, PrismRetrievePaymentInput, PrismRetrieveRefundInput, PrismRetrieveWebhookEndpointInput, PrismRetryPaymentInput, PrismReusableReferenceDerivation, PrismUpdateWebhookEndpointInput, PrismWebhookEndpoint, PrismWebhookEndpointCreated, PrismWebhookParseInput, PrismWebhookParseResult } from './types';
|
|
2
|
+
import type { PrismAuthorizeApprovedPaymentInput, PrismCancelInput, PrismCaptureInput, PrismCreateWebhookEndpointInput, PrismDeleteWebhookEndpointInput, PrismDisputeResult, PrismEstablishReusableReferenceInput, PrismFindPaymentInput, PrismFindRefundInput, PrismMerchantOperation, PrismPayPalConnectorOptions, PrismPayPalConnectorV1, PrismPayPalApprovedOrder, PrismPayPalCaptureApprovedOrderInput, PrismPayPalOrderOwner, PrismPayPalUpdateApprovedOrderInput, PrismPayPalOrderLifecycle, PrismPayPalRuntimeVerification, PrismPayPalRuntimeVerificationInput, PrismPaymentResult, PrismRefundInput, PrismRefundResult, PrismRetrieveDisputeInput, PrismRetrieveOrderLifecycleInput, PrismRetrievePaymentInput, PrismRetrieveRefundInput, PrismRetrieveWebhookEndpointInput, PrismRetryPaymentInput, PrismReusableReferenceDerivation, PrismUpdateWebhookEndpointInput, PrismWebhookEndpoint, PrismWebhookEndpointCreated, PrismWebhookParseInput, PrismWebhookParseResult } from './types';
|
|
3
3
|
interface MerchantAuthenticationPort {
|
|
4
4
|
createServerAuthenticationToken(input: types.IMerchantAuthenticationServiceCreateServerAuthenticationTokenRequest): Promise<types.MerchantAuthenticationServiceCreateServerAuthenticationTokenResponse>;
|
|
5
5
|
}
|
|
@@ -32,9 +32,16 @@ export declare class PrismPayPalConnector implements PrismPayPalConnectorV1 {
|
|
|
32
32
|
private validateOptions;
|
|
33
33
|
private assertOperation;
|
|
34
34
|
private connectorState;
|
|
35
|
+
private paypalResponse;
|
|
35
36
|
private paypalRequest;
|
|
36
37
|
private retrievePayPalVaultState;
|
|
37
38
|
createPayment(input: PrismMerchantOperation): Promise<PrismPaymentResult>;
|
|
39
|
+
createAdjustableOrder(input: PrismMerchantOperation): Promise<PrismPaymentResult>;
|
|
40
|
+
private createPayPalOrder;
|
|
41
|
+
private assertOrderOwner;
|
|
42
|
+
retrieveApprovedOrder(input: PrismPayPalOrderOwner): Promise<PrismPayPalApprovedOrder>;
|
|
43
|
+
updateApprovedOrder(input: PrismPayPalUpdateApprovedOrderInput): Promise<PrismPayPalApprovedOrder>;
|
|
44
|
+
captureApprovedOrder(input: PrismPayPalCaptureApprovedOrderInput): Promise<PrismPayPalApprovedOrder>;
|
|
38
45
|
authorizeApprovedPayment(input: PrismAuthorizeApprovedPaymentInput): Promise<PrismPaymentResult>;
|
|
39
46
|
repeatPayment(input: PrismMerchantOperation): Promise<PrismPaymentResult>;
|
|
40
47
|
retryPayment(input: PrismRetryPaymentInput): Promise<PrismPaymentResult>;
|
|
@@ -8,6 +8,14 @@ const proto_1 = require("../payments/generated/proto");
|
|
|
8
8
|
const error_1 = require("./error");
|
|
9
9
|
const manifest_1 = require("./manifest");
|
|
10
10
|
const money_1 = require("./money");
|
|
11
|
+
const paypal_approved_order_1 = require("./paypal-approved-order");
|
|
12
|
+
function safeProviderIdentifier(value) {
|
|
13
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 127)
|
|
14
|
+
return undefined;
|
|
15
|
+
return [...value].every((character) => (character >= 'a' && character <= 'z') ||
|
|
16
|
+
(character >= 'A' && character <= 'Z') ||
|
|
17
|
+
(character >= '0' && character <= '9') || character === '_' || character === '-') ? value : undefined;
|
|
18
|
+
}
|
|
11
19
|
const PAYPAL_API_BASE = {
|
|
12
20
|
test: 'https://api-m.sandbox.paypal.com',
|
|
13
21
|
live: 'https://api-m.paypal.com',
|
|
@@ -253,11 +261,13 @@ function connectorMetadataFrom(response) {
|
|
|
253
261
|
}
|
|
254
262
|
function failureFrom(response) {
|
|
255
263
|
const error = recordOf(recordOf(response).error);
|
|
256
|
-
const code =
|
|
264
|
+
const code = safeProviderIdentifier(recordOf(error.connectorDetails).code) ??
|
|
265
|
+
safeProviderIdentifier(error.code);
|
|
257
266
|
if (!code && Object.keys(error).length === 0)
|
|
258
267
|
return undefined;
|
|
259
268
|
return {
|
|
260
|
-
category: '
|
|
269
|
+
category: code === 'INSTRUMENT_DECLINED' ? 'declined' :
|
|
270
|
+
code === 'PAYER_ACTION_REQUIRED' ? 'customer_action' : 'unknown',
|
|
261
271
|
...(code ? { providerCode: code } : {}),
|
|
262
272
|
retryable: false,
|
|
263
273
|
};
|
|
@@ -363,6 +373,10 @@ function paymentResult(response, money, options, fallbackConnectorPaymentId, app
|
|
|
363
373
|
const connectorPaymentId = connectorPaymentIdFrom(response) ?? fallbackConnectorPaymentId ?? null;
|
|
364
374
|
const connectorMetadata = connectorMetadataFrom(response);
|
|
365
375
|
const failure = failureFrom(response);
|
|
376
|
+
const responseData = recordOf(response);
|
|
377
|
+
const providerRequestId = safeProviderIdentifier(Object.entries(recordOf(responseData.responseHeaders))
|
|
378
|
+
.find(([key]) => key.toLowerCase() === 'paypal-debug-id')?.[1]);
|
|
379
|
+
const providerHttpStatus = Number(responseData.statusCode);
|
|
366
380
|
const providerReference = connectorPaymentId
|
|
367
381
|
? reusableReferenceFrom(response, options, connectorPaymentId)
|
|
368
382
|
: undefined;
|
|
@@ -376,6 +390,9 @@ function paymentResult(response, money, options, fallbackConnectorPaymentId, app
|
|
|
376
390
|
...(connectorMetadata ? { connectorMetadata } : {}),
|
|
377
391
|
...(reusableReference ? { reusableReference } : {}),
|
|
378
392
|
...(failure ? { failure } : {}),
|
|
393
|
+
...(providerRequestId ? { providerRequestId } : {}),
|
|
394
|
+
...(Number.isInteger(providerHttpStatus) && providerHttpStatus >= 100 && providerHttpStatus <= 599
|
|
395
|
+
? { providerHttpStatus } : {}),
|
|
379
396
|
};
|
|
380
397
|
}
|
|
381
398
|
function refundStatus(status) {
|
|
@@ -757,7 +774,7 @@ class PrismPayPalConnector {
|
|
|
757
774
|
},
|
|
758
775
|
};
|
|
759
776
|
}
|
|
760
|
-
async
|
|
777
|
+
async paypalResponse(method, path, body, idempotencyKey) {
|
|
761
778
|
const state = await this.connectorState();
|
|
762
779
|
const accessToken = readSecret(state.accessToken?.token);
|
|
763
780
|
if (!accessToken) {
|
|
@@ -776,7 +793,7 @@ class PrismPayPalConnector {
|
|
|
776
793
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
777
794
|
signal: controller.signal,
|
|
778
795
|
});
|
|
779
|
-
const requestId = response.headers.get('paypal-debug-id')
|
|
796
|
+
const requestId = safeProviderIdentifier(response.headers.get('paypal-debug-id'));
|
|
780
797
|
const text = await response.text();
|
|
781
798
|
let payload = null;
|
|
782
799
|
if (text.length > 0) {
|
|
@@ -789,22 +806,37 @@ class PrismPayPalConnector {
|
|
|
789
806
|
}
|
|
790
807
|
if (!response.ok) {
|
|
791
808
|
const data = recordOf(payload);
|
|
809
|
+
const issues = (Array.isArray(data.details) ? data.details : [])
|
|
810
|
+
.map((detail) => safeProviderIdentifier(recordOf(detail).issue))
|
|
811
|
+
.filter((issue) => issue !== undefined)
|
|
812
|
+
.slice(0, 10);
|
|
813
|
+
const providerCode = safeProviderIdentifier(data.name);
|
|
814
|
+
const providerRequestId = requestId ?? safeProviderIdentifier(data.debug_id);
|
|
792
815
|
throw new error_1.PrismOperationError({
|
|
793
816
|
category: response.status === 401 || response.status === 403
|
|
794
817
|
? 'authentication'
|
|
795
|
-
: response.status ===
|
|
796
|
-
? '
|
|
797
|
-
: response.status
|
|
798
|
-
? '
|
|
799
|
-
:
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
818
|
+
: response.status === 409
|
|
819
|
+
? 'conflict'
|
|
820
|
+
: response.status === 429
|
|
821
|
+
? 'rate_limited'
|
|
822
|
+
: response.status >= 500
|
|
823
|
+
? 'provider_server'
|
|
824
|
+
: 'validation',
|
|
825
|
+
...(providerCode ? { providerCode } : {}),
|
|
826
|
+
...(providerRequestId ? { providerRequestId } : {}),
|
|
827
|
+
providerHttpStatus: response.status,
|
|
828
|
+
...(issues.length ? { providerIssueCodes: [...new Set(issues)] } : {}),
|
|
829
|
+
retryable: response.status === 409 || response.status === 429 || response.status >= 500,
|
|
830
|
+
outcome: response.status === 409 || response.status >= 500
|
|
831
|
+
? 'ambiguous' : 'definitely_not_applied',
|
|
804
832
|
safeMessage: 'PayPal rejected the payment operation.',
|
|
805
833
|
});
|
|
806
834
|
}
|
|
807
|
-
return
|
|
835
|
+
return {
|
|
836
|
+
payload,
|
|
837
|
+
providerHttpStatus: response.status,
|
|
838
|
+
...(requestId ? { providerRequestId: requestId } : {}),
|
|
839
|
+
};
|
|
808
840
|
}
|
|
809
841
|
catch (error) {
|
|
810
842
|
throw (0, error_1.normalizePrismError)(error);
|
|
@@ -813,11 +845,23 @@ class PrismPayPalConnector {
|
|
|
813
845
|
clearTimeout(timeout);
|
|
814
846
|
}
|
|
815
847
|
}
|
|
848
|
+
async paypalRequest(method, path, body, idempotencyKey) {
|
|
849
|
+
return (await this.paypalResponse(method, path, body, idempotencyKey)).payload;
|
|
850
|
+
}
|
|
816
851
|
async retrievePayPalVaultState(connectorPaymentId) {
|
|
817
852
|
const payload = await this.paypalRequest('GET', `/v2/checkout/orders/${encodeURIComponent(connectorPaymentId)}?fields=payment_source`);
|
|
818
853
|
return paypalVaultStateFromOrder(payload, this.options, connectorPaymentId);
|
|
819
854
|
}
|
|
820
855
|
async createPayment(input) {
|
|
856
|
+
return this.createPayPalOrder(input, true);
|
|
857
|
+
}
|
|
858
|
+
async createAdjustableOrder(input) {
|
|
859
|
+
if (input.metadata.alphaweb_capture_method === 'manual') {
|
|
860
|
+
throw validationError('An adjustable PayPal order must use CAPTURE intent.', 'PRISM_PAYPAL_ORDER_INTENT');
|
|
861
|
+
}
|
|
862
|
+
return this.createPayPalOrder(input, false);
|
|
863
|
+
}
|
|
864
|
+
async createPayPalOrder(input, commit) {
|
|
821
865
|
this.assertOperation(input);
|
|
822
866
|
if (input.methodSelection.kind !== 'interactive_dynamic' || input.reusableReference) {
|
|
823
867
|
throw validationError('Interactive payment input is invalid.', 'PRISM_INTERACTIVE_INPUT');
|
|
@@ -833,7 +877,10 @@ class PrismPayPalConnector {
|
|
|
833
877
|
const order = await this.dependencies.payments.createOrder({
|
|
834
878
|
merchantOrderId: input.operationId,
|
|
835
879
|
amount: moneyMessage(input.money),
|
|
836
|
-
metadata: secret(JSON.stringify(
|
|
880
|
+
metadata: secret(JSON.stringify({
|
|
881
|
+
...operationMetadata(input),
|
|
882
|
+
...(!commit ? { alphaweb_paypal_commit: false } : {}),
|
|
883
|
+
})),
|
|
837
884
|
state,
|
|
838
885
|
testMode: this.options.environment === 'test',
|
|
839
886
|
paymentMethodType: proto_1.types.PaymentMethodType.PAY_PAL,
|
|
@@ -849,6 +896,18 @@ class PrismPayPalConnector {
|
|
|
849
896
|
});
|
|
850
897
|
const orderId = stringOf(order.connectorOrderId);
|
|
851
898
|
if (!orderId) {
|
|
899
|
+
if (!commit) {
|
|
900
|
+
const result = paymentResult(order, input.money, this.options);
|
|
901
|
+
throw new error_1.PrismOperationError({
|
|
902
|
+
category: 'unknown',
|
|
903
|
+
providerCode: result.failure?.providerCode ?? 'PRISM_PAYPAL_ORDER_CREATE_UNCONFIRMED',
|
|
904
|
+
...(result.providerRequestId ? { providerRequestId: result.providerRequestId } : {}),
|
|
905
|
+
...(result.providerHttpStatus ? { providerHttpStatus: result.providerHttpStatus } : {}),
|
|
906
|
+
outcome: 'ambiguous',
|
|
907
|
+
retryable: false,
|
|
908
|
+
safeMessage: 'The PayPal order creation result must be verified before continuing.',
|
|
909
|
+
});
|
|
910
|
+
}
|
|
852
911
|
throw validationError('Prism did not return a PayPal order ID.', 'PRISM_PAYPAL_ORDER');
|
|
853
912
|
}
|
|
854
913
|
return {
|
|
@@ -863,6 +922,7 @@ class PrismPayPalConnector {
|
|
|
863
922
|
clientId: this.options.clientId,
|
|
864
923
|
environment: this.options.environment === 'test' ? 'sandbox' : 'production',
|
|
865
924
|
savePayment: setupFutureUsage,
|
|
925
|
+
...(!commit ? { commit: false } : {}),
|
|
866
926
|
},
|
|
867
927
|
};
|
|
868
928
|
}
|
|
@@ -870,6 +930,147 @@ class PrismPayPalConnector {
|
|
|
870
930
|
throw (0, error_1.normalizePrismError)(error);
|
|
871
931
|
}
|
|
872
932
|
}
|
|
933
|
+
assertOrderOwner(input) {
|
|
934
|
+
assertIdentifier(input.rootOperationId, 'rootOperationId');
|
|
935
|
+
assertIdentifier(input.connectorPaymentId, 'connectorPaymentId');
|
|
936
|
+
if (input.credentialBindingId !== this.options.credentialBindingId ||
|
|
937
|
+
input.providerAccountId !== this.options.providerAccountId ||
|
|
938
|
+
input.livemode !== (this.options.environment === 'live')) {
|
|
939
|
+
throw (0, paypal_approved_order_1.approvedOrderError)('PRISM_PAYPAL_ORDER_OWNER', 'definitely_not_applied');
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
async retrieveApprovedOrder(input) {
|
|
943
|
+
this.assertOrderOwner(input);
|
|
944
|
+
const response = await this.paypalResponse('GET', `/v2/checkout/orders/${encodeURIComponent(input.connectorPaymentId)}`);
|
|
945
|
+
try {
|
|
946
|
+
return {
|
|
947
|
+
...(0, paypal_approved_order_1.readApprovedOrder)(response.payload, input, this.options),
|
|
948
|
+
...(response.providerRequestId ? { providerRequestId: response.providerRequestId } : {}),
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
catch (error) {
|
|
952
|
+
const normalized = error instanceof error_1.PrismOperationError
|
|
953
|
+
? error : (0, paypal_approved_order_1.approvedOrderError)('PRISM_PAYPAL_ORDER_RESPONSE');
|
|
954
|
+
throw new error_1.PrismOperationError({
|
|
955
|
+
...normalized.shape,
|
|
956
|
+
outcome: 'ambiguous',
|
|
957
|
+
providerHttpStatus: response.providerHttpStatus,
|
|
958
|
+
...(response.providerRequestId ? { providerRequestId: response.providerRequestId } : {}),
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
async updateApprovedOrder(input) {
|
|
963
|
+
this.assertOrderOwner(input);
|
|
964
|
+
const payload = (0, paypal_approved_order_1.paypalQuotePayload)(input.quote);
|
|
965
|
+
(0, paypal_approved_order_1.assertRecurringDisclosurePreserved)(input.expectedQuote, input.quote);
|
|
966
|
+
const current = await this.retrieveApprovedOrder(input);
|
|
967
|
+
if (current.hasCapture || current.hasAuthorization ||
|
|
968
|
+
(current.orderStatus !== 'CREATED' && current.orderStatus !== 'APPROVED')) {
|
|
969
|
+
throw (0, paypal_approved_order_1.approvedOrderError)('PRISM_PAYPAL_ORDER_NOT_EDITABLE', 'definitely_not_applied');
|
|
970
|
+
}
|
|
971
|
+
if ((0, paypal_approved_order_1.paypalQuotesMatch)(current.quote, input.quote))
|
|
972
|
+
return current;
|
|
973
|
+
if (!(0, paypal_approved_order_1.paypalQuotesMatch)(current.quote, input.expectedQuote) ||
|
|
974
|
+
current.quote.money.currency !== input.quote.money.currency.toLowerCase()) {
|
|
975
|
+
throw (0, paypal_approved_order_1.approvedOrderError)('PRISM_PAYPAL_ORDER_QUOTE_CHANGED', 'definitely_not_applied');
|
|
976
|
+
}
|
|
977
|
+
const base = `/purchase_units/@reference_id=='${input.rootOperationId}'`;
|
|
978
|
+
const patches = [
|
|
979
|
+
{ op: 'replace', path: `${base}/amount`, value: payload.amount },
|
|
980
|
+
];
|
|
981
|
+
if (payload.items)
|
|
982
|
+
patches.push({ op: 'add', path: `${base}/items`, value: payload.items });
|
|
983
|
+
else if (current.quote.items)
|
|
984
|
+
patches.push({ op: 'remove', path: `${base}/items` });
|
|
985
|
+
// PATCH has no promised provider idempotency. The caller owns serialization;
|
|
986
|
+
// after an uncertain result it must retrieve this order before any next quote.
|
|
987
|
+
const response = await this.paypalResponse('PATCH', `/v2/checkout/orders/${encodeURIComponent(input.connectorPaymentId)}`, patches);
|
|
988
|
+
let updated;
|
|
989
|
+
try {
|
|
990
|
+
updated = await this.retrieveApprovedOrder(input);
|
|
991
|
+
}
|
|
992
|
+
catch (error) {
|
|
993
|
+
// A failed read after an acknowledged PATCH is not a rejected PATCH.
|
|
994
|
+
throw new error_1.PrismOperationError({
|
|
995
|
+
...(0, error_1.normalizePrismError)(error).shape,
|
|
996
|
+
outcome: 'ambiguous',
|
|
997
|
+
...(response.providerRequestId ? { providerRequestId: response.providerRequestId } : {}),
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
if (!(0, paypal_approved_order_1.paypalQuotesMatch)(updated.quote, input.quote) || updated.hasCapture || updated.hasAuthorization) {
|
|
1001
|
+
throw new error_1.PrismOperationError({
|
|
1002
|
+
...(0, paypal_approved_order_1.approvedOrderError)('PRISM_PAYPAL_ORDER_UPDATE_UNCONFIRMED').shape,
|
|
1003
|
+
...(response.providerRequestId ? { providerRequestId: response.providerRequestId } : {}),
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
return {
|
|
1007
|
+
...updated,
|
|
1008
|
+
...(response.providerRequestId ? { providerRequestId: response.providerRequestId } : {}),
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
async captureApprovedOrder(input) {
|
|
1012
|
+
this.assertOrderOwner(input);
|
|
1013
|
+
assertIdentifier(input.idempotencyKey, 'idempotencyKey');
|
|
1014
|
+
const current = await this.retrieveApprovedOrder(input);
|
|
1015
|
+
if (!(0, paypal_approved_order_1.paypalQuotesMatch)(current.quote, input.quote)) {
|
|
1016
|
+
throw (0, paypal_approved_order_1.approvedOrderError)('PRISM_PAYPAL_ORDER_QUOTE_CHANGED', 'definitely_not_applied');
|
|
1017
|
+
}
|
|
1018
|
+
// A pending capture also owns the payment. Never create a second capture.
|
|
1019
|
+
if (current.hasCapture)
|
|
1020
|
+
return current;
|
|
1021
|
+
if (current.hasAuthorization || current.orderStatus !== 'APPROVED') {
|
|
1022
|
+
throw (0, paypal_approved_order_1.approvedOrderError)('PRISM_PAYPAL_ORDER_NOT_APPROVED', 'definitely_not_applied');
|
|
1023
|
+
}
|
|
1024
|
+
const providerKey = (0, node_crypto_1.createHash)('sha256')
|
|
1025
|
+
.update(JSON.stringify([input.credentialBindingId, input.connectorPaymentId, input.idempotencyKey]))
|
|
1026
|
+
.digest('hex')
|
|
1027
|
+
.slice(0, 32);
|
|
1028
|
+
const result = await this.authorizeApprovedPayment({
|
|
1029
|
+
connectorPaymentId: input.connectorPaymentId,
|
|
1030
|
+
operationId: input.rootOperationId,
|
|
1031
|
+
idempotencyKey: providerKey,
|
|
1032
|
+
money: input.quote.money,
|
|
1033
|
+
captureMethod: 'automatic',
|
|
1034
|
+
});
|
|
1035
|
+
let captured;
|
|
1036
|
+
try {
|
|
1037
|
+
captured = await this.retrieveApprovedOrder(input);
|
|
1038
|
+
}
|
|
1039
|
+
catch (error) {
|
|
1040
|
+
// Capture may already have succeeded; a later GET failure cannot undo it.
|
|
1041
|
+
throw new error_1.PrismOperationError({
|
|
1042
|
+
...(0, error_1.normalizePrismError)(error).shape,
|
|
1043
|
+
outcome: 'ambiguous',
|
|
1044
|
+
...(result.providerRequestId ? { providerRequestId: result.providerRequestId } : {}),
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
if (!(0, paypal_approved_order_1.paypalQuotesMatch)(captured.quote, input.quote)) {
|
|
1048
|
+
throw (0, paypal_approved_order_1.approvedOrderError)('PRISM_PAYPAL_ORDER_QUOTE_CHANGED');
|
|
1049
|
+
}
|
|
1050
|
+
if (!captured.capture) {
|
|
1051
|
+
if (result.status === 'failed' || result.status === 'requires_payment_method' ||
|
|
1052
|
+
result.status === 'requires_customer_action') {
|
|
1053
|
+
const category = result.status === 'requires_customer_action'
|
|
1054
|
+
? 'customer_action' : result.failure?.category ?? 'unknown';
|
|
1055
|
+
throw new error_1.PrismOperationError({
|
|
1056
|
+
category: category === 'invalid_payment_method' ? 'validation' : category,
|
|
1057
|
+
providerCode: result.failure?.providerCode ?? 'PRISM_PAYPAL_CAPTURE_NOT_COMPLETED',
|
|
1058
|
+
...(result.providerRequestId ? { providerRequestId: result.providerRequestId } : {}),
|
|
1059
|
+
...(result.providerHttpStatus ? { providerHttpStatus: result.providerHttpStatus } : {}),
|
|
1060
|
+
retryable: false,
|
|
1061
|
+
outcome: category === 'unknown' ? 'ambiguous' : 'definitely_not_applied',
|
|
1062
|
+
safeMessage: result.status === 'requires_customer_action'
|
|
1063
|
+
? 'PayPal requires customer action to complete this payment.'
|
|
1064
|
+
: 'PayPal did not complete this payment.',
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
throw (0, paypal_approved_order_1.approvedOrderError)('PRISM_PAYPAL_CAPTURE_UNCONFIRMED');
|
|
1068
|
+
}
|
|
1069
|
+
return {
|
|
1070
|
+
...captured,
|
|
1071
|
+
...(result.providerRequestId ? { providerRequestId: result.providerRequestId } : {}),
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
873
1074
|
async authorizeApprovedPayment(input) {
|
|
874
1075
|
assertIdentifier(input.operationId, 'operationId');
|
|
875
1076
|
assertIdentifier(input.idempotencyKey, 'idempotencyKey');
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { types } from '../payments/generated/proto';
|
|
2
|
-
import type { PrismCancelInput, PrismCaptureInput, PrismCreateWebhookEndpointInput, PrismDeleteWebhookEndpointInput, PrismDisputeResult, PrismEstablishReusableReferenceInput, PrismFindPaymentInput, PrismFindRefundInput, PrismMerchantOperation, PrismPaymentResult, PrismRefundInput, PrismRefundResult, PrismRetrieveDisputeInput, PrismRetrieveRefundInput, PrismRetryPaymentInput, PrismReusableReferenceDerivation, PrismRetrieveWebhookEndpointInput, PrismRetrievePaymentInput, PrismStripeConnectorOptions, PrismStripeConnectorV1, PrismStripeRuntimeVerification, PrismStripeRuntimeVerificationInput, PrismUpdateWebhookEndpointInput, PrismWebhookEndpoint, PrismWebhookEndpointCreated, PrismWebhookParseInput, PrismWebhookParseResult } from './types';
|
|
2
|
+
import type { PrismCancelInput, PrismCaptureInput, PrismCreateWebhookEndpointInput, PrismDeleteWebhookEndpointInput, PrismDisputeResult, PrismEstablishReusableReferenceInput, PrismFindPaymentInput, PrismFindRefundInput, PrismMerchantOperation, PrismPaymentResult, PrismRefundInput, PrismRefundResult, PrismRetrieveDisputeInput, PrismRetrieveRefundInput, PrismRetryPaymentInput, PrismReusableReferenceDerivation, PrismRetrieveWebhookEndpointInput, PrismRetrievePaymentInput, PrismStripeConnectorOptions, PrismStripeConnectorV1, PrismStripeAuthorizationOwner, PrismStripeAuthorization, PrismStripeIncrementAuthorizationInput, PrismStripeCaptureAuthorizationInput, PrismStripeRuntimeVerification, PrismStripeRuntimeVerificationInput, PrismUpdateWebhookEndpointInput, PrismWebhookEndpoint, PrismWebhookEndpointCreated, PrismWebhookParseInput, PrismWebhookParseResult } from './types';
|
|
3
3
|
interface MerchantAuthenticationPort {
|
|
4
4
|
createClientAuthenticationToken(input: types.IMerchantAuthenticationServiceCreateClientAuthenticationTokenRequest): Promise<types.MerchantAuthenticationServiceCreateClientAuthenticationTokenResponse>;
|
|
5
5
|
}
|
|
6
6
|
interface PaymentPort {
|
|
7
|
+
incrementalAuthorization?(input: types.IPaymentServiceIncrementalAuthorizationRequest): Promise<types.PaymentServiceIncrementalAuthorizationResponse>;
|
|
7
8
|
get(input: types.IPaymentServiceGetRequest): Promise<types.PaymentServiceGetResponse>;
|
|
8
9
|
capture(input: types.IPaymentServiceCaptureRequest): Promise<types.PaymentServiceCaptureResponse>;
|
|
9
10
|
void(input: types.IPaymentServiceVoidRequest): Promise<types.PaymentServiceVoidResponse>;
|
|
@@ -36,6 +37,9 @@ export declare class PrismStripeConnector implements PrismStripeConnectorV1 {
|
|
|
36
37
|
retrievePayment(input: PrismRetrievePaymentInput): Promise<PrismPaymentResult>;
|
|
37
38
|
establishReusableReference(input: PrismEstablishReusableReferenceInput): Promise<PrismReusableReferenceDerivation>;
|
|
38
39
|
findPaymentsByMerchantOperation(input: PrismFindPaymentInput): Promise<PrismPaymentResult[]>;
|
|
40
|
+
retrieveAuthorization(input: PrismStripeAuthorizationOwner): Promise<PrismStripeAuthorization>;
|
|
41
|
+
incrementAuthorization(input: PrismStripeIncrementAuthorizationInput): Promise<PrismStripeAuthorization>;
|
|
42
|
+
captureAuthorization(input: PrismStripeCaptureAuthorizationInput): Promise<PrismStripeAuthorization>;
|
|
39
43
|
capturePayment(input: PrismCaptureInput): Promise<PrismPaymentResult>;
|
|
40
44
|
cancelPayment(input: PrismCancelInput): Promise<PrismPaymentResult>;
|
|
41
45
|
refundPayment(input: PrismRefundInput): Promise<PrismRefundResult>;
|
|
@@ -735,6 +735,7 @@ class PrismStripeConnector {
|
|
|
735
735
|
: 'validation',
|
|
736
736
|
...(providerCode ? { providerCode } : {}),
|
|
737
737
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
738
|
+
providerHttpStatus: response.status,
|
|
738
739
|
retryable: response.status === 429 || response.status >= 500,
|
|
739
740
|
outcome: response.status >= 500 ? 'ambiguous' : 'definitely_not_applied',
|
|
740
741
|
safeMessage: 'Stripe rejected the payment operation.',
|
|
@@ -764,11 +765,13 @@ class PrismStripeConnector {
|
|
|
764
765
|
payment: {
|
|
765
766
|
amount: moneyMessage(input.money),
|
|
766
767
|
connectorCustomerId: input.customerReference,
|
|
767
|
-
setupFutureUsage:
|
|
768
|
+
setupFutureUsage: input.kind === 'combined_checkout' ||
|
|
769
|
+
Boolean(input.futureUse) ||
|
|
768
770
|
input.metadata.alphaweb_setup_future_usage === 'off_session'
|
|
769
771
|
? proto_1.types.FutureUsage.OFF_SESSION
|
|
770
772
|
: proto_1.types.FutureUsage.FUTURE_USAGE_UNSPECIFIED,
|
|
771
|
-
captureMethod: input.
|
|
773
|
+
captureMethod: input.kind === 'combined_checkout' ||
|
|
774
|
+
input.metadata.alphaweb_capture_method === 'manual'
|
|
772
775
|
? proto_1.types.CaptureMethod.MANUAL
|
|
773
776
|
: proto_1.types.CaptureMethod.AUTOMATIC,
|
|
774
777
|
paymentMethodConfigurationId: input.methodSelection.paymentMethodConfigurationId,
|
|
@@ -1073,6 +1076,124 @@ class PrismStripeConnector {
|
|
|
1073
1076
|
}
|
|
1074
1077
|
throw validationError('Stripe payment search exceeded the bounded reconciliation window.', 'PRISM_SEARCH_LIMIT');
|
|
1075
1078
|
}
|
|
1079
|
+
async retrieveAuthorization(input) {
|
|
1080
|
+
assertIdentifier(input.rootOperationId, 'rootOperationId');
|
|
1081
|
+
assertIdentifier(input.connectorPaymentId, 'connectorPaymentId');
|
|
1082
|
+
if (input.credentialBindingId !== this.options.credentialBindingId ||
|
|
1083
|
+
input.providerAccountId !== this.options.providerAccountId ||
|
|
1084
|
+
input.livemode !== (this.options.environment === 'live')) {
|
|
1085
|
+
throw configurationError('Stripe authorization ownership does not match runtime.', 'PRISM_PAYMENT_OWNERSHIP');
|
|
1086
|
+
}
|
|
1087
|
+
const raw = recordOf(await this.stripeRequest('GET', `/payment_intents/${encodeURIComponent(input.connectorPaymentId)}?expand[]=latest_charge`));
|
|
1088
|
+
const intent = parseStripePaymentIntent(raw);
|
|
1089
|
+
const capturable = integerOf(raw.amount_capturable);
|
|
1090
|
+
const received = integerOf(raw.amount_received);
|
|
1091
|
+
const charge = recordOf(raw.latest_charge);
|
|
1092
|
+
if (intent.id !== input.connectorPaymentId || intent.metadata.alphaweb_operation_id !== input.rootOperationId ||
|
|
1093
|
+
intent.metadata.alphaweb_operation_kind !== 'combined_checkout' || intent.livemode !== input.livemode ||
|
|
1094
|
+
raw.capture_method !== 'manual' || capturable === null || received === null || capturable < 0 || received < 0 ||
|
|
1095
|
+
!Number.isSafeInteger(capturable) || !Number.isSafeInteger(received)) {
|
|
1096
|
+
throw configurationError('Stripe authorization does not match its checkout.', 'PRISM_PAYMENT_OWNERSHIP');
|
|
1097
|
+
}
|
|
1098
|
+
(0, money_1.assertSafeProviderMoney)({ amountMinor: intent.amount.toString(), currency: intent.currency });
|
|
1099
|
+
const captureId = stringOf(charge.id);
|
|
1100
|
+
if (received > 0 && (!captureId || charge.payment_intent !== intent.id ||
|
|
1101
|
+
charge.livemode !== input.livemode || charge.captured !== true || charge.paid !== true ||
|
|
1102
|
+
charge.amount_captured !== received || charge.currency !== intent.currency)) {
|
|
1103
|
+
throw configurationError('Stripe capture evidence does not match its checkout.', 'PRISM_CAPTURE_OWNERSHIP');
|
|
1104
|
+
}
|
|
1105
|
+
const card = recordOf(recordOf(charge.payment_method_details).card);
|
|
1106
|
+
const clientSecret = stringOf(raw.client_secret);
|
|
1107
|
+
return {
|
|
1108
|
+
connectorPaymentId: intent.id, status: normalizedStripeIntentStatus(intent),
|
|
1109
|
+
money: { amountMinor: intent.amount.toString(), currency: intent.currency },
|
|
1110
|
+
amountCapturable: capturable.toString(), amountReceived: received.toString(),
|
|
1111
|
+
incrementalAuthorizationAvailable: recordOf(card.incremental_authorization).status === 'available',
|
|
1112
|
+
lastAdjustmentId: intent.metadata.alphaweb_adjustment_id ?? null,
|
|
1113
|
+
connectorCaptureId: received > 0 ? captureId : null,
|
|
1114
|
+
...(['requires_payment_method', 'requires_customer_action'].includes(normalizedStripeIntentStatus(intent)) &&
|
|
1115
|
+
clientSecret?.startsWith(`${intent.id}_secret_`) ? { clientSecret } : {}),
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
async incrementAuthorization(input) {
|
|
1119
|
+
assertIdentifier(input.idempotencyKey, 'idempotencyKey');
|
|
1120
|
+
const target = (0, money_1.assertSafeProviderMoney)(input.money);
|
|
1121
|
+
const expected = (0, money_1.assertSafeProviderMoney)(input.expectedMoney);
|
|
1122
|
+
const observed = await this.retrieveAuthorization(input);
|
|
1123
|
+
if (observed.status !== 'authorized' || observed.amountReceived !== '0' ||
|
|
1124
|
+
observed.money.currency !== target.currency || expected.currency !== target.currency ||
|
|
1125
|
+
BigInt(observed.amountCapturable) < BigInt(observed.money.amountMinor)) {
|
|
1126
|
+
throw validationError('Stripe authorization cannot be adjusted in this state.', 'PRISM_AUTHORIZATION_STATE');
|
|
1127
|
+
}
|
|
1128
|
+
if (observed.lastAdjustmentId === input.idempotencyKey && observed.money.amountMinor === input.money.amountMinor)
|
|
1129
|
+
return observed;
|
|
1130
|
+
if (BigInt(observed.amountCapturable) < BigInt(input.expectedMoney.amountMinor)) {
|
|
1131
|
+
throw configurationError('Stripe no longer covers the approved selection.', 'PRISM_AUTHORIZATION_AMOUNT');
|
|
1132
|
+
}
|
|
1133
|
+
// A smaller final total is captured for less, without modifying the hold.
|
|
1134
|
+
if (BigInt(input.money.amountMinor) <= BigInt(observed.amountCapturable))
|
|
1135
|
+
return observed;
|
|
1136
|
+
if (!observed.incrementalAuthorizationAvailable) {
|
|
1137
|
+
throw validationError('This payment cannot authorize a higher total. The offer was not added.', 'PRISM_INCREMENT_UNAVAILABLE');
|
|
1138
|
+
}
|
|
1139
|
+
const increment = this.dependencies.payments.incrementalAuthorization;
|
|
1140
|
+
if (!increment)
|
|
1141
|
+
throw configurationError('Prism lacks incremental authorization.', 'PRISM_CAPABILITY_UNSUPPORTED');
|
|
1142
|
+
try {
|
|
1143
|
+
const result = await increment.call(this.dependencies.payments, {
|
|
1144
|
+
merchantAuthorizationId: input.idempotencyKey,
|
|
1145
|
+
connectorTransactionId: input.connectorPaymentId, amount: moneyMessage(input.money),
|
|
1146
|
+
});
|
|
1147
|
+
const error = recordOf(result.error);
|
|
1148
|
+
if (result.statusCode >= 400 || Object.keys(error).length) {
|
|
1149
|
+
throw new error_1.PrismOperationError({
|
|
1150
|
+
category: result.statusCode >= 500 ? 'provider_server' : 'declined',
|
|
1151
|
+
outcome: result.statusCode >= 500 ? 'ambiguous' : 'definitely_not_applied', retryable: false,
|
|
1152
|
+
providerHttpStatus: result.statusCode, providerCode: stringOf(error.code) ?? undefined,
|
|
1153
|
+
safeMessage: 'Stripe did not authorize the higher total. The offer was not added.',
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
catch (error) {
|
|
1158
|
+
throw (0, error_1.normalizePrismError)(error);
|
|
1159
|
+
}
|
|
1160
|
+
let aligned;
|
|
1161
|
+
try {
|
|
1162
|
+
aligned = await this.retrieveAuthorization(input);
|
|
1163
|
+
}
|
|
1164
|
+
catch (error) {
|
|
1165
|
+
throw new error_1.PrismOperationError({ ...(0, error_1.normalizePrismError)(error).shape, outcome: 'ambiguous', retryable: false });
|
|
1166
|
+
}
|
|
1167
|
+
if (aligned.lastAdjustmentId !== input.idempotencyKey || aligned.money.amountMinor !== input.money.amountMinor ||
|
|
1168
|
+
aligned.status !== 'authorized' || BigInt(aligned.amountCapturable) < BigInt(input.money.amountMinor)) {
|
|
1169
|
+
throw new error_1.PrismOperationError({ category: 'unknown', outcome: 'ambiguous', retryable: false,
|
|
1170
|
+
providerCode: 'PRISM_INCREMENT_EVIDENCE', safeMessage: 'Stripe authorization update is awaiting verification.' });
|
|
1171
|
+
}
|
|
1172
|
+
return aligned;
|
|
1173
|
+
}
|
|
1174
|
+
async captureAuthorization(input) {
|
|
1175
|
+
assertIdentifier(input.idempotencyKey, 'idempotencyKey');
|
|
1176
|
+
const target = (0, money_1.assertSafeProviderMoney)(input.money);
|
|
1177
|
+
const observed = await this.retrieveAuthorization(input);
|
|
1178
|
+
if (observed.money.currency !== target.currency)
|
|
1179
|
+
throw configurationError('Stripe capture currency changed.', 'PRISM_CAPTURE_OWNERSHIP');
|
|
1180
|
+
if (observed.status === 'captured' && observed.amountReceived === input.money.amountMinor)
|
|
1181
|
+
return observed;
|
|
1182
|
+
if (observed.status !== 'authorized' || observed.amountReceived !== '0' || BigInt(observed.amountCapturable) < BigInt(input.money.amountMinor)) {
|
|
1183
|
+
throw validationError('Stripe has not authorized the final checkout total.', 'PRISM_CAPTURE_AMOUNT');
|
|
1184
|
+
}
|
|
1185
|
+
const result = await this.capturePayment({ operationId: input.rootOperationId, idempotencyKey: input.idempotencyKey,
|
|
1186
|
+
connectorPaymentId: input.connectorPaymentId, money: input.money });
|
|
1187
|
+
if (result.status === 'failed')
|
|
1188
|
+
throw new error_1.PrismOperationError({ category: 'declined',
|
|
1189
|
+
outcome: 'definitely_not_applied', retryable: false, safeMessage: 'Stripe declined the final capture.' });
|
|
1190
|
+
try {
|
|
1191
|
+
return await this.retrieveAuthorization(input);
|
|
1192
|
+
}
|
|
1193
|
+
catch (error) {
|
|
1194
|
+
throw new error_1.PrismOperationError({ ...(0, error_1.normalizePrismError)(error).shape, outcome: 'ambiguous', retryable: false });
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1076
1197
|
async capturePayment(input) {
|
|
1077
1198
|
assertIdentifier(input.idempotencyKey, 'idempotencyKey');
|
|
1078
1199
|
try {
|
|
@@ -5,7 +5,7 @@ export type PrismEnvironment = 'test' | 'live';
|
|
|
5
5
|
* payment contract a Stripe/PayPal-only union.
|
|
6
6
|
*/
|
|
7
7
|
export type PrismConnectorId = string;
|
|
8
|
-
export type PrismOperationKind = 'checkout' | 'post_purchase' | 'subscription_renewal' | 'refund' | 'cancel';
|
|
8
|
+
export type PrismOperationKind = 'checkout' | 'combined_checkout' | 'post_purchase' | 'subscription_renewal' | 'refund' | 'cancel';
|
|
9
9
|
export interface PrismMoney {
|
|
10
10
|
amountMinor: string;
|
|
11
11
|
currency: string;
|
|
@@ -58,6 +58,8 @@ export interface PrismPayPalBrowserSession {
|
|
|
58
58
|
clientId: string;
|
|
59
59
|
environment: 'sandbox' | 'production';
|
|
60
60
|
savePayment: boolean;
|
|
61
|
+
/** False for an approved order whose final total is selected before capture. */
|
|
62
|
+
commit?: boolean;
|
|
61
63
|
}
|
|
62
64
|
/**
|
|
63
65
|
* Declaration-merging extension point for future qualified connectors.
|
|
@@ -117,6 +119,7 @@ export interface PrismPaymentResult {
|
|
|
117
119
|
connectorMetadata?: string;
|
|
118
120
|
reusableReference?: PrismReusablePaymentReference;
|
|
119
121
|
providerRequestId?: string;
|
|
122
|
+
providerHttpStatus?: number;
|
|
120
123
|
failure?: PrismPaymentFailure;
|
|
121
124
|
}
|
|
122
125
|
export interface PrismRefundResult {
|
|
@@ -324,7 +327,39 @@ export interface PrismConnectorV1 {
|
|
|
324
327
|
deleteWebhookEndpoint(input: PrismDeleteWebhookEndpointInput): Promise<void>;
|
|
325
328
|
retrieveWebhookEndpoint(input: PrismRetrieveWebhookEndpointInput): Promise<PrismWebhookEndpoint>;
|
|
326
329
|
}
|
|
327
|
-
export
|
|
330
|
+
export interface PrismStripeAuthorizationOwner {
|
|
331
|
+
rootOperationId: string;
|
|
332
|
+
connectorPaymentId: string;
|
|
333
|
+
credentialBindingId: string;
|
|
334
|
+
providerAccountId: string;
|
|
335
|
+
livemode: boolean;
|
|
336
|
+
}
|
|
337
|
+
export interface PrismStripeAuthorization {
|
|
338
|
+
connectorPaymentId: string;
|
|
339
|
+
status: PrismNormalizedPaymentStatus;
|
|
340
|
+
money: PrismMoney;
|
|
341
|
+
amountCapturable: string;
|
|
342
|
+
amountReceived: string;
|
|
343
|
+
incrementalAuthorizationAvailable: boolean;
|
|
344
|
+
lastAdjustmentId: string | null;
|
|
345
|
+
connectorCaptureId: string | null;
|
|
346
|
+
/** Only returned for an unconfirmed, owned PaymentIntent. Never log this. */
|
|
347
|
+
clientSecret?: string;
|
|
348
|
+
}
|
|
349
|
+
export interface PrismStripeIncrementAuthorizationInput extends PrismStripeAuthorizationOwner {
|
|
350
|
+
expectedMoney: PrismMoney;
|
|
351
|
+
money: PrismMoney;
|
|
352
|
+
idempotencyKey: string;
|
|
353
|
+
}
|
|
354
|
+
export interface PrismStripeCaptureAuthorizationInput extends PrismStripeAuthorizationOwner {
|
|
355
|
+
money: PrismMoney;
|
|
356
|
+
idempotencyKey: string;
|
|
357
|
+
}
|
|
358
|
+
export interface PrismStripeConnectorV1 extends PrismConnectorV1 {
|
|
359
|
+
retrieveAuthorization?(input: PrismStripeAuthorizationOwner): Promise<PrismStripeAuthorization>;
|
|
360
|
+
incrementAuthorization?(input: PrismStripeIncrementAuthorizationInput): Promise<PrismStripeAuthorization>;
|
|
361
|
+
captureAuthorization?(input: PrismStripeCaptureAuthorizationInput): Promise<PrismStripeAuthorization>;
|
|
362
|
+
}
|
|
328
363
|
export type PrismPayPalOrderLifecycleStatus = 'CREATED' | 'SAVED' | 'PAYER_ACTION_REQUIRED' | 'APPROVED' | 'VOIDED' | 'COMPLETED' | 'UNKNOWN';
|
|
329
364
|
export interface PrismPayPalOrderLifecycle {
|
|
330
365
|
orderStatus: PrismPayPalOrderLifecycleStatus;
|
|
@@ -334,9 +369,69 @@ export interface PrismPayPalOrderLifecycle {
|
|
|
334
369
|
export interface PrismRetrieveOrderLifecycleInput {
|
|
335
370
|
connectorPaymentId: string;
|
|
336
371
|
}
|
|
372
|
+
export interface PrismPayPalOrderItem {
|
|
373
|
+
name: string;
|
|
374
|
+
sku?: string;
|
|
375
|
+
category?: 'DIGITAL_GOODS' | 'PHYSICAL_GOODS' | 'DONATION';
|
|
376
|
+
quantity: number;
|
|
377
|
+
unitAmountMinor: string;
|
|
378
|
+
taxMinor?: string;
|
|
379
|
+
/** Existing merchant-managed recurring disclosure; never inferred from an OTO total. */
|
|
380
|
+
recurringAgreement?: PrismRecurringAgreement;
|
|
381
|
+
}
|
|
382
|
+
export interface PrismPayPalOrderQuote {
|
|
383
|
+
money: PrismMoney;
|
|
384
|
+
breakdown?: {
|
|
385
|
+
itemTotalMinor?: string;
|
|
386
|
+
taxTotalMinor?: string;
|
|
387
|
+
shippingMinor?: string;
|
|
388
|
+
handlingMinor?: string;
|
|
389
|
+
insuranceMinor?: string;
|
|
390
|
+
shippingDiscountMinor?: string;
|
|
391
|
+
discountMinor?: string;
|
|
392
|
+
};
|
|
393
|
+
items?: PrismPayPalOrderItem[];
|
|
394
|
+
}
|
|
395
|
+
export interface PrismPayPalOrderOwner {
|
|
396
|
+
connectorPaymentId: string;
|
|
397
|
+
/** The original order-creation operation, never the later adjustment/capture. */
|
|
398
|
+
rootOperationId: string;
|
|
399
|
+
credentialBindingId: string;
|
|
400
|
+
providerAccountId: string;
|
|
401
|
+
livemode: boolean;
|
|
402
|
+
expectedCustomId?: string;
|
|
403
|
+
}
|
|
404
|
+
export interface PrismPayPalApprovedOrder extends PrismPayPalOrderLifecycle {
|
|
405
|
+
connectorPaymentId: string;
|
|
406
|
+
rootOperationId: string;
|
|
407
|
+
quote: PrismPayPalOrderQuote;
|
|
408
|
+
providerUpdatedAt?: string;
|
|
409
|
+
providerRequestId?: string;
|
|
410
|
+
/** Native Prism metadata, retained for refunds even after a lost capture response. */
|
|
411
|
+
connectorMetadata?: string;
|
|
412
|
+
capture?: {
|
|
413
|
+
connectorCaptureId: string;
|
|
414
|
+
status: 'pending' | 'captured' | 'failed' | 'refunded' | 'partially_refunded' | 'unknown';
|
|
415
|
+
money: PrismMoney;
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
export interface PrismPayPalUpdateApprovedOrderInput extends PrismPayPalOrderOwner {
|
|
419
|
+
expectedQuote: PrismPayPalOrderQuote;
|
|
420
|
+
quote: PrismPayPalOrderQuote;
|
|
421
|
+
}
|
|
422
|
+
export interface PrismPayPalCaptureApprovedOrderInput extends PrismPayPalOrderOwner {
|
|
423
|
+
quote: PrismPayPalOrderQuote;
|
|
424
|
+
/** Persist once for the final capture; must not change when a claim is renewed. */
|
|
425
|
+
idempotencyKey: string;
|
|
426
|
+
}
|
|
337
427
|
export interface PrismPayPalConnectorV1 extends PrismConnectorV1 {
|
|
338
428
|
authorizeApprovedPayment(input: PrismAuthorizeApprovedPaymentInput): Promise<PrismPaymentResult>;
|
|
339
429
|
retrieveOrderLifecycle?(input: PrismRetrieveOrderLifecycleInput): Promise<PrismPayPalOrderLifecycle>;
|
|
430
|
+
createAdjustableOrder?(input: PrismMerchantOperation): Promise<PrismPaymentResult>;
|
|
431
|
+
retrieveApprovedOrder?(input: PrismPayPalOrderOwner): Promise<PrismPayPalApprovedOrder>;
|
|
432
|
+
/** Caller serializes decisions and reconciles an uncertain PATCH before another mutation. */
|
|
433
|
+
updateApprovedOrder?(input: PrismPayPalUpdateApprovedOrderInput): Promise<PrismPayPalApprovedOrder>;
|
|
434
|
+
captureApprovedOrder?(input: PrismPayPalCaptureApprovedOrderInput): Promise<PrismPayPalApprovedOrder>;
|
|
340
435
|
}
|
|
341
436
|
export interface PrismStripeConnectorOptions {
|
|
342
437
|
environment: PrismEnvironment;
|
|
Binary file
|
package/package.json
CHANGED