Test the checkout code; publish images only for releases (#5)
build / test (push) Successful in 7s
build / build (push) Skipped

- src/checkout/ (PayPal order request, pricing, VAT) and src/logging/format.ts
  hold the checkout's pure code, moved out of PayPalApi, the product service
  and config without a behaviour change.
- npm test runs node:test on the TypeScript sources (type stripping), with no
  dependencies: 21 tests, 13 pin today's output, 8 describe the correct
  behaviour for the known defects of libretech/mp#67
  and are marked todo.
- CI: the tests run in the image's base (node:22.14.0-alpine) for every pull
  request, push to main and release tag. The image is built for pull requests
  and built and published only for v* tags; main no longer publishes :main
  and :sha-* images.

Refs libretech/mp#71

Co-authored-by: Michael Czechowski <mail@librete.ch>
This commit was merged in pull request #5.
This commit is contained in:
libretech authored and Michael Czechowski committed 2026-10-08 22:36:47 +02:00
1 parent 901a3b385f
commit 9b0c6af8b7
15 files changed
+542 -96

No files matched your search

+6 -79
View File
@@ -1,16 +1,7 @@
import {
Client,
Environment,
LogLevel,
OrdersController,
CheckoutPaymentIntent,
OrderRequest,
OrderApplicationContextShippingPreference,
Item
} from "@paypal/paypal-server-sdk";
import { paypalClientId, paypalClientSecret, paypalEnvironment, vatIncludedDecimal, vatDecimalExcluded } from "../../config/constants";
import { Client, Environment, LogLevel, OrdersController, OrderRequest } from "@paypal/paypal-server-sdk";
import { paypalClientId, paypalClientSecret, paypalEnvironment } from "../../config/constants";
import { Order } from "../../types";
import { calculateTotalProductPrice } from "../api/product/services/product";
import { buildPayPalOrderItems, buildPayPalOrderRequest } from "../checkout/paypal-order";
class PayPalApi {
private readonly client: Client;
@@ -34,76 +25,12 @@ class PayPalApi {
}
async createSessionOrThrow(returnUrl: string, order: Order) {
if (!order.total) throw new Error("Cart is empty or has no total");
const items = 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()
} as Item;
});
// Outside the try block, so these throws are not logged here. The items are computed but not sent.
buildPayPalOrderItems(order);
try {
const collect = {
body: {
intent: CheckoutPaymentIntent.Capture,
purchaseUnits: [
{
// items,
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: OrderApplicationContextShippingPreference.SetProvidedAddress
}
} as OrderRequest,
body: buildPayPalOrderRequest(returnUrl, order) as OrderRequest,
prefer: "return=minimal"
};