@alphawebai/hs-prism 0.1.0-rc.39 → 0.1.0-rc.41
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 +2 -1
- package/dist/src/alphaweb/manifest.js +2 -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/types.d.ts +64 -1
- 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.41";
|
|
5
5
|
readonly upstreamCommit: "b07af7d9dac271de729548e74217ddcf895c602b";
|
|
6
6
|
readonly medusaRange: ">=2.17.2 <2.18.0";
|
|
7
7
|
readonly stripeApiVersion: "2026-06-24.dahlia";
|
|
@@ -34,6 +34,7 @@ export declare const ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST: Readonly<{
|
|
|
34
34
|
};
|
|
35
35
|
readonly paypal: {
|
|
36
36
|
readonly browserSession: "paypal_checkout";
|
|
37
|
+
readonly approvedOrderCheckout: "v1";
|
|
37
38
|
readonly oneTimePayment: true;
|
|
38
39
|
readonly futureUse: true;
|
|
39
40
|
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.41',
|
|
53
53
|
upstreamCommit: 'b07af7d9dac271de729548e74217ddcf895c602b',
|
|
54
54
|
medusaRange: '>=2.17.2 <2.18.0',
|
|
55
55
|
stripeApiVersion: '2026-06-24.dahlia',
|
|
@@ -82,6 +82,7 @@ exports.ALPHAWEB_PRISM_COMPATIBILITY_MANIFEST = Object.freeze({
|
|
|
82
82
|
},
|
|
83
83
|
paypal: {
|
|
84
84
|
browserSession: 'paypal_checkout',
|
|
85
|
+
approvedOrderCheckout: 'v1',
|
|
85
86
|
oneTimePayment: true,
|
|
86
87
|
futureUse: true,
|
|
87
88
|
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');
|
|
@@ -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 {
|
|
@@ -334,9 +337,69 @@ export interface PrismPayPalOrderLifecycle {
|
|
|
334
337
|
export interface PrismRetrieveOrderLifecycleInput {
|
|
335
338
|
connectorPaymentId: string;
|
|
336
339
|
}
|
|
340
|
+
export interface PrismPayPalOrderItem {
|
|
341
|
+
name: string;
|
|
342
|
+
sku?: string;
|
|
343
|
+
category?: 'DIGITAL_GOODS' | 'PHYSICAL_GOODS' | 'DONATION';
|
|
344
|
+
quantity: number;
|
|
345
|
+
unitAmountMinor: string;
|
|
346
|
+
taxMinor?: string;
|
|
347
|
+
/** Existing merchant-managed recurring disclosure; never inferred from an OTO total. */
|
|
348
|
+
recurringAgreement?: PrismRecurringAgreement;
|
|
349
|
+
}
|
|
350
|
+
export interface PrismPayPalOrderQuote {
|
|
351
|
+
money: PrismMoney;
|
|
352
|
+
breakdown?: {
|
|
353
|
+
itemTotalMinor?: string;
|
|
354
|
+
taxTotalMinor?: string;
|
|
355
|
+
shippingMinor?: string;
|
|
356
|
+
handlingMinor?: string;
|
|
357
|
+
insuranceMinor?: string;
|
|
358
|
+
shippingDiscountMinor?: string;
|
|
359
|
+
discountMinor?: string;
|
|
360
|
+
};
|
|
361
|
+
items?: PrismPayPalOrderItem[];
|
|
362
|
+
}
|
|
363
|
+
export interface PrismPayPalOrderOwner {
|
|
364
|
+
connectorPaymentId: string;
|
|
365
|
+
/** The original order-creation operation, never the later adjustment/capture. */
|
|
366
|
+
rootOperationId: string;
|
|
367
|
+
credentialBindingId: string;
|
|
368
|
+
providerAccountId: string;
|
|
369
|
+
livemode: boolean;
|
|
370
|
+
expectedCustomId?: string;
|
|
371
|
+
}
|
|
372
|
+
export interface PrismPayPalApprovedOrder extends PrismPayPalOrderLifecycle {
|
|
373
|
+
connectorPaymentId: string;
|
|
374
|
+
rootOperationId: string;
|
|
375
|
+
quote: PrismPayPalOrderQuote;
|
|
376
|
+
providerUpdatedAt?: string;
|
|
377
|
+
providerRequestId?: string;
|
|
378
|
+
/** Native Prism metadata, retained for refunds even after a lost capture response. */
|
|
379
|
+
connectorMetadata?: string;
|
|
380
|
+
capture?: {
|
|
381
|
+
connectorCaptureId: string;
|
|
382
|
+
status: 'pending' | 'captured' | 'failed' | 'refunded' | 'partially_refunded' | 'unknown';
|
|
383
|
+
money: PrismMoney;
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
export interface PrismPayPalUpdateApprovedOrderInput extends PrismPayPalOrderOwner {
|
|
387
|
+
expectedQuote: PrismPayPalOrderQuote;
|
|
388
|
+
quote: PrismPayPalOrderQuote;
|
|
389
|
+
}
|
|
390
|
+
export interface PrismPayPalCaptureApprovedOrderInput extends PrismPayPalOrderOwner {
|
|
391
|
+
quote: PrismPayPalOrderQuote;
|
|
392
|
+
/** Persist once for the final capture; must not change when a claim is renewed. */
|
|
393
|
+
idempotencyKey: string;
|
|
394
|
+
}
|
|
337
395
|
export interface PrismPayPalConnectorV1 extends PrismConnectorV1 {
|
|
338
396
|
authorizeApprovedPayment(input: PrismAuthorizeApprovedPaymentInput): Promise<PrismPaymentResult>;
|
|
339
397
|
retrieveOrderLifecycle?(input: PrismRetrieveOrderLifecycleInput): Promise<PrismPayPalOrderLifecycle>;
|
|
398
|
+
createAdjustableOrder?(input: PrismMerchantOperation): Promise<PrismPaymentResult>;
|
|
399
|
+
retrieveApprovedOrder?(input: PrismPayPalOrderOwner): Promise<PrismPayPalApprovedOrder>;
|
|
400
|
+
/** Caller serializes decisions and reconciles an uncertain PATCH before another mutation. */
|
|
401
|
+
updateApprovedOrder?(input: PrismPayPalUpdateApprovedOrderInput): Promise<PrismPayPalApprovedOrder>;
|
|
402
|
+
captureApprovedOrder?(input: PrismPayPalCaptureApprovedOrderInput): Promise<PrismPayPalApprovedOrder>;
|
|
340
403
|
}
|
|
341
404
|
export interface PrismStripeConnectorOptions {
|
|
342
405
|
environment: PrismEnvironment;
|
|
Binary file
|
package/package.json
CHANGED