import { Client, Environment, LogLevel, OrdersController, OrderRequest } from "@paypal/paypal-server-sdk"; import { paypalClientId, paypalClientSecret, paypalEnvironment } from "../../config/constants"; import { Order } from "../../types"; import { buildPayPalOrderItems, buildPayPalOrderRequest } from "../checkout/paypal-order"; import { describeError } from "../logging/error"; class PayPalApi { private readonly client: Client; private readonly ordersController: OrdersController; constructor(clientId: string, clientSecret: string, environment: Environment) { this.client = new Client({ clientCredentialsAuthCredentials: { oAuthClientId: clientId, oAuthClientSecret: clientSecret }, timeout: 0, environment, logging: { logLevel: LogLevel.Info, logRequest: { logBody: true }, logResponse: { logHeaders: true } } }); this.ordersController = new OrdersController(this.client); } /** Creates the PayPal order for the order and returns PayPal's id for it, which the capture accepts, and its links. */ async createSessionOrThrow(returnUrl: string, order: Order): Promise<{ id: string; links: unknown }> { // Outside the try block, so these throws are not logged here. The items are computed but not sent. buildPayPalOrderItems(order); try { const collect = { body: buildPayPalOrderRequest(returnUrl, order) as OrderRequest, prefer: "return=minimal" }; strapi.log.silly("app:d:paypal-api: Creating PayPal order " + JSON.stringify({ collect })); const { body, ...httpResponse } = await this.ordersController.ordersCreate(collect); const sessionData = JSON.parse(body as string); strapi.log.info("app:i:paypal-api: " + JSON.stringify({ session: sessionData })); if (typeof sessionData?.id !== "string" || sessionData.id === "") throw new Error("PayPal answered without an order id"); return { id: sessionData.id, links: sessionData.links }; } catch (error) { // Never the error object: an SDK or axios error carries the request with its Authorization header. strapi.log.error("app:e:paypal-api: Creating the PayPal order failed", { error: describeError(error) }); throw error; } } /** PayPal's order as parsed JSON in PayPal's snake_case (GET /v2/checkout/orders/:id). Throws PayPal's and network errors. */ async getOrder(id: string): Promise { const { body } = await this.ordersController.ordersGet({ id }); return JSON.parse(body as string); } /** * Captures the PayPal order and returns the whole order after the capture as parsed JSON in PayPal's snake_case * (POST /v2/checkout/orders/:id/capture, Prefer: return=representation). PayPal answers a repeated requestId with the * response of the first capture instead of capturing again. Throws PayPal's and network errors. */ async captureOrder(id: string, requestId: string): Promise { const { body } = await this.ordersController.ordersCapture({ id, paypalRequestId: requestId, prefer: "return=representation" }); return JSON.parse(body as string); } async handleWebhook(req: any) { // Implement PayPal webhook validation and processing strapi.log.verbose("app:i:paypal-api: " + JSON.stringify({ req })); // Note: PayPal webhook handling requires specific implementation // This is a placeholder and should be replaced with actual PayPal webhook validation return { message: "Webhook received" }; } getClientKey() { // PayPal doesn't use a client key like Adyen, so we'll return the client ID return paypalClientId; } } export const paypalApi = new PayPalApi(paypalClientId, paypalClientSecret, paypalEnvironment); export type PayPalApiOptions = { clientId: string; clientSecret: string; environment: Environment; };