Test the checkout code; publish images only for releases #5

Merged
libretech merged 4 commits from refactor/checkout-tests into main 2026-10-08 22:36:48 +02:00
6 changed files with 144 additions and 87 deletions
Showing only changes of commit 9873c184d6 - Show all commits
+2 -4
View File
@@ -1,4 +1,5 @@
import { Environment } from "@paypal/paypal-server-sdk";
import { vatDecimal } from "../src/checkout/vat";
export const pdfApiUrl = process.env.PDF_API_ADDRESS!;
export const mailApiUrl = process.env.MAIL_API_ADDRESS!;
@@ -18,10 +19,7 @@ export const shopEmail = process.env.SHOP_EMAIL ?? process.env.ADMIN_EMAIL_ADDRE
export const shopLogoUrl = process.env.SHOP_LOGO_URL ?? "";
export const shopSecondaryLogoUrl = process.env.SHOP_SECONDARY_LOGO_URL ?? "";
// TODO: Should be retrieved from DepotApi
export const vatIncludedDecimal = 1.19;
// TODO: Should be retrieved from DepotApi
export const vatDecimal = 0.19;
export { vatIncludedDecimal, vatDecimal } from "../src/checkout/vat";
export const vatDecimalExcluded = 1 - vatDecimal;
export const maxProductsSitemap = 500;
+3 -4
View File
@@ -3,7 +3,7 @@
*/
import { factories } from "@strapi/strapi";
import { Product } from "../../../../types";
import { calculateTotalProductPrice } from "../../../checkout/pricing";
export default factories.createCoreService("api::product.product", ({ strapi }) => ({
find: async (params: Record<string, any>) => {
@@ -110,9 +110,8 @@ export default factories.createCoreService("api::product.product", ({ strapi })
}
}));
export const calculateTotalProductPrice = (product: Partial<Product>): number => {
return (product?.cover?.price ?? 0) + (product?.pages?.price ?? 0) + (product?.ruling?.price ?? 0);
};
// Implemented in src/checkout/pricing.ts; re-exported so existing importers keep working.
export { calculateTotalProductPrice };
export interface ProductParams {
populate?: {
+124
View File
@@ -0,0 +1,124 @@
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"
}
});
+5
View File
@@ -0,0 +1,5 @@
import type { Product } from "../../types";
export const calculateTotalProductPrice = (product: Partial<Product>): number => {
return (product?.cover?.price ?? 0) + (product?.pages?.price ?? 0) + (product?.ruling?.price ?? 0);
};
+4
View File
@@ -0,0 +1,4 @@
// TODO: Should be retrieved from DepotApi
export const vatIncludedDecimal = 1.19;
// TODO: Should be retrieved from DepotApi
export const vatDecimal = 0.19;
+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"
};