@paykit-sdk/paypal 1.0.1 → 1.0.3

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.
@@ -1,504 +0,0 @@
1
- import {
2
- PayKitProvider,
3
- Checkout,
4
- CreateCheckoutSchema,
5
- CreateCustomerParams,
6
- Customer,
7
- UpdateCustomerParams,
8
- Subscription,
9
- UpdateSubscriptionSchema,
10
- paykitEvent$InboundSchema,
11
- WebhookEventPayload,
12
- PaykitProviderOptions,
13
- HandleWebhookParams,
14
- UpdateCheckoutSchema,
15
- CreateSubscriptionSchema,
16
- CreatePaymentSchema,
17
- Payment,
18
- UpdatePaymentSchema,
19
- CreateRefundSchema,
20
- Refund,
21
- validateRequiredKeys,
22
- ProviderNotSupportedError,
23
- ConstraintViolationError,
24
- ResourceNotFoundError,
25
- WebhookError,
26
- NotImplementedError,
27
- schema,
28
- AbstractPayKitProvider,
29
- } from '@paykit-sdk/core';
30
- import {
31
- CheckoutPaymentIntent,
32
- Client,
33
- Environment,
34
- LogLevel,
35
- Order,
36
- OrdersController,
37
- PaymentsController,
38
- Refund as PayPalRefund,
39
- OrderApplicationContextUserAction,
40
- } from '@paypal/paypal-server-sdk';
41
- import { z } from 'zod';
42
- import { SubscriptionsController } from './controllers/subscription';
43
- import { WebhookController } from './controllers/webhook';
44
- import { VerifyWebhookStatus } from './schema';
45
- import {
46
- paykitCheckout$InboundSchema,
47
- paykitPayment$InboundSchema,
48
- paykitRefund$InboundSchema,
49
- } from './utils/mapper';
50
-
51
- const PAYPAL_METADATA_MAX_LENGTH = 127;
52
-
53
- export interface PayPalOptions extends PaykitProviderOptions {
54
- /**
55
- * The client ID for the PayPal API
56
- */
57
- clientId: string;
58
-
59
- /**
60
- * The client secret for the PayPal API
61
- */
62
- clientSecret: string;
63
-
64
- /**
65
- * Whether to use the sandbox environment
66
- */
67
- isSandbox: boolean;
68
- }
69
-
70
- const paypalOptionsSchema = schema<PayPalOptions>()(
71
- z.object({
72
- clientId: z.string(),
73
- clientSecret: z.string(),
74
- isSandbox: z.boolean(),
75
- debug: z.boolean().optional(),
76
- }),
77
- );
78
-
79
- const providerName = 'paypal';
80
- export class PayPalProvider extends AbstractPayKitProvider implements PayKitProvider {
81
- readonly providerName = providerName;
82
-
83
- private client: Client;
84
- private ordersController: OrdersController;
85
- private paymentsController: PaymentsController;
86
- private subscriptionsController: SubscriptionsController;
87
- private webhookController: WebhookController;
88
-
89
- constructor(config: PayPalOptions) {
90
- super(paypalOptionsSchema, config, providerName);
91
-
92
- const { clientId, clientSecret, isSandbox = true, debug } = config;
93
-
94
- const environment = isSandbox ? Environment.Sandbox : Environment.Production;
95
-
96
- this.client = new Client({
97
- clientCredentialsAuthCredentials: {
98
- oAuthClientId: clientId,
99
- oAuthClientSecret: clientSecret,
100
- },
101
- timeout: 0,
102
- environment,
103
- logging: {
104
- logLevel: debug ? LogLevel.Info : LogLevel.Error,
105
- logRequest: { logBody: debug },
106
- logResponse: { logHeaders: debug },
107
- },
108
- });
109
-
110
- this.ordersController = new OrdersController(this.client);
111
- this.paymentsController = new PaymentsController(this.client);
112
- this.subscriptionsController = new SubscriptionsController(this.client);
113
- this.webhookController = new WebhookController(this.client);
114
- }
115
-
116
- /**
117
- * Checkout management
118
- * In PayPal, Order IS the checkout
119
- */
120
- createCheckout = async (params: CreateCheckoutSchema): Promise<Checkout> => {
121
- const stringifiedMetadata = JSON.stringify(params.metadata);
122
-
123
- if (stringifiedMetadata.length > PAYPAL_METADATA_MAX_LENGTH) {
124
- throw new ConstraintViolationError('Metadata exceeds maximum length', {
125
- value: stringifiedMetadata.length,
126
- limit: PAYPAL_METADATA_MAX_LENGTH,
127
- provider: this.providerName,
128
- });
129
- }
130
- const {
131
- currency = 'USD',
132
- amount = '0',
133
- itemName = 'Untitled Item',
134
- } = validateRequiredKeys(
135
- ['currency', 'amount', 'itemName'],
136
- params.provider_metadata as Record<string, string>,
137
- 'Missing required parameters: {keys}',
138
- );
139
-
140
- const orderOptionsBody: Parameters<OrdersController['createOrder']>[0]['body'] = {
141
- intent: CheckoutPaymentIntent.Capture,
142
- payer: {
143
- ...(typeof params.customer === 'string' && { payerId: params.customer }),
144
- ...(typeof params.customer === 'object' &&
145
- 'email' in params.customer && { emailAddress: params.customer.email }),
146
- },
147
- purchaseUnits: [
148
- {
149
- amount: { currencyCode: currency, value: amount },
150
- customId: stringifiedMetadata,
151
- items: [
152
- {
153
- sku: params.item_id,
154
- quantity: params.quantity.toString(),
155
- name: itemName,
156
- unitAmount: { currencyCode: currency, value: amount },
157
- },
158
- ],
159
- },
160
- ],
161
- applicationContext: { userAction: OrderApplicationContextUserAction.PayNow },
162
- ...(params.provider_metadata && { ...params.provider_metadata }),
163
- };
164
-
165
- if (params.billing) {
166
- orderOptionsBody.purchaseUnits[0].shipping = {
167
- name: { fullName: params.billing.address.name },
168
- address: {
169
- addressLine1: params.billing.address.line1,
170
- addressLine2: params.billing.address.line2,
171
- adminArea1: params.billing.address.city,
172
- adminArea2: params.billing.address.state,
173
- postalCode: params.billing.address.postal_code,
174
- countryCode: params.billing.address.country,
175
- },
176
- ...(params.billing.address.phone && {
177
- phoneNumber: {
178
- nationalNumber: params.billing.address.phone,
179
- countryCode: params.billing.address.country,
180
- },
181
- }),
182
- };
183
- }
184
-
185
- const order = await this.ordersController.createOrder({ body: orderOptionsBody });
186
-
187
- return paykitCheckout$InboundSchema(order.result);
188
- };
189
-
190
- retrieveCheckout = async (id: string): Promise<Checkout> => {
191
- const order = await this.ordersController.getOrder({ id });
192
-
193
- return paykitCheckout$InboundSchema(order.result);
194
- };
195
-
196
- updateCheckout = async (
197
- id: string,
198
- params: UpdateCheckoutSchema,
199
- ): Promise<Checkout> => {
200
- throw new ProviderNotSupportedError('updateCheckout', this.providerName, {
201
- reason:
202
- 'PayPal does not support updating orders. Cancel and create a new order instead.',
203
- });
204
- };
205
-
206
- deleteCheckout = async (id: string): Promise<null> => {
207
- throw new ProviderNotSupportedError('deleteCheckout', this.providerName, {
208
- reason: 'PayPal orders cannot be deleted. They expire automatically.',
209
- });
210
- };
211
-
212
- createCustomer = async (params: CreateCustomerParams): Promise<Customer> => {
213
- throw new ProviderNotSupportedError('customer management', this.providerName, {
214
- reason: 'PayPal does not have standalone customer entities.',
215
- alternative: 'Use Payer information within orders or implement PayPal Vault API',
216
- });
217
- };
218
-
219
- updateCustomer = async (
220
- id: string,
221
- params: UpdateCustomerParams,
222
- ): Promise<Customer> => {
223
- throw new ProviderNotSupportedError('updateCustomer', this.providerName, {
224
- reason: 'PayPal does not support standalone customer management.',
225
- });
226
- };
227
-
228
- deleteCustomer = async (id: string): Promise<null> => {
229
- throw new ProviderNotSupportedError('deleteCustomer', this.providerName, {
230
- reason: 'PayPal does not support standalone customer management.',
231
- });
232
- };
233
-
234
- retrieveCustomer = async (id: string): Promise<Customer | null> => {
235
- throw new ProviderNotSupportedError('retrieveCustomer', this.providerName, {
236
- reason: 'PayPal does not support standalone customer management.',
237
- });
238
- };
239
-
240
- /**
241
- * Subscription management
242
- * Would need PayPal Subscriptions API - different from Orders
243
- */
244
- createSubscription = async (
245
- params: CreateSubscriptionSchema,
246
- ): Promise<Subscription> => {
247
- const subscription = await this.subscriptionsController.createSubscription({
248
- body: params,
249
- });
250
- return subscription as unknown as Subscription;
251
- };
252
-
253
- cancelSubscription = async (id: string): Promise<Subscription> => {
254
- const subscription = await this.subscriptionsController.cancelSubscription({
255
- subscriptionId: id,
256
- reason: 'Customer requested cancellation',
257
- });
258
-
259
- return subscription as unknown as Subscription;
260
- };
261
-
262
- updateSubscription = async (
263
- id: string,
264
- params: UpdateSubscriptionSchema,
265
- ): Promise<Subscription> => {
266
- const stringifiedMetadata = JSON.stringify(params.metadata);
267
-
268
- if (stringifiedMetadata.length > PAYPAL_METADATA_MAX_LENGTH) {
269
- throw new ConstraintViolationError('Metadata exceeds maximum length', {
270
- value: stringifiedMetadata.length,
271
- limit: PAYPAL_METADATA_MAX_LENGTH,
272
- provider: this.providerName,
273
- });
274
- }
275
- const subscription = await this.subscriptionsController.updateSubscription({
276
- subscriptionId: id,
277
- metadata: params.metadata ?? {},
278
- });
279
-
280
- return subscription as unknown as Subscription;
281
- };
282
-
283
- retrieveSubscription = async (id: string): Promise<Subscription> => {
284
- const subscription = await this.subscriptionsController.retrieveSubscription({
285
- subscriptionId: id,
286
- });
287
-
288
- return subscription as unknown as Subscription;
289
- };
290
-
291
- deleteSubscription = async (id: string): Promise<null> => {
292
- throw new NotImplementedError('deleteSubscription', 'PayPal', {
293
- futureSupport: false,
294
- });
295
- };
296
-
297
- /**
298
- * Payment management
299
- * In PayPal, Order IS the payment
300
- */
301
- createPayment = async (params: CreatePaymentSchema): Promise<Payment> => {
302
- const stringifiedMetadata = JSON.stringify(params.metadata);
303
-
304
- if (stringifiedMetadata.length > PAYPAL_METADATA_MAX_LENGTH) {
305
- throw new ConstraintViolationError('Metadata exceeds maximum length', {
306
- value: stringifiedMetadata.length,
307
- limit: PAYPAL_METADATA_MAX_LENGTH,
308
- provider: this.providerName,
309
- });
310
- }
311
-
312
- const orderOptionsBody: Parameters<OrdersController['createOrder']>[0]['body'] = {
313
- intent: CheckoutPaymentIntent.Capture,
314
- payer: {
315
- ...(typeof params.customer === 'string' && { payerId: params.customer }),
316
- ...(typeof params.customer === 'object' &&
317
- 'email' in params.customer && { emailAddress: params.customer.email }),
318
- },
319
- purchaseUnits: [
320
- {
321
- amount: { currencyCode: params.currency, value: params.amount.toString() },
322
- customId: stringifiedMetadata,
323
- },
324
- ],
325
- };
326
-
327
- if (params.billing) {
328
- orderOptionsBody.purchaseUnits[0].shipping = {
329
- name: { fullName: params.billing.address.name },
330
- address: {
331
- addressLine1: params.billing.address.line1,
332
- addressLine2: params.billing.address.line2,
333
- adminArea1: params.billing.address.city,
334
- adminArea2: params.billing.address.state,
335
- postalCode: params.billing.address.postal_code,
336
- countryCode: params.billing.address.country,
337
- },
338
- ...(params.billing.address.phone && {
339
- phoneNumber: {
340
- nationalNumber: params.billing.address.phone,
341
- countryCode: params.billing.address.country,
342
- },
343
- }),
344
- };
345
- }
346
-
347
- const order = await this.ordersController.createOrder({ body: orderOptionsBody });
348
-
349
- return paykitPayment$InboundSchema(order.result);
350
- };
351
-
352
- updatePayment = async (id: string, params: UpdatePaymentSchema): Promise<Payment> => {
353
- throw new ProviderNotSupportedError('updatePayment', this.providerName, {
354
- reason: 'PayPal does not support updating orders.',
355
- });
356
- };
357
-
358
- retrievePayment = async (id: string): Promise<Payment | null> => {
359
- const order = await this.ordersController.getOrder({ id });
360
-
361
- return paykitPayment$InboundSchema(order.result);
362
- };
363
-
364
- deletePayment = async (id: string): Promise<null> => {
365
- throw new ProviderNotSupportedError('deletePayment', this.providerName, {
366
- reason: 'PayPal orders cannot be deleted. They expire automatically.',
367
- });
368
- };
369
-
370
- capturePayment = async (id: string): Promise<Payment> => {
371
- const captured = await this.ordersController.captureOrder({ id });
372
- return paykitPayment$InboundSchema(captured.result);
373
- };
374
-
375
- cancelPayment = async (id: string): Promise<Payment> => {
376
- // PayPal doesn't have explicit cancel, but you can void authorizations
377
- throw new ProviderNotSupportedError('cancelPayment', this.providerName, {
378
- reason:
379
- 'PayPal order cancellation not directly supported. Orders expire automatically.',
380
- });
381
- };
382
-
383
- /**
384
- * Refund management
385
- */
386
- createRefund = async (params: CreateRefundSchema): Promise<Refund> => {
387
- const order = await this.ordersController.getOrder({ id: params.payment_id });
388
-
389
- const captureIds =
390
- order.result.purchaseUnits?.[0]?.payments?.captures?.map(c => c.id!) || [];
391
-
392
- if (captureIds.length === 0) {
393
- throw new ResourceNotFoundError('Capture', params.payment_id, this.providerName);
394
- }
395
-
396
- const currencyCode = order.result.purchaseUnits?.[0]?.amount?.currencyCode || 'USD';
397
- const amount = params.amount
398
- ? params.amount.toString()
399
- : order.result.purchaseUnits?.[0]?.amount?.value || '0';
400
-
401
- const refund = await this.paymentsController.refundCapturedPayment({
402
- captureId: captureIds[0],
403
- body: { amount: { currencyCode, value: amount } },
404
- });
405
-
406
- return paykitRefund$InboundSchema(refund.result);
407
- };
408
-
409
- /**
410
- * Webhook management
411
- */
412
- handleWebhook = async (
413
- params: HandleWebhookParams,
414
- ): Promise<Array<WebhookEventPayload>> => {
415
- const { body, headers, webhookSecret: webhookId } = params;
416
-
417
- const { result } = await this.webhookController.verifyWebhook({
418
- authAlgo: headers.get('paypal-auth-algo') as string,
419
- certUrl: headers.get('paypal-cert-url') as string,
420
- transmissionId: headers.get('paypal-transmission-id') as string,
421
- transmissionSig: headers.get('paypal-transmission-sig') as string,
422
- transmissionTime: headers.get('paypal-transmission-time') as string,
423
- webhookId,
424
- webhookEvent: JSON.parse(body),
425
- });
426
-
427
- if (result.verification_status !== VerifyWebhookStatus.SUCCESS) {
428
- throw new WebhookError('Webhook verification failed', {
429
- provider: this.providerName,
430
- });
431
- }
432
-
433
- const event = JSON.parse(body);
434
- const eventType = event.event_type;
435
-
436
- const webhookHandlers: Record<string, () => Promise<Array<WebhookEventPayload>>> = {
437
- 'CHECKOUT.ORDER.APPROVED': async () => {
438
- const orderData = event.resource as Order;
439
- const payment = paykitPayment$InboundSchema(orderData);
440
-
441
- return [
442
- paykitEvent$InboundSchema<Payment>({
443
- type: 'payment.created',
444
- created: Date.now() / 1000,
445
- id: event.id,
446
- data: payment,
447
- }),
448
- ];
449
- },
450
-
451
- 'CHECKOUT.ORDER.COMPLETED': async () => {
452
- const orderData = event.resource as Order;
453
- const payment = paykitPayment$InboundSchema(orderData);
454
-
455
- return [
456
- paykitEvent$InboundSchema<Payment>({
457
- type: 'payment.updated',
458
- created: Date.now() / 1000,
459
- id: event.id,
460
- data: payment,
461
- }),
462
- ];
463
- },
464
-
465
- 'PAYMENT.CAPTURE.COMPLETED': async () => {
466
- const orderData = event.resource as Order;
467
- const payment = paykitPayment$InboundSchema(orderData);
468
-
469
- return [
470
- paykitEvent$InboundSchema<Payment>({
471
- type: 'payment.updated',
472
- created: Date.now() / 1000,
473
- id: event.id,
474
- data: payment,
475
- }),
476
- ];
477
- },
478
-
479
- 'PAYMENT.CAPTURE.REFUNDED': async () => {
480
- const refundData = event.resource as PayPalRefund;
481
- const refund = paykitRefund$InboundSchema(refundData);
482
-
483
- return [
484
- paykitEvent$InboundSchema<Refund>({
485
- type: 'refund.created',
486
- created: Date.now() / 1000,
487
- id: event.id,
488
- data: refund,
489
- }),
490
- ];
491
- },
492
- };
493
-
494
- const handler = webhookHandlers[eventType];
495
-
496
- if (!handler) {
497
- throw new WebhookError(`Unhandled event type: ${eventType}`, {
498
- provider: this.providerName,
499
- });
500
- }
501
-
502
- return await handler();
503
- };
504
- }