feat: extract cms from mp/cms — initial libreshop/cms
Some checks failed
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:
Michael Czechowski
2026-04-29 17:48:30 +02:00
commit 32a296baf2
127 changed files with 44618 additions and 0 deletions

View File

@@ -0,0 +1,269 @@
import path from "path";
import fs from "fs";
import { factories } from "@strapi/strapi";
import { sanitize } from "@strapi/utils";
import { ID } from "@strapi/database/dist/types";
import { vatDecimal, vatIncludedDecimal, baseUrl, adminEmail } from "../../../../config/constants";
import { calculateTotalProductPrice, productDefaultParams } from "../../product/services/product";
import { CartProduct, Order, PdfBody } from "../../../../types";
import { pdfApi } from "../../../services/PdfApi";
import { invoiceEmailTemplate, deliveryNoteEmailTemplate } from "../../../templates/emailTemplates";
import _, { template } from "lodash";
import { PdfMessageBody, MessageBody, mailApi } from "../../../services/MailApi";
/**
* This service is responsible for handling the order logic.
*/
export default factories.createCoreService("api::order.order", ({ strapi }) => ({
findOne: async (id: ID) => {
return await strapi.entityService.findOne("api::order.order", id, orderDefaultParams);
},
findOneByUuid: async (uuid: string) => {
const params = {
filters: {
uuid: {
$eq: uuid
}
},
...orderDefaultParams
};
const orderUnsafe = await strapi.db.query("api::order.order").findOne({
where: { uuid },
...orderDefaultParams
});
orderUnsafe.cart = orderUnsafe?.cart.map((item: CartProduct) => ({
...item,
product: {
...item.product,
totalProductPrice: calculateTotalProductPrice(item.product)
}
}));
return (await sanitize.contentAPI.output(orderUnsafe, strapi.getModel("api::order.order"))) as Order;
},
uploadPdf: async (orderUnsafe: Order, field: "invoice" | "deliveryNote", name = "Upload") => {
strapi.log.verbose(`app:v:order-service: Generating ${field} pdf`);
const pdfBody = field === "invoice" ? invoicePdfBody(orderUnsafe) : deliveryNotePdfBody(orderUnsafe);
strapi.log.debug(`app:d:order-service: - PDF body`, pdfBody);
const blob = field === "invoice" ? await pdfApi.generateInvoice(pdfBody) : await pdfApi.generateDeliveryNote(pdfBody);
strapi.log.verbose(`app:v:order-service: ✔ Generating ${field} pdf`);
// Save blob to a temp folder
const tempFolder = path.join(__dirname, "temp");
if (!fs.existsSync(tempFolder)) {
fs.mkdirSync(tempFolder);
}
const deliveryNotePath = path.join(tempFolder, `${name}.pdf`);
fs.writeFileSync(deliveryNotePath, Buffer.from(blob));
const uploadData = {
data: {
ref: "api::order.order",
refId: orderUnsafe.id,
field
},
files: {
path: deliveryNotePath,
name: `${name}_${pdfBody.date.replace(/[^a-z0-9]/gi, "_").toLowerCase()}`,
type: "application/pdf",
size: fs.statSync(deliveryNotePath).size
}
};
strapi.log.verbose(`app:v:order-service: - Uploading ${field} pdf`);
await strapi.plugins.upload.services.upload.upload(uploadData);
strapi.log.verbose(`app:v:order-service: ✔ Uploading ${field} pdf`);
return await strapi.service("api::order.order").findOne(orderUnsafe.id);
},
sendInvoice: async (uuid: string, blob?: any) => {
strapi.log.info(`app:i:order-service: - Sending invoice for order ${uuid}`);
const oderUnsafe = (await strapi.service("api::order.order").findOneByUuid(uuid)) as Partial<Order>;
try {
const emailConfig: MessageBody = {
to_email: oderUnsafe.email,
subject: template(invoiceEmailTemplate.subject)(oderUnsafe),
message: template(invoiceEmailTemplate.text)({ baseUrl, ...oderUnsafe }),
html: template(invoiceEmailTemplate.html)({ baseUrl, ...oderUnsafe })
};
strapi.log.debug(`app:d:order-service: - Email config ${emailConfig.html}`);
// Send email to customer
await mailApi.sendTextMessage(emailConfig);
// Send email to administator
await mailApi.sendTextMessage({
...emailConfig,
to_email: adminEmail
});
strapi.log.info(`app:i:order-service: ✔ Sending invoice for order ${uuid}`);
return await strapi.db.query("api::order.order").update({
where: {
id: oderUnsafe.id
},
data: {
invoiceSent: true
}
});
} catch (error) {
strapi.log.error(`app:e:order-service: χ Sending invoice for order ${uuid}`);
strapi.log.debug(`app:d:order-service: Error: ${JSON.stringify({ error })}`);
return new Error("Could not send invoice");
}
},
sendDeliveryNote: async (uuid: string, blob?: ArrayBuffer) => {
strapi.log.info(`app:i:order-service: - Sending delivery note for order ${uuid}`);
const oderUnsafe = (await strapi.service("api::order.order").findOneByUuid(uuid)) as Partial<Order>;
try {
const emailConfig: MessageBody = {
to_email: oderUnsafe.email,
subject: template(deliveryNoteEmailTemplate.subject)(oderUnsafe),
message: template(deliveryNoteEmailTemplate.text)(oderUnsafe),
html: template(deliveryNoteEmailTemplate.html)({ ...oderUnsafe, baseUrl })
};
await mailApi.sendTextMessage(emailConfig);
strapi.log.info(`app:i:order-service: ✔ Sending delivery note for order ${uuid}`);
return await strapi.db.query("api::order.order").update({
where: {
id: oderUnsafe.id
},
data: {
deliveryNoteSent: true
}
});
} catch (error) {
strapi.log.error(`app:e:order-service: χ Sending delivery note for order ${uuid}`);
strapi.log.debug(`app:d:order-service: Error: ${JSON.stringify({ error })}`);
return new Error("Could not send delivery note");
}
},
update: async (id: ID, params: Record<string, any>): Promise<Order> => {
const dataUnsafe = (await strapi.entityService.update("api::order.order", id, params)) as Order;
strapi.log.verbose(`app:v:order-service: Updated order ${id} with initial params`);
let data: { subtotal: number; total: number; VAT: number };
if (dataUnsafe?.cart) {
const productsTotal = dataUnsafe.cart.reduce((v, p) => v + (calculateTotalProductPrice(p.product) ?? 0) * p.count, 0);
const productsVAT = dataUnsafe.cart.reduce((v, p) => {
const totalProductPrice = calculateTotalProductPrice(p.product) ?? 0;
const amount = totalProductPrice / vatIncludedDecimal;
const tax = totalProductPrice - amount;
return v + tax * p.count;
}, 0);
const deliveryPrice = dataUnsafe.delivery?.price ?? 0;
const paymentPrice = dataUnsafe.payment?.price ?? 0;
strapi.log.debug(`app:d:order-service: Calculating totals`, { productsTotal, deliveryPrice, paymentPrice });
const subtotal = productsTotal;
const total = Math.round((subtotal + deliveryPrice + paymentPrice) * 100) / 100;
const VAT = Math.round((subtotal / vatIncludedDecimal) * vatDecimal * 100) / 100;
strapi.log.verbose(`app:v:order-service: Calculated totals`, { productsTotal, total, VAT, subtotal });
data = {
subtotal,
total,
VAT
};
}
const orderUnsafe = await strapi.entityService.update("api::order.order", id, { data, ...orderDefaultParams });
strapi.log.verbose(`app:v:order-service: Updated order ${id} with calculated totals`);
orderUnsafe.cart = orderUnsafe?.cart.map((item: CartProduct) => ({
...item,
product: {
...item.product,
totalProductPrice: calculateTotalProductPrice(item.product)
}
}));
return (await sanitize.contentAPI.output(orderUnsafe, strapi.getModel("api::order.order"))) as Order;
}
}));
export const orderDefaultParams = {
populate: {
invoice: true,
deliveryNote: true,
delivery: true,
payment: true,
customer: {
populate: {
addressStructured: true
}
},
invoiceAddressStructured: true,
deliveryAddressStructured: true,
cart: {
populate: {
product: productDefaultParams
}
}
}
};
const invoicePdfBody = (order: Order): PdfBody => {
const pdfBody = defaultPdfBody(order);
return {
...pdfBody,
subject: "RECHNUNG",
to: {
name: `${order.customer.addressStructured?.givenName} ${order.customer.addressStructured?.familyName}` || "",
address: [
order.customer.addressStructured?.streetAddress || "",
`${order.customer.addressStructured?.postalCode} ${order.customer.addressStructured?.addressLevel2}` || ""
]
},
nr: {
...pdfBody.nr,
invoice: order.invoiceNumber
}
} as PdfBody;
};
const deliveryNotePdfBody = (order: Order): PdfBody => {
const pdfBody = defaultPdfBody(order);
return {
...pdfBody,
subject: "LIEFERSCHEIN",
to: {
name: "",
address: order.deliveryAddress.split("\n")
},
nr: {
...pdfBody.nr,
shipping: order.deliveryNoteNumber
}
} as PdfBody;
};
const defaultPdfBody = (order: Order): Partial<PdfBody> => ({
date: new Date(order.acceptedTermsAndConditionsAt).toLocaleDateString("de-DE"),
nr: {
customer: `${order.customer.id}`,
order: `${order.id}`
},
service:
order.cart?.map((cartProduct) => ({
description: cartProduct.product.name,
price: {
per_unit: calculateTotalProductPrice(cartProduct.product) || 0,
total: (calculateTotalProductPrice(cartProduct.product) || 0) * cartProduct.count
},
count: cartProduct.count,
nr: `P${cartProduct.product.id}`
})) || [],
currency: "\\euro",
body: "Vielen Dank für Ihren Einkauf und Ihr Vertrauen.",
total: order.total,
subtotal: order.subtotal,
VAT: {
amount: order.VAT,
rate: vatDecimal * 100
},
shipping: order.delivery?.price ?? 0
});