Files
cms/src/api/order/services/order.ts
Michael Czechowski defd078e4b
All checks were successful
Build and publish / build (push) Successful in 7m11s
feat(cms): white-label email templates and API doc config (#2)
## Summary

- Replace all hardcoded muellerprints brand identity in email templates with template variables sourced from env vars
- Inject shop context into both `sendInvoice` and `sendDeliveryNote` template calls in `order.ts`
- Make API documentation title/description env-var driven

## New env vars (all optional with safe defaults)

| Var | Default | Used in |
|-----|---------|---------|
| `SHOP_NAME` | `"Shop"` | Email subject, body, footer |
| `SHOP_CONTACT_NAME` | `""` | Email footer |
| `SHOP_ADDRESS` | `""` | Email footer |
| `SHOP_PHONE` | `""` | Email footer |
| `SHOP_EMAIL` | `ADMIN_EMAIL_ADDRESS` | Email footer |
| `SHOP_LOGO_URL` | `""` | Email header logo |
| `SHOP_SECONDARY_LOGO_URL` | `""` | Email footer logo |
| `API_TITLE` | `"Paperwork API"` | Swagger/OpenAPI title |
| `API_DESCRIPTION` | `"Paperwork API"` | Swagger/OpenAPI description |

## Test plan

- [ ] Set `SHOP_NAME=TestShop` and trigger invoice send → confirm subject and body use `TestShop`
- [ ] Leave `SHOP_NAME` unset → confirm default `"Shop"` appears
- [ ] Set `SHOP_LOGO_URL` → confirm logo renders in email header; unset → confirm no broken `<img>` tag
- [ ] Check Strapi admin `/documentation` with `API_TITLE=MyAPI` env var

Closes #1

Reviewed-on: #2
Co-authored-by: Michael Czechowski <mail@dailysh.it>
Co-committed-by: Michael Czechowski <mail@dailysh.it>
2026-04-29 20:13:23 +02:00

272 lines
9.4 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 { 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
});
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>;
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>;
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
});