import type { Order } from "../../types"; import { calculateTotalProductPrice } from "./pricing"; import { vatIncludedDecimal } from "./vat"; // The PayPal order that PayPalApi.createSessionOrThrow creates, built without the PayPal SDK so it can be unit tested. // String literals stand in for the SDK enums, with the values of @paypal/paypal-server-sdk 0.6.1. export type PayPalMoney = { currencyCode: string; value: string; }; export type PayPalOrderItem = { name: string; unitAmount: PayPalMoney; tax: PayPalMoney; quantity: string; }; export type PayPalOrderRequest = { /** CheckoutPaymentIntent.Capture */ intent: "CAPTURE"; purchaseUnits: { referenceId: string; amount: PayPalMoney & { breakdown: { itemTotal: PayPalMoney; taxTotal: PayPalMoney; shipping: PayPalMoney | undefined; }; }; shipping: { name: { fullName: string }; address: { addressLine1: string; postalCode: string; adminArea2: string; countryCode: string }; }; customId: string; invoiceId: string; }[]; applicationContext: { returnUrl: string; cancelUrl: string; /** OrderApplicationContextShippingPreference.SetProvidedAddress */ shippingPreference: "SET_PROVIDED_ADDRESS"; }; }; /** * The part of PayPalApi.createSessionOrThrow that runs before its try block, so nothing thrown here is logged there. * Rejects an order without a total, then computes one item per cart line. The items are not sent to PayPal, but computing * them throws a TypeError for a cart line without a product. */ export const buildPayPalOrderItems = (order: Order): PayPalOrderItem[] => { if (!order.total) throw new Error("Cart is empty or has no total"); return order.cart.map((item) => { const totalProductPrice = calculateTotalProductPrice(item.product); const amount = totalProductPrice / vatIncludedDecimal; const tax = totalProductPrice - amount; return { name: item.product.name, unitAmount: { currencyCode: "EUR", value: amount.toFixed(2) }, tax: { currencyCode: "EUR", value: tax.toFixed(2) }, quantity: item.count.toString() }; }); }; /** * The order request body PayPalApi.createSessionOrThrow sends. It is built inside that try block, so whatever throws here is logged there. */ export const buildPayPalOrderRequest = (returnUrl: string, order: Order): PayPalOrderRequest => ({ intent: "CAPTURE", purchaseUnits: [ { // items: computed by buildPayPalOrderItems, but not sent referenceId: order.uuid, amount: { currencyCode: "EUR", value: order.total.toFixed(2), breakdown: { itemTotal: { currencyCode: "EUR", value: (Math.round((order.subtotal / vatIncludedDecimal) * 100) / 100).toFixed(2) }, taxTotal: { currencyCode: "EUR", value: order.VAT.toFixed(2) }, shipping: order.delivery ? { currencyCode: "EUR", value: order.delivery.price.toFixed(2) } : undefined } }, shipping: { name: { fullName: order.deliveryAddress.split("\n")[0] }, address: { addressLine1: order.deliveryAddress.split("\n")[1], postalCode: order.deliveryAddress.split("\n")[2].slice(0, 5), adminArea2: order.deliveryAddress.split("\n")[2].slice(6), countryCode: "DE" } }, customId: order.uuid, invoiceId: order.invoiceNumber } ], applicationContext: { returnUrl: returnUrl, cancelUrl: returnUrl, shippingPreference: "SET_PROVIDED_ADDRESS" } });