Files
cms/src/api/order/services/order.ts
T
libretech 2992e4805e fix(order): answer 404 instead of 500 for a missing order
findOne and findOneByUuid mapped order.cart before checking that the order
exists, and the service's findOneByUuid did the same, so a missing order
threw a TypeError: GET /orders/:uuid/cart, add-product, remove-product,
generate-invoice and generate-delivery-note answered 500.

The service's findOneByUuid now returns null for a missing order, and each
caller handles it: the controllers answer 404, sendInvoice and
sendDeliveryNote throw a NotFoundError.

Refs libretech/mp#71
2026-10-08 22:55:13 +02:00

275 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import path from "path";
import fs from "fs";
import { factories } from "@strapi/strapi";
import { errors, sanitize } from "@strapi/utils";
import { ID } from "@strapi/database/dist/types";
import { vatDecimal, vatIncludedDecimal, baseUrl, adminEmail, shopName, shopContactName, shopAddress, shopPhone, shopEmail, shopLogoUrl, shopSecondaryLogoUrl } 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
});
if (!orderUnsafe) return null;
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>;
if (!oderUnsafe) throw new errors.NotFoundError("Order not found");
const shopContext = { shopName, shopContactName, shopAddress, shopPhone, shopEmail, shopLogoUrl, shopSecondaryLogoUrl };
try {
const emailConfig: MessageBody = {
to_email: oderUnsafe.email,
subject: template(invoiceEmailTemplate.subject)({ ...shopContext, ...oderUnsafe }),
message: template(invoiceEmailTemplate.text)({ baseUrl, ...shopContext, ...oderUnsafe }),
html: template(invoiceEmailTemplate.html)({ baseUrl, ...shopContext, ...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>;
if (!oderUnsafe) throw new errors.NotFoundError("Order not found");
const shopContext = { shopName, shopContactName, shopAddress, shopPhone, shopEmail, shopLogoUrl, shopSecondaryLogoUrl };
try {
const emailConfig: MessageBody = {
to_email: oderUnsafe.email,
subject: template(deliveryNoteEmailTemplate.subject)({ ...shopContext, ...oderUnsafe }),
message: template(deliveryNoteEmailTemplate.text)({ ...shopContext, ...oderUnsafe }),
html: template(deliveryNoteEmailTemplate.html)({ baseUrl, ...shopContext, ...oderUnsafe })
};
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
});