All checks were successful
Build and publish / build (push) Successful in 7m11s
## 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>
272 lines
9.4 KiB
TypeScript
272 lines
9.4 KiB
TypeScript
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
|
||
});
|