feat: extract cms from mp/cms — initial libreshop/cms
Build and publish / build (push) Failing after 17s
Build and publish / build (push) Failing after 17s
Source moved verbatim from mp/cms/ on 2026-04-29; mp was the first concrete adapter consuming the libreshop toolkit. Builds and publishes git.librete.ch/libreshop/cms on every main / v* push via the standard .gitea/workflows/build.yml shared across libreshop components.
This commit is contained in:
127 files changed
+44618
No files matched your search
@@ -0,0 +1,307 @@
|
||||
import { factories } from "@strapi/strapi";
|
||||
import { sanitize } from "@strapi/utils";
|
||||
import { Order } from "../../../../types";
|
||||
import { paypalApi } from "../../../services/PayPalApi";
|
||||
import { orderDefaultParams } from "../services/order";
|
||||
import { calculateTotalProductPrice } from "../../product/services/product";
|
||||
|
||||
/**
|
||||
* The order controller is a funnel for the client to interact with the order service.
|
||||
* From creating until finalizing an order, the controller is responsible for handling
|
||||
* the business logic.
|
||||
*
|
||||
* ---------------------------------------
|
||||
* INIT
|
||||
* ---------------------------------------
|
||||
* 1. Create order
|
||||
* ---------------------------------------
|
||||
* PRE-ORDER (must have order uuid)
|
||||
* ---------------------------------------
|
||||
* 1.1. Find order
|
||||
* 2. Add/remove product from order
|
||||
* ---------------------------------------
|
||||
* ORDER VALIDATION (must have at least one product)
|
||||
* ---------------------------------------
|
||||
* 3. Update order
|
||||
* 3.1. Contact info (email)
|
||||
* 3.2. Confirm terms and conditions
|
||||
* 3.3. Address (invoice, delivery)
|
||||
* 3.4. Delivery method
|
||||
* ---------------------------------------
|
||||
* ORDER PAYMENT (order must include all above properties)
|
||||
* ---------------------------------------
|
||||
* 4. Checkout order (create adyen payment session)
|
||||
* ---------------------------------------
|
||||
* POST-ORDER (payment must be authorised)
|
||||
* ---------------------------------------
|
||||
* 5. Finalize order (generate invoice, delivery note)
|
||||
* 6. Send invoice and delivery note via email
|
||||
*/
|
||||
export default factories.createCoreController("api::order.order", ({ strapi }) => ({
|
||||
create: async (_) => {
|
||||
const order = await strapi.entityService.create("api::order.order", { data: {} });
|
||||
strapi.log.debug(`app:d:order-controller: Order created: ${JSON.stringify({ order })}`);
|
||||
return await sanitize.contentAPI.output(order, strapi.getModel("api::order.order"));
|
||||
},
|
||||
|
||||
update: async (ctx) => {
|
||||
const { id } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
|
||||
strapi.log.verbose(`app:v:order-controller: - Updating order ID${id}`, orderDefaultParams);
|
||||
const order = await strapi.db.query("api::order.order").findOne({ where: { id }, ...orderDefaultParams });
|
||||
if (!order) return ctx.notFound("Order not found");
|
||||
|
||||
const sanitizedBody = await sanitize.contentAPI.input(ctx.request.body, strapi.getModel("api::order.order"));
|
||||
strapi.log.verbose("app:v:order-controller: - Updating order", { sanitizedBody });
|
||||
|
||||
const updatedOrder = await strapi.entityService.update("api::order.order", order.id, {
|
||||
// @ts-expect-error
|
||||
data: sanitizedBody?.data,
|
||||
...orderDefaultParams
|
||||
});
|
||||
strapi.log.verbose("app:v:order-controller: ✔ Updating order");
|
||||
return await sanitize.contentAPI.output(updatedOrder, strapi.getModel("api::order.order"));
|
||||
},
|
||||
|
||||
findOne: async (ctx) => {
|
||||
const { id } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
|
||||
const order: Order = await strapi.db.query("api::order.order").findOne({
|
||||
where: { id },
|
||||
...orderDefaultParams
|
||||
});
|
||||
order.cart = order?.cart.map((item) => ({
|
||||
...item,
|
||||
product: {
|
||||
...item.product,
|
||||
totalProductPrice: calculateTotalProductPrice(item.product)
|
||||
}
|
||||
}));
|
||||
if (!order) return ctx.notFound("Order not found");
|
||||
return await sanitize.contentAPI.output(order, strapi.getModel("api::order.order"));
|
||||
},
|
||||
|
||||
findOneByUuid: async (ctx) => {
|
||||
const { uuid } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
|
||||
const order: Order = await strapi.db.query("api::order.order").findOne({
|
||||
where: { uuid },
|
||||
...orderDefaultParams
|
||||
});
|
||||
order.cart = order?.cart.map((item) => ({
|
||||
...item,
|
||||
product: {
|
||||
...item.product,
|
||||
totalProductPrice: calculateTotalProductPrice(item.product)
|
||||
}
|
||||
}));
|
||||
if (!order) return ctx.notFound("Order not found");
|
||||
return await sanitize.contentAPI.output(order, strapi.getModel("api::order.order"));
|
||||
},
|
||||
|
||||
updateByUuid: async (ctx) => {
|
||||
const { uuid } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
|
||||
const order = await strapi.db.query("api::order.order").findOne({ where: { uuid } });
|
||||
if (!order) return ctx.notFound("Order not found");
|
||||
|
||||
const sanitizedBody = await sanitize.contentAPI.input(ctx.request.body, strapi.getModel("api::order.order"));
|
||||
|
||||
// Handle customer creation/update if structured addresses are provided
|
||||
// @ts-expect-error
|
||||
if (sanitizedBody?.data?.invoiceAddressStructured) {
|
||||
const customerAddress =
|
||||
// @ts-expect-error
|
||||
sanitizedBody.data.invoiceAddress ||
|
||||
// @ts-expect-error
|
||||
`${sanitizedBody.data.invoiceAddressStructured.givenName} ${sanitizedBody.data.invoiceAddressStructured.familyName}\n${sanitizedBody.data.invoiceAddressStructured.streetAddress}\n${sanitizedBody.data.invoiceAddressStructured.postalCode} ${sanitizedBody.data.invoiceAddressStructured.addressLevel2}`;
|
||||
// @ts-expect-error
|
||||
sanitizedBody.data.customer = await strapi
|
||||
.service("api::customer.customer")
|
||||
// @ts-expect-error
|
||||
.findOrCreateCustomer(customerAddress, sanitizedBody.data.invoiceAddressStructured);
|
||||
}
|
||||
|
||||
const orderUnsafe = await strapi.service("api::order.order").update(order.id, {
|
||||
// @ts-expect-error
|
||||
data: sanitizedBody?.data,
|
||||
...orderDefaultParams
|
||||
});
|
||||
|
||||
return await sanitize.contentAPI.output(orderUnsafe, strapi.getModel("api::order.order"));
|
||||
},
|
||||
|
||||
addProduct: async (ctx) => {
|
||||
const { uuid, productId } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
|
||||
const order = (await strapi.service("api::order.order").findOneByUuid(uuid)) as Order;
|
||||
if (!order) return ctx.notFound("Order not found");
|
||||
|
||||
const product = parseInt(productId as string);
|
||||
const count = parseInt(ctx.query.count as string) || 1;
|
||||
const cart = order.cart || [];
|
||||
const existingIndex = cart.findIndex((item) => item.product.id === product);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// Product already in cart
|
||||
cart[existingIndex].count += count;
|
||||
} else {
|
||||
// @ts-expect-error
|
||||
cart.push({ count, product });
|
||||
}
|
||||
|
||||
strapi.log.info(`app:v:order-controller: - Adding product to cart ${order.id}`, { totalBefore: order.total });
|
||||
|
||||
const orderUnsafe = await strapi.service("api::order.order").update(order.id, {
|
||||
data: { cart: cart },
|
||||
...orderDefaultParams
|
||||
});
|
||||
strapi.log.info("app:v:order-controller: ✔ Adding product to cart", { totalAfter: orderUnsafe.total });
|
||||
return await sanitize.contentAPI.output(orderUnsafe, strapi.getModel("api::order.order"));
|
||||
},
|
||||
|
||||
removeProduct: async (ctx) => {
|
||||
const { uuid, productId } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
|
||||
const count = parseInt(ctx.query.count as string) || 1;
|
||||
const order = (await strapi.service("api::order.order").findOneByUuid(uuid)) as Order;
|
||||
const product = parseInt(productId as string);
|
||||
|
||||
if (!order) return ctx.notFound("Order not found");
|
||||
strapi.log.info("app:v:order-controller: - Removing product from cart", { totalBefore: order.total });
|
||||
|
||||
const updatedCart = [...(order.cart || [])];
|
||||
let existingIndex = -1;
|
||||
|
||||
if (typeof productId === "string") {
|
||||
existingIndex = updatedCart.findIndex((item) => item.product.id === parseInt(productId));
|
||||
}
|
||||
|
||||
if (existingIndex === -1) return ctx.badRequest("Product not found in cart");
|
||||
|
||||
if (updatedCart[existingIndex].count <= count) {
|
||||
updatedCart.splice(existingIndex, 1);
|
||||
} else {
|
||||
updatedCart[existingIndex].count -= count;
|
||||
}
|
||||
|
||||
const orderUnsafe = await strapi.service("api::order.order").update(order.id, {
|
||||
data: { cart: updatedCart },
|
||||
...orderDefaultParams
|
||||
});
|
||||
strapi.log.info("app:v:order-controller: ✔ Removing product from cart", { totalAfter: order.total });
|
||||
return await sanitize.contentAPI.output(orderUnsafe, strapi.getModel("api::order.order"));
|
||||
},
|
||||
|
||||
checkout: async (ctx) => {
|
||||
const { uuid } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
|
||||
const { returnUrl } = await sanitize.contentAPI.query(ctx.query, strapi.getModel("api::order.order"));
|
||||
const order = await strapi.db.query("api::order.order").findOne({
|
||||
where: { uuid },
|
||||
...orderDefaultParams
|
||||
});
|
||||
if (!order) return ctx.notFound("Order not found");
|
||||
|
||||
return await paypalApi.createSessionOrThrow(returnUrl as string, order);
|
||||
},
|
||||
|
||||
capturePayment: async (ctx) => {
|
||||
const { uuid, paypalOrderId } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
|
||||
strapi.log.info(`app:i:order-controller: Capturing payment for order ${uuid} with PayPal order ${paypalOrderId}`);
|
||||
|
||||
const order = await strapi.db.query("api::order.order").findOne({
|
||||
where: { uuid },
|
||||
...orderDefaultParams
|
||||
});
|
||||
if (!order) return ctx.notFound("Order not found");
|
||||
|
||||
if (order.paymentAuthorised) {
|
||||
strapi.log.info(`app:i:order-controller: Payment already authorised for order ${uuid}`);
|
||||
return { success: true, alreadyCaptured: true };
|
||||
}
|
||||
|
||||
try {
|
||||
// Capture the payment via PayPal server SDK
|
||||
const captureResult = await paypalApi.checkPaymentStatus({ orderID: paypalOrderId as string });
|
||||
|
||||
if (captureResult.status === "COMPLETED") {
|
||||
// Update order with payment info
|
||||
const updatedOrder = await strapi.entityService.update("api::order.order", order.id, {
|
||||
data: {
|
||||
paymentAuthorised: true,
|
||||
paymentStatus: "authorised",
|
||||
paypalOrderId: paypalOrderId as string
|
||||
},
|
||||
...orderDefaultParams
|
||||
});
|
||||
|
||||
strapi.log.info(`app:i:order-controller: Payment captured successfully for order ${uuid}`);
|
||||
return await sanitize.contentAPI.output(updatedOrder, strapi.getModel("api::order.order"));
|
||||
} else {
|
||||
strapi.log.error(`app:e:order-controller: Payment capture failed for order ${uuid}`, { captureResult });
|
||||
return ctx.badRequest("Payment capture failed", { status: captureResult.status });
|
||||
}
|
||||
} catch (error) {
|
||||
strapi.log.error(`app:e:order-controller: Error capturing payment for order ${uuid}`, { error });
|
||||
return ctx.internalServerError("Payment capture failed");
|
||||
}
|
||||
},
|
||||
|
||||
generateInvoice: async (ctx) => {
|
||||
const { uuid } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
|
||||
strapi.log.info(`app:i:order-controller: Started invoice generation`);
|
||||
const orderUnsafe = (await strapi.service("api::order.order").findOneByUuid(uuid)) as Order;
|
||||
|
||||
if (!orderUnsafe.paymentAuthorised) {
|
||||
return ctx.locked("Payment not authorised, cannot generate invoice");
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedOrderUnsafe = await strapi.service("api::order.order").uploadPdf(orderUnsafe, "invoice", "Rechnung");
|
||||
return await sanitize.contentAPI.output(updatedOrderUnsafe, strapi.getModel("api::order.order"));
|
||||
} catch (error) {
|
||||
strapi.log.error("app:e:order-controller: Error uploading delivery note pdf", { error });
|
||||
return ctx.internalServerError("Could not upload delivery note");
|
||||
}
|
||||
},
|
||||
|
||||
generateDeliveryNote: async (ctx) => {
|
||||
const { uuid } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
|
||||
strapi.log.info(`app:i:order-controller: Started delivery note generation`);
|
||||
const orderUnsafe = (await strapi.service("api::order.order").findOneByUuid(uuid)) as Order;
|
||||
|
||||
if (!orderUnsafe.paymentAuthorised) {
|
||||
return ctx.locked("Payment not authorised, cannot generate delivery note");
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedOrderUnsafe = await strapi.service("api::order.order").uploadPdf(orderUnsafe, "deliveryNote", "Lieferschein");
|
||||
return await sanitize.contentAPI.output(updatedOrderUnsafe, strapi.getModel("api::order.order"));
|
||||
} catch (error) {
|
||||
strapi.log.error("app:e:order-controller: Error uploading delivery note pdf", { error });
|
||||
return ctx.internalServerError("Could not upload delivery note");
|
||||
}
|
||||
},
|
||||
|
||||
sendInvoice: async (ctx) => {
|
||||
const { uuid } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
|
||||
strapi.log.info("app:i:order-controller: Sending invoice for order", { uuid });
|
||||
|
||||
try {
|
||||
const updatedOrderUnsafe = await strapi.service("api::order.order").sendInvoice(uuid, undefined);
|
||||
return await sanitize.contentAPI.output(updatedOrderUnsafe, strapi.getModel("api::order.order"));
|
||||
} catch (error) {
|
||||
strapi.log.error("app:e:order-controller: Error sending invoice", { error });
|
||||
return ctx.internalServerError("Could not send invoice");
|
||||
}
|
||||
},
|
||||
|
||||
sendDeliveryNote: async (ctx) => {
|
||||
const { uuid } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
|
||||
strapi.log.info("app:i:order-controller: Sending delivery note for", { uuid });
|
||||
return await strapi.service("api::order.order").sendDeliveryNote(uuid, undefined);
|
||||
},
|
||||
|
||||
webhook: async (ctx) => {
|
||||
try {
|
||||
return await paypalApi.handleWebhook(ctx.request.body);
|
||||
} catch (error) {
|
||||
strapi.log.error(`paypal-api: ${JSON.stringify({ error })}`);
|
||||
return ctx.internalServerError();
|
||||
}
|
||||
}
|
||||
}));
|
||||
Reference in new issue
Block a user