vona-module-pay-stripe 5.0.4 → 5.0.6
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/bean/payProvider.stripe.d.ts +23 -10
- package/dist/bean/payProvider.stripe.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +502 -15
- package/dist/index.js.map +1 -1
- package/dist/lib/stripeGateway.d.ts +21 -0
- package/dist/lib/stripeGateway.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/bean/payProvider.stripe.ts +458 -22
- package/src/index.ts +1 -0
- package/src/lib/stripeGateway.ts +85 -0
- package/src/types/index.ts +2 -0
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
import type { IDecoratorPayProviderOptions, IPayProviderClientOptions, IPayProviderClientRecord, IPayProviderExecute, IPayProviderPaymentInput, IPayProviderPaymentSnapshot, IPayProviderRefundInput, IPayProviderRefundSnapshot, IPayProviderVerifiedWebhook, IPayProviderWebhookInput } from 'vona-module-a-pay';
|
|
2
2
|
import { BeanBase } from 'vona';
|
|
3
|
+
import type { IStripeGateway } from '../lib/stripeGateway.ts';
|
|
3
4
|
export interface IPayProviderStripeClientRecord extends IPayProviderClientRecord {
|
|
4
5
|
}
|
|
5
6
|
export interface IPayProviderStripeClientOptions extends IPayProviderClientOptions {
|
|
6
7
|
secretCredential: string | undefined;
|
|
7
8
|
secretWebhook: string | undefined;
|
|
9
|
+
gateway?: IStripeGateway;
|
|
8
10
|
}
|
|
9
11
|
export interface IPayProviderOptionsStripe extends IDecoratorPayProviderOptions<IPayProviderStripeClientRecord, IPayProviderStripeClientOptions> {
|
|
10
12
|
}
|
|
11
13
|
export declare class PayProviderStripe extends BeanBase implements IPayProviderExecute<IPayProviderStripeClientOptions> {
|
|
12
|
-
startPayment(
|
|
13
|
-
confirmPayment(
|
|
14
|
-
queryPayment(
|
|
15
|
-
createRefund(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
startPayment(input: IPayProviderPaymentInput, clientOptions: IPayProviderStripeClientOptions): Promise<IPayProviderPaymentSnapshot>;
|
|
15
|
+
confirmPayment(input: IPayProviderPaymentInput, clientOptions: IPayProviderStripeClientOptions): Promise<IPayProviderPaymentSnapshot>;
|
|
16
|
+
queryPayment(input: IPayProviderPaymentInput, clientOptions: IPayProviderStripeClientOptions): Promise<IPayProviderPaymentSnapshot>;
|
|
17
|
+
createRefund(input: IPayProviderRefundInput, clientOptions: IPayProviderStripeClientOptions): Promise<IPayProviderRefundSnapshot>;
|
|
18
|
+
queryRefund(input: IPayProviderRefundInput, clientOptions: IPayProviderStripeClientOptions): Promise<IPayProviderRefundSnapshot>;
|
|
19
|
+
verifyWebhook(input: IPayProviderWebhookInput, clientOptions: IPayProviderStripeClientOptions): Promise<IPayProviderVerifiedWebhook>;
|
|
20
|
+
private _mapCheckoutWebhook;
|
|
21
|
+
private _mapPaymentIntentWebhook;
|
|
22
|
+
private _mapRefundWebhook;
|
|
23
|
+
private _inputFromCheckoutSession;
|
|
24
|
+
private _mapCheckoutSession;
|
|
25
|
+
private _mapPaymentIntent;
|
|
26
|
+
private _mapRefund;
|
|
27
|
+
private _paymentMetadata;
|
|
28
|
+
private _refundMetadata;
|
|
29
|
+
private _gateway;
|
|
30
|
+
private _gatewayOptions;
|
|
31
|
+
private _matchesEnvironment;
|
|
32
|
+
private _isDefinitiveRefundRejection;
|
|
33
|
+
private _throwTerminalPaymentFailure;
|
|
34
|
+
private _throwGatewayError;
|
|
22
35
|
}
|
|
23
36
|
//# sourceMappingURL=payProvider.stripe.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payProvider.stripe.d.ts","sourceRoot":"","sources":["../../src/bean/payProvider.stripe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,4BAA4B,EAC5B,yBAAyB,EACzB,wBAAwB,EACxB,mBAAmB,EACnB,wBAAwB,EACxB,2BAA2B,EAC3B,uBAAuB,EACvB,0BAA0B,EAC1B,2BAA2B,EAC3B,wBAAwB,EACzB,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"payProvider.stripe.d.ts","sourceRoot":"","sources":["../../src/bean/payProvider.stripe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,4BAA4B,EAC5B,yBAAyB,EACzB,wBAAwB,EACxB,mBAAmB,EACnB,wBAAwB,EACxB,2BAA2B,EAC3B,uBAAuB,EACvB,0BAA0B,EAC1B,2BAA2B,EAC3B,wBAAwB,EACzB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,QAAQ,EAAU,MAAM,MAAM,CAAC;AAGxC,OAAO,KAAK,EAAE,cAAc,EAAyB,MAAM,yBAAyB,CAAC;AAIrF,MAAM,WAAW,8BAA+B,SAAQ,wBAAwB;CAAG;AAEnF,MAAM,WAAW,+BAAgC,SAAQ,yBAAyB;IAChF,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAID,MAAM,WAAW,yBAA0B,SAAQ,4BAA4B,CAC7E,8BAA8B,EAC9B,+BAA+B,CAChC;CAAG;AAEJ,qBAqBa,iBACX,SAAQ,QACR,YAAW,mBAAmB,CAAC,+BAA+B,CAAC;IAEzD,YAAY,CAChB,KAAK,EAAE,wBAAwB,EAC/B,aAAa,EAAE,+BAA+B,GAC7C,OAAO,CAAC,2BAA2B,CAAC;IAsCjC,cAAc,CAClB,KAAK,EAAE,wBAAwB,EAC/B,aAAa,EAAE,+BAA+B,GAC7C,OAAO,CAAC,2BAA2B,CAAC;IAIjC,YAAY,CAChB,KAAK,EAAE,wBAAwB,EAC/B,aAAa,EAAE,+BAA+B,GAC7C,OAAO,CAAC,2BAA2B,CAAC;IAUjC,YAAY,CAChB,KAAK,EAAE,uBAAuB,EAC9B,aAAa,EAAE,+BAA+B,GAC7C,OAAO,CAAC,0BAA0B,CAAC;IAmBhC,WAAW,CACf,KAAK,EAAE,uBAAuB,EAC9B,aAAa,EAAE,+BAA+B,GAC7C,OAAO,CAAC,0BAA0B,CAAC;IAUhC,aAAa,CACjB,KAAK,EAAE,wBAAwB,EAC/B,aAAa,EAAE,+BAA+B,GAC7C,OAAO,CAAC,2BAA2B,CAAC;YA4BzB,mBAAmB;YAkCnB,wBAAwB;YAgDxB,iBAAiB;YAsCjB,yBAAyB;IA4BvC,OAAO,CAAC,mBAAmB;IAmD3B,OAAO,CAAC,iBAAiB;IAmCzB,OAAO,CAAC,UAAU;IAyBlB,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,4BAA4B;IAIpC,OAAO,CAAC,4BAA4B;IAUpC,OAAO,CAAC,kBAAkB;CAI3B"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,+BAA+B,EAAE,MAAM,8BAA8B,CAAC;AACpF,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,31 +1,498 @@
|
|
|
1
1
|
import { useApp, BeanInfo, BeanBase, BeanScopeBase } from 'vona';
|
|
2
2
|
import { __decorate } from 'tslib';
|
|
3
|
-
import { PayProvider } from 'vona-module-a-pay';
|
|
3
|
+
import { PayProvider, ProviderOperationFailure } from 'vona-module-a-pay';
|
|
4
|
+
import Stripe from 'stripe';
|
|
4
5
|
import { Scope } from 'vona-module-a-bean';
|
|
5
6
|
|
|
7
|
+
const StripeRequestTimeoutMilliseconds = 15_000;
|
|
8
|
+
class StripeGatewayError extends Error {
|
|
9
|
+
constructor(status, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.status = void 0;
|
|
12
|
+
this.status = status;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function createStripeGateway() {
|
|
16
|
+
return {
|
|
17
|
+
async createCheckoutSession(options, input) {
|
|
18
|
+
const request = input;
|
|
19
|
+
return await createClient(options).checkout.sessions.create(request.body, {
|
|
20
|
+
idempotencyKey: request.idempotencyKey
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
async retrieveCheckoutSession(options, input) {
|
|
24
|
+
const request = input;
|
|
25
|
+
return await createClient(options).checkout.sessions.retrieve(request.id, {
|
|
26
|
+
expand: ['payment_intent']
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
async createRefund(options, input) {
|
|
30
|
+
const request = input;
|
|
31
|
+
return await createClient(options).refunds.create(request.body, {
|
|
32
|
+
idempotencyKey: request.idempotencyKey
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
async retrieveRefund(options, input) {
|
|
36
|
+
const request = input;
|
|
37
|
+
return await createClient(options).refunds.retrieve(request.id);
|
|
38
|
+
},
|
|
39
|
+
async constructWebhookEvent(options, input) {
|
|
40
|
+
const signature = getHeader(input.headers, 'stripe-signature');
|
|
41
|
+
if (!input.rawBody || !signature) {
|
|
42
|
+
throw new StripeGatewayError(401, 'Stripe webhook signature is invalid');
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return createClient(options).webhooks.constructEvent(input.rawBody, signature, options.webhookSecret);
|
|
46
|
+
} catch {
|
|
47
|
+
throw new StripeGatewayError(401, 'Stripe webhook signature is invalid');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function createClient(options) {
|
|
53
|
+
return new Stripe(options.secretKey, {
|
|
54
|
+
maxNetworkRetries: 0,
|
|
55
|
+
timeout: StripeRequestTimeoutMilliseconds
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function getHeader(headers, key) {
|
|
59
|
+
const value = headers[key];
|
|
60
|
+
return Array.isArray(value) ? value[0] : value;
|
|
61
|
+
}
|
|
62
|
+
|
|
6
63
|
const app = useApp();
|
|
7
64
|
let PayProviderStripe = class PayProviderStripe extends BeanBase {
|
|
8
|
-
async startPayment(
|
|
9
|
-
this.
|
|
65
|
+
async startPayment(input, clientOptions) {
|
|
66
|
+
if (input.providerOrderId) return await this.queryPayment(input, clientOptions);
|
|
67
|
+
if (!input.returnUrl || !input.cancelUrl) {
|
|
68
|
+
this.app.throw(500, 'Stripe payment requires return and cancel URLs');
|
|
69
|
+
}
|
|
70
|
+
const gatewayOptions = this._gatewayOptions(clientOptions);
|
|
71
|
+
assertStripeMoney(input.amountMinor, input.currency);
|
|
72
|
+
const body = {
|
|
73
|
+
mode: 'payment',
|
|
74
|
+
success_url: input.returnUrl,
|
|
75
|
+
cancel_url: input.cancelUrl,
|
|
76
|
+
client_reference_id: input.providerCorrelationReference,
|
|
77
|
+
metadata: this._paymentMetadata(input),
|
|
78
|
+
payment_intent_data: {
|
|
79
|
+
metadata: this._paymentMetadata(input)
|
|
80
|
+
},
|
|
81
|
+
managed_payments: {
|
|
82
|
+
enabled: false
|
|
83
|
+
},
|
|
84
|
+
line_items: [{
|
|
85
|
+
quantity: 1,
|
|
86
|
+
price_data: {
|
|
87
|
+
currency: input.currency.toLowerCase(),
|
|
88
|
+
unit_amount: input.amountMinor,
|
|
89
|
+
product_data: {
|
|
90
|
+
name: `Payment ${input.providerInvoiceReference}`
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}]
|
|
94
|
+
};
|
|
95
|
+
let checkoutSession;
|
|
96
|
+
try {
|
|
97
|
+
checkoutSession = await this._gateway(clientOptions).createCheckoutSession(gatewayOptions, {
|
|
98
|
+
idempotencyKey: input.idempotencyKey,
|
|
99
|
+
body
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
this._throwTerminalPaymentFailure(error);
|
|
103
|
+
}
|
|
104
|
+
return this._mapCheckoutSession(checkoutSession, input, clientOptions, true);
|
|
105
|
+
}
|
|
106
|
+
async confirmPayment(input, clientOptions) {
|
|
107
|
+
return await this.queryPayment(input, clientOptions);
|
|
108
|
+
}
|
|
109
|
+
async queryPayment(input, clientOptions) {
|
|
110
|
+
const gatewayOptions = this._gatewayOptions(clientOptions);
|
|
111
|
+
if (!input.providerOrderId) this.app.throw(409, 'Stripe payment has no Checkout Session');
|
|
112
|
+
const checkoutSession = await this._gateway(clientOptions).retrieveCheckoutSession(gatewayOptions, {
|
|
113
|
+
id: input.providerOrderId
|
|
114
|
+
});
|
|
115
|
+
return this._mapCheckoutSession(checkoutSession, input, clientOptions);
|
|
116
|
+
}
|
|
117
|
+
async createRefund(input, clientOptions) {
|
|
118
|
+
const gatewayOptions = this._gatewayOptions(clientOptions);
|
|
119
|
+
assertStripeMoney(input.amountMinor, input.currency);
|
|
120
|
+
try {
|
|
121
|
+
const refund = await this._gateway(clientOptions).createRefund(gatewayOptions, {
|
|
122
|
+
idempotencyKey: input.idempotencyKey,
|
|
123
|
+
body: {
|
|
124
|
+
payment_intent: input.providerCaptureId,
|
|
125
|
+
amount: input.amountMinor,
|
|
126
|
+
metadata: this._refundMetadata(input)
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return this._mapRefund(refund, input);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (this._isDefinitiveRefundRejection(error)) return {
|
|
132
|
+
state: 'failed'
|
|
133
|
+
};
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async queryRefund(input, clientOptions) {
|
|
138
|
+
const gatewayOptions = this._gatewayOptions(clientOptions);
|
|
139
|
+
if (!input.providerRefundId) this.app.throw(409, 'Stripe refund has no provider refund identifier');
|
|
140
|
+
const refund = await this._gateway(clientOptions).retrieveRefund(gatewayOptions, {
|
|
141
|
+
id: input.providerRefundId
|
|
142
|
+
});
|
|
143
|
+
return this._mapRefund(refund, input);
|
|
144
|
+
}
|
|
145
|
+
async verifyWebhook(input, clientOptions) {
|
|
146
|
+
const gatewayOptions = this._gatewayOptions(clientOptions);
|
|
147
|
+
let event;
|
|
148
|
+
try {
|
|
149
|
+
event = asRecord(await this._gateway(clientOptions).constructWebhookEvent(gatewayOptions, input));
|
|
150
|
+
} catch (error) {
|
|
151
|
+
this._throwGatewayError(error);
|
|
152
|
+
}
|
|
153
|
+
const eventId = readString(event.id);
|
|
154
|
+
const eventType = readString(event.type);
|
|
155
|
+
const resource = asRecord(asRecord(event.data).object);
|
|
156
|
+
if (!eventId || !eventType || !Object.keys(resource).length) {
|
|
157
|
+
this.app.throw(400, 'Stripe webhook is invalid');
|
|
158
|
+
}
|
|
159
|
+
if (eventType.startsWith('checkout.session.')) {
|
|
160
|
+
return await this._mapCheckoutWebhook(eventId, eventType, resource, clientOptions);
|
|
161
|
+
}
|
|
162
|
+
if (eventType.startsWith('payment_intent.')) {
|
|
163
|
+
return await this._mapPaymentIntentWebhook(eventId, eventType, resource);
|
|
164
|
+
}
|
|
165
|
+
if (eventType.startsWith('refund.')) {
|
|
166
|
+
return await this._mapRefundWebhook(eventId, eventType, resource);
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
eventId,
|
|
170
|
+
eventType,
|
|
171
|
+
ignored: true,
|
|
172
|
+
summary: {
|
|
173
|
+
amountMinor: 0,
|
|
174
|
+
currency: 'USD'
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
async _mapCheckoutWebhook(eventId, eventType, checkoutSession, clientOptions) {
|
|
179
|
+
const input = await this._inputFromCheckoutSession(checkoutSession);
|
|
180
|
+
const payment = await this._mapCheckoutSession(checkoutSession, input, clientOptions);
|
|
181
|
+
if (eventType === 'checkout.session.async_payment_failed') {
|
|
182
|
+
return {
|
|
183
|
+
eventId,
|
|
184
|
+
eventType,
|
|
185
|
+
paymentSessionId: input.paymentSessionId,
|
|
186
|
+
payment: {
|
|
187
|
+
...payment,
|
|
188
|
+
state: 'failed'
|
|
189
|
+
},
|
|
190
|
+
summary: {
|
|
191
|
+
amountMinor: input.amountMinor,
|
|
192
|
+
currency: input.currency
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (!['succeeded', 'failed', 'cancelled'].includes(payment.state)) {
|
|
197
|
+
return {
|
|
198
|
+
eventId,
|
|
199
|
+
eventType,
|
|
200
|
+
ignored: true,
|
|
201
|
+
summary: {
|
|
202
|
+
amountMinor: input.amountMinor,
|
|
203
|
+
currency: input.currency
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
eventId,
|
|
209
|
+
eventType,
|
|
210
|
+
paymentSessionId: input.paymentSessionId,
|
|
211
|
+
payment,
|
|
212
|
+
summary: {
|
|
213
|
+
amountMinor: input.amountMinor,
|
|
214
|
+
currency: input.currency
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
async _mapPaymentIntentWebhook(eventId, eventType, paymentIntent) {
|
|
219
|
+
const metadata = asRecord(paymentIntent.metadata);
|
|
220
|
+
const providerCorrelationReference = readString(metadata.providerCorrelationReference);
|
|
221
|
+
const providerInvoiceReference = readString(metadata.providerInvoiceReference);
|
|
222
|
+
if (!providerCorrelationReference || !providerInvoiceReference) {
|
|
223
|
+
this.app.throw(400, 'Stripe PaymentIntent webhook is not correlated');
|
|
224
|
+
}
|
|
225
|
+
const session = await this.$scope.pay.model.paymentSession.get({
|
|
226
|
+
providerCorrelationReference
|
|
227
|
+
});
|
|
228
|
+
if (!session || session.providerInvoiceReference !== providerInvoiceReference) {
|
|
229
|
+
this.app.throw(400, 'Stripe PaymentIntent webhook is not correlated');
|
|
230
|
+
}
|
|
231
|
+
const payment = this._mapPaymentIntent(paymentIntent, {
|
|
232
|
+
paymentSessionId: session.id,
|
|
233
|
+
businessReference: session.businessReference,
|
|
234
|
+
providerInvoiceReference: session.providerInvoiceReference,
|
|
235
|
+
providerCorrelationReference: session.providerCorrelationReference,
|
|
236
|
+
idempotencyKey: '',
|
|
237
|
+
amountMinor: session.amountMinor,
|
|
238
|
+
currency: session.currency,
|
|
239
|
+
providerOrderId: session.providerOrderId
|
|
240
|
+
});
|
|
241
|
+
const terminalPayment = eventType === 'payment_intent.payment_failed' ? {
|
|
242
|
+
...payment,
|
|
243
|
+
state: 'failed'
|
|
244
|
+
} : payment;
|
|
245
|
+
if (!['succeeded', 'failed', 'cancelled'].includes(terminalPayment.state)) {
|
|
246
|
+
return {
|
|
247
|
+
eventId,
|
|
248
|
+
eventType,
|
|
249
|
+
ignored: true,
|
|
250
|
+
summary: {
|
|
251
|
+
amountMinor: session.amountMinor,
|
|
252
|
+
currency: session.currency
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
eventId,
|
|
258
|
+
eventType,
|
|
259
|
+
paymentSessionId: session.id,
|
|
260
|
+
payment: terminalPayment,
|
|
261
|
+
summary: {
|
|
262
|
+
amountMinor: session.amountMinor,
|
|
263
|
+
currency: session.currency
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async _mapRefundWebhook(eventId, eventType, refundResource) {
|
|
268
|
+
const refundId = readString(refundResource.id);
|
|
269
|
+
const metadata = asRecord(refundResource.metadata);
|
|
270
|
+
const providerCorrelationReference = readString(metadata.providerCorrelationReference);
|
|
271
|
+
if (!refundId || !providerCorrelationReference) {
|
|
272
|
+
this.app.throw(400, 'Stripe refund webhook is not correlated');
|
|
273
|
+
}
|
|
274
|
+
const refund = await this.$scope.pay.model.refundOperation.get({
|
|
275
|
+
providerCorrelationReference
|
|
276
|
+
});
|
|
277
|
+
if (!refund) this.app.throw(400, 'Stripe refund webhook is not correlated');
|
|
278
|
+
const session = await this.$scope.pay.model.paymentSession.getById(refund.paymentSessionId);
|
|
279
|
+
if (!session?.providerCaptureId) this.app.throw(409, 'Stripe refund webhook payment conflicts');
|
|
280
|
+
const snapshot = this._mapRefund(refundResource, {
|
|
281
|
+
paymentSessionId: session.id,
|
|
282
|
+
refundOperationId: refund.id,
|
|
283
|
+
businessReference: refund.businessReference,
|
|
284
|
+
providerInvoiceReference: refund.providerInvoiceReference,
|
|
285
|
+
providerCorrelationReference: refund.providerCorrelationReference,
|
|
286
|
+
idempotencyKey: '',
|
|
287
|
+
amountMinor: refund.amountMinor,
|
|
288
|
+
currency: refund.currency,
|
|
289
|
+
providerCaptureId: session.providerCaptureId,
|
|
290
|
+
providerRefundId: refundId
|
|
291
|
+
});
|
|
292
|
+
return {
|
|
293
|
+
eventId,
|
|
294
|
+
eventType,
|
|
295
|
+
refundOperationId: refund.id,
|
|
296
|
+
refund: snapshot,
|
|
297
|
+
summary: {
|
|
298
|
+
amountMinor: refund.amountMinor,
|
|
299
|
+
currency: refund.currency
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async _inputFromCheckoutSession(checkoutSession) {
|
|
304
|
+
const metadata = asRecord(checkoutSession.metadata);
|
|
305
|
+
const providerCorrelationReference = readString(metadata.providerCorrelationReference);
|
|
306
|
+
const providerInvoiceReference = readString(metadata.providerInvoiceReference);
|
|
307
|
+
const providerOrderId = readString(checkoutSession.id);
|
|
308
|
+
if (!providerCorrelationReference || !providerInvoiceReference || !providerOrderId) {
|
|
309
|
+
this.app.throw(400, 'Stripe Checkout Session is not correlated');
|
|
310
|
+
}
|
|
311
|
+
const session = await this.$scope.pay.model.paymentSession.get({
|
|
312
|
+
providerCorrelationReference
|
|
313
|
+
});
|
|
314
|
+
if (!session || session.providerInvoiceReference !== providerInvoiceReference) {
|
|
315
|
+
this.app.throw(400, 'Stripe Checkout Session is not correlated');
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
paymentSessionId: session.id,
|
|
319
|
+
businessReference: session.businessReference,
|
|
320
|
+
providerInvoiceReference: session.providerInvoiceReference,
|
|
321
|
+
providerCorrelationReference: session.providerCorrelationReference,
|
|
322
|
+
idempotencyKey: '',
|
|
323
|
+
amountMinor: session.amountMinor,
|
|
324
|
+
currency: session.currency,
|
|
325
|
+
providerOrderId
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
_mapCheckoutSession(checkoutSession, input, clientOptions, requireRedirect = false) {
|
|
329
|
+
const record = asRecord(checkoutSession);
|
|
330
|
+
const providerOrderId = readString(record.id);
|
|
331
|
+
const amountMinor = readInteger(record.amount_total);
|
|
332
|
+
const currency = readString(record.currency)?.toUpperCase();
|
|
333
|
+
const metadata = asRecord(record.metadata);
|
|
334
|
+
if (!providerOrderId || input.providerOrderId && input.providerOrderId !== providerOrderId || amountMinor !== input.amountMinor || currency !== input.currency || readString(record.client_reference_id) !== input.providerCorrelationReference || readString(metadata.providerCorrelationReference) !== input.providerCorrelationReference || readString(metadata.providerInvoiceReference) !== input.providerInvoiceReference || !this._matchesEnvironment(record, clientOptions)) {
|
|
335
|
+
this.app.throw(409, 'Stripe Checkout Session facts conflict with the payment session');
|
|
336
|
+
}
|
|
337
|
+
const paymentIntent = asRecord(record.payment_intent);
|
|
338
|
+
const paymentIntentId = readString(record.payment_intent) ?? readString(paymentIntent.id) ?? undefined;
|
|
339
|
+
if (paymentIntentId && Object.keys(paymentIntent).length) {
|
|
340
|
+
return this._mapPaymentIntent(paymentIntent, {
|
|
341
|
+
...input,
|
|
342
|
+
providerOrderId
|
|
343
|
+
}, providerOrderId);
|
|
344
|
+
}
|
|
345
|
+
if (record.status === 'expired') {
|
|
346
|
+
return {
|
|
347
|
+
state: 'cancelled',
|
|
348
|
+
providerPaymentId: providerOrderId,
|
|
349
|
+
providerOrderId
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (record.payment_status === 'paid' && paymentIntentId) {
|
|
353
|
+
return {
|
|
354
|
+
state: 'succeeded',
|
|
355
|
+
providerPaymentId: paymentIntentId,
|
|
356
|
+
providerOrderId,
|
|
357
|
+
providerCaptureId: paymentIntentId,
|
|
358
|
+
nextAction: {
|
|
359
|
+
kind: 'completed'
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const url = readString(record.url);
|
|
364
|
+
if (requireRedirect && !url) this.app.throw(502, 'Stripe did not return a Checkout URL');
|
|
365
|
+
return {
|
|
366
|
+
state: 'requires_action',
|
|
367
|
+
providerPaymentId: providerOrderId,
|
|
368
|
+
providerOrderId,
|
|
369
|
+
...(url && {
|
|
370
|
+
nextAction: {
|
|
371
|
+
kind: 'redirect',
|
|
372
|
+
url
|
|
373
|
+
}
|
|
374
|
+
})
|
|
375
|
+
};
|
|
10
376
|
}
|
|
11
|
-
|
|
12
|
-
|
|
377
|
+
_mapPaymentIntent(paymentIntent, input, providerOrderId = input.providerOrderId) {
|
|
378
|
+
const providerPaymentId = readString(paymentIntent.id);
|
|
379
|
+
const amountMinor = readInteger(paymentIntent.amount);
|
|
380
|
+
const currency = readString(paymentIntent.currency)?.toUpperCase();
|
|
381
|
+
const metadata = asRecord(paymentIntent.metadata);
|
|
382
|
+
if (!providerPaymentId || amountMinor !== input.amountMinor || currency !== input.currency || readString(metadata.providerCorrelationReference) !== input.providerCorrelationReference || readString(metadata.providerInvoiceReference) !== input.providerInvoiceReference) {
|
|
383
|
+
this.app.throw(409, 'Stripe PaymentIntent facts conflict with the payment session');
|
|
384
|
+
}
|
|
385
|
+
const status = readString(paymentIntent.status);
|
|
386
|
+
if (status === 'succeeded') {
|
|
387
|
+
return {
|
|
388
|
+
state: 'succeeded',
|
|
389
|
+
providerPaymentId,
|
|
390
|
+
providerOrderId,
|
|
391
|
+
providerCaptureId: providerPaymentId,
|
|
392
|
+
nextAction: {
|
|
393
|
+
kind: 'completed'
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
if (status === 'processing') return {
|
|
398
|
+
state: 'processing',
|
|
399
|
+
providerPaymentId,
|
|
400
|
+
providerOrderId
|
|
401
|
+
};
|
|
402
|
+
if (status === 'canceled') return {
|
|
403
|
+
state: 'cancelled',
|
|
404
|
+
providerPaymentId,
|
|
405
|
+
providerOrderId
|
|
406
|
+
};
|
|
407
|
+
if (status === 'requires_payment_method') return {
|
|
408
|
+
state: 'failed',
|
|
409
|
+
providerPaymentId,
|
|
410
|
+
providerOrderId
|
|
411
|
+
};
|
|
412
|
+
return {
|
|
413
|
+
state: 'requires_action',
|
|
414
|
+
providerPaymentId,
|
|
415
|
+
providerOrderId
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
_mapRefund(refund, input) {
|
|
419
|
+
const record = asRecord(refund);
|
|
420
|
+
const providerRefundId = readString(record.id);
|
|
421
|
+
const amountMinor = readInteger(record.amount);
|
|
422
|
+
const currency = readString(record.currency)?.toUpperCase();
|
|
423
|
+
const metadata = asRecord(record.metadata);
|
|
424
|
+
const paymentIntent = readString(record.payment_intent);
|
|
425
|
+
if (!providerRefundId || input.providerRefundId && input.providerRefundId !== providerRefundId || amountMinor !== input.amountMinor || currency !== input.currency || paymentIntent !== input.providerCaptureId || readString(metadata.providerCorrelationReference) !== input.providerCorrelationReference || readString(metadata.providerInvoiceReference) !== input.providerInvoiceReference) {
|
|
426
|
+
this.app.throw(409, 'Stripe refund facts conflict with the refund operation');
|
|
427
|
+
}
|
|
428
|
+
const status = readString(record.status);
|
|
429
|
+
if (status === 'succeeded') return {
|
|
430
|
+
state: 'succeeded',
|
|
431
|
+
providerRefundId
|
|
432
|
+
};
|
|
433
|
+
if (status === 'failed') return {
|
|
434
|
+
state: 'failed',
|
|
435
|
+
providerRefundId
|
|
436
|
+
};
|
|
437
|
+
if (status === 'canceled') return {
|
|
438
|
+
state: 'cancelled',
|
|
439
|
+
providerRefundId
|
|
440
|
+
};
|
|
441
|
+
return {
|
|
442
|
+
state: 'pending',
|
|
443
|
+
providerRefundId
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
_paymentMetadata(input) {
|
|
447
|
+
return {
|
|
448
|
+
paymentSessionId: String(input.paymentSessionId),
|
|
449
|
+
providerCorrelationReference: input.providerCorrelationReference,
|
|
450
|
+
providerInvoiceReference: input.providerInvoiceReference
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
_refundMetadata(input) {
|
|
454
|
+
return {
|
|
455
|
+
paymentSessionId: String(input.paymentSessionId),
|
|
456
|
+
refundOperationId: String(input.refundOperationId),
|
|
457
|
+
providerCorrelationReference: input.providerCorrelationReference,
|
|
458
|
+
providerInvoiceReference: input.providerInvoiceReference
|
|
459
|
+
};
|
|
13
460
|
}
|
|
14
|
-
|
|
15
|
-
|
|
461
|
+
_gateway(clientOptions) {
|
|
462
|
+
return clientOptions.gateway ?? createStripeGateway();
|
|
16
463
|
}
|
|
17
|
-
|
|
18
|
-
|
|
464
|
+
_gatewayOptions(clientOptions) {
|
|
465
|
+
const secretKey = clientOptions.secretCredential;
|
|
466
|
+
const webhookSecret = clientOptions.secretWebhook;
|
|
467
|
+
if (!secretKey || !webhookSecret) this.app.throw(503, 'Stripe is not fully configured');
|
|
468
|
+
return {
|
|
469
|
+
secretKey,
|
|
470
|
+
webhookSecret
|
|
471
|
+
};
|
|
19
472
|
}
|
|
20
|
-
|
|
21
|
-
|
|
473
|
+
_matchesEnvironment(value, clientOptions) {
|
|
474
|
+
const livemode = value.livemode;
|
|
475
|
+
return typeof livemode !== 'boolean' || livemode === (clientOptions.environment === 'live');
|
|
476
|
+
}
|
|
477
|
+
_isDefinitiveRefundRejection(error) {
|
|
478
|
+
return isStripeDefinitiveRejection(error);
|
|
479
|
+
}
|
|
480
|
+
_throwTerminalPaymentFailure(error) {
|
|
481
|
+
if (isStripeDefinitiveRejection(error)) {
|
|
482
|
+
throw new ProviderOperationFailure('stripe_request_rejected', 'Stripe rejected the payment request');
|
|
483
|
+
}
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
_throwGatewayError(error) {
|
|
487
|
+
if (error instanceof StripeGatewayError) this.app.throw(error.status, error.message);
|
|
488
|
+
throw error;
|
|
22
489
|
}
|
|
23
490
|
};
|
|
24
491
|
PayProviderStripe = __decorate([PayProvider({
|
|
25
492
|
base: {
|
|
26
493
|
capabilities: {
|
|
27
|
-
redirectCheckout:
|
|
28
|
-
embeddedCheckout:
|
|
494
|
+
redirectCheckout: true,
|
|
495
|
+
embeddedCheckout: false,
|
|
29
496
|
automaticCapture: true,
|
|
30
497
|
manualCapture: false,
|
|
31
498
|
refunds: true,
|
|
@@ -35,14 +502,34 @@ PayProviderStripe = __decorate([PayProvider({
|
|
|
35
502
|
},
|
|
36
503
|
clients: {
|
|
37
504
|
default: {
|
|
38
|
-
environment:
|
|
505
|
+
environment: app.meta.env.STRIPE_ENVIRONMENT,
|
|
39
506
|
secretCredential: app.meta.env.STRIPE_SECRET_KEY,
|
|
40
|
-
secretWebhook: app.meta.env.STRIPE_WEBHOOK_SECRET
|
|
507
|
+
secretWebhook: app.meta.env.STRIPE_WEBHOOK_SECRET,
|
|
508
|
+
merchantReference: app.meta.env.STRIPE_MERCHANT_REFERENCE
|
|
41
509
|
}
|
|
42
510
|
}
|
|
43
511
|
}), BeanInfo({
|
|
44
512
|
module: "pay-stripe"
|
|
45
513
|
})], PayProviderStripe);
|
|
514
|
+
function isStripeDefinitiveRejection(error) {
|
|
515
|
+
if (error instanceof StripeGatewayError) return error.status >= 400 && error.status < 500;
|
|
516
|
+
const type = error?.type;
|
|
517
|
+
return ['StripeInvalidRequestError', 'StripeIdempotencyError', 'StripeAuthenticationError', 'StripePermissionError'].includes(type);
|
|
518
|
+
}
|
|
519
|
+
function assertStripeMoney(amountMinor, currency) {
|
|
520
|
+
if (!Number.isSafeInteger(amountMinor) || amountMinor <= 0 || currency !== 'USD') {
|
|
521
|
+
throw new Error('Stripe supports only positive USD minor-unit amounts');
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function asRecord(value) {
|
|
525
|
+
return value && typeof value === 'object' ? value : {};
|
|
526
|
+
}
|
|
527
|
+
function readString(value) {
|
|
528
|
+
return typeof value === 'string' && value ? value : undefined;
|
|
529
|
+
}
|
|
530
|
+
function readInteger(value) {
|
|
531
|
+
return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
|
|
532
|
+
}
|
|
46
533
|
|
|
47
534
|
let ScopeModulePayStripe = class ScopeModulePayStripe extends BeanScopeBase {};
|
|
48
535
|
ScopeModulePayStripe = __decorate([Scope(), BeanInfo({
|