Compare commits

8 Commits
Author SHA1 Message Date
libretech 901a3b385f feat(product): price standalone products from the cover alone
build / build (push) Successful in 8m52s
Recovered work in progress from 2026-05. A product-cover can be marked
standalone, and a standalone product (Journaling Booklet) prices as the
cover price without the pattern/pages/ruling lookups, which do not exist
for it. The remaining component lookups are now guarded so a missing
relation yields null instead of calling findOne with undefined.
2026-09-23 22:10:06 +02:00
libretech 7f7cacc448 ci: bump runner-image pin v0.1.0 → v0.2.0 (#4)
build / build (push) Successful in 5m57s
Co-authored-by: Michael Czechowski <mail@dailysh.it>
Co-committed-by: Michael Czechowski <mail@dailysh.it>
2026-05-04 23:46:06 +02:00
libretech fa2a81ecd1 fix(ci): correct image digest separator
build / build (push) Successful in 7m47s
2026-04-30 12:20:33 +02:00
libretech 8e0554572b ci: digest-pin runner-image v0.1.0
build / build (push) Failing after 0s
2026-04-30 12:17:55 +02:00
libretech 3213a5a400 ci: update runner-image path to public/ namespace
build / build (push) Failing after 1s
2026-04-30 11:57:23 +02:00
libretech 5c36ce7dbb chore: update action ref to public/actions
build / build (push) Successful in 6m4s
2026-04-29 22:50:14 +02:00
libretech 9600bc8fd3 chore: slim build.yml to use shared docker-build action (#3)
build / build (push) Successful in 6m43s
2026-04-29 22:08:31 +02:00
libretech defd078e4b feat(cms): white-label email templates and API doc config (#2)
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>
2026-04-29 20:13:23 +02:00
12 changed files with 134 additions and 92 deletions
+6 -37
View File
@@ -1,4 +1,4 @@
name: Build and publish name: build
on: on:
push: push:
@@ -7,49 +7,18 @@ on:
pull_request: pull_request:
branches: [main] branches: [main]
# Required secrets:
# REGISTRY git.librete.ch
# REGISTRY_USER libretech-bot
# REGISTRY_PASS bot PAT (write:package; bot is in libreshop Owners team)
# Required variable:
# PUBLISH_ENABLED "true" to actually push (off = build-only on PRs)
#
# Image: git.librete.ch/libreshop/cms
# main pushes → :main + :sha-<short>
# tag pushes → :<tag> + :latest
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: git.librete.ch/libretech/runner-image:v1 image: git.librete.ch/public/runner-image:v0.2.0@sha256:f60c587d3c0b0aac04a572db5349e27672bf76baec2ce547a3dcc28cebcf1b7e
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3 - uses: https://git.librete.ch/public/actions/.gitea/actions/docker-build@main
- name: Login (only when publishing)
if: ${{ vars.PUBLISH_ENABLED == 'true' }}
uses: docker/login-action@v3
with: with:
registry: ${{ secrets.REGISTRY }} registry: ${{ secrets.REGISTRY }}
username: ${{ secrets.REGISTRY_USER }} registry_user: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASS }} registry_pass: ${{ secrets.REGISTRY_PASS }}
publish: ${{ github.event_name == 'push' && vars.PUBLISH_ENABLED == 'true' }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.REGISTRY }}/libreshop/cms
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,format=short
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
- uses: docker/build-push-action@v6
with:
context: .
push: ${{ vars.PUBLISH_ENABLED == 'true' && github.event_name == 'push' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+20 -1
View File
@@ -2,7 +2,26 @@
All notable changes to libreshop/cms are documented here. All notable changes to libreshop/cms are documented here.
## Unreleased ## [0.1.1] - 2026-04-29
### Changed
- White-label email templates: replace all hardcoded muellerprints company
identity (name, contact, address, phone, email, logo URLs) with
`<%= shopName %>` / `<%= shopContactName %>` / `<%= shopAddress %>` /
`<%= shopPhone %>` / `<%= shopEmail %>` / `<%= shopLogoUrl %>` /
`<%= shopSecondaryLogoUrl %>` template variables, sourced from env vars
`SHOP_NAME`, `SHOP_CONTACT_NAME`, `SHOP_ADDRESS`, `SHOP_PHONE`,
`SHOP_EMAIL`, `SHOP_LOGO_URL`, `SHOP_SECONDARY_LOGO_URL`.
- `config/constants.ts`: export shop brand constants from env.
- `src/api/order/services/order.ts`: inject shop context into template calls.
- `config/plugins.ts`: API doc title/description driven by `API_TITLE` /
`API_DESCRIPTION` env vars (defaults: `"Paperwork API"`).
- `src/extensions/documentation/config/settings.json`: neutral placeholder.
Closes #1
## [0.1.0] - 2026-04-29
- Extracted from `mp/cms/` (2026-04-29). The component history before - Extracted from `mp/cms/` (2026-04-29). The component history before
the extraction lives in the `muellerprints` repository. the extraction lives in the `muellerprints` repository.
+8
View File
@@ -10,6 +10,14 @@ export const paypalEnvironment = process.env.PAYPAL_ENVIRONMENT! === "production
export const adminEmail = process.env.ADMIN_EMAIL_ADDRESS!; export const adminEmail = process.env.ADMIN_EMAIL_ADDRESS!;
export const shopName = process.env.SHOP_NAME ?? "Shop";
export const shopContactName = process.env.SHOP_CONTACT_NAME ?? "";
export const shopAddress = process.env.SHOP_ADDRESS ?? "";
export const shopPhone = process.env.SHOP_PHONE ?? "";
export const shopEmail = process.env.SHOP_EMAIL ?? process.env.ADMIN_EMAIL_ADDRESS ?? "";
export const shopLogoUrl = process.env.SHOP_LOGO_URL ?? "";
export const shopSecondaryLogoUrl = process.env.SHOP_SECONDARY_LOGO_URL ?? "";
// TODO: Should be retrieved from DepotApi // TODO: Should be retrieved from DepotApi
export const vatIncludedDecimal = 1.19; export const vatIncludedDecimal = 1.19;
// TODO: Should be retrieved from DepotApi // TODO: Should be retrieved from DepotApi
+4 -8
View File
@@ -10,15 +10,11 @@ export default ({ env }) => ({
openapi: "3.0.1", openapi: "3.0.1",
info: { info: {
version: "1.0.0", version: "1.0.0",
title: "MUELLERPTINTS. Paperwork", title: env("API_TITLE", "Paperwork API"),
description: "API Documentation for MUELLERPRINTS. Paperwork", description: env("API_DESCRIPTION", "Paperwork API"),
termsOfService: false, termsOfService: false,
contact: { contact: false,
name: "Michael W. Czechowski", license: false,
email: "mail@dailysh.it",
url: "https://dailysh.it"
},
license: "Copyright (C) 2024 Michael W. Czechowski",
externalDocs: false externalDocs: false
}, },
"x-strapi-config": { "x-strapi-config": {
+9 -7
View File
@@ -3,7 +3,7 @@ import fs from "fs";
import { factories } from "@strapi/strapi"; import { factories } from "@strapi/strapi";
import { sanitize } from "@strapi/utils"; import { sanitize } from "@strapi/utils";
import { ID } from "@strapi/database/dist/types"; import { ID } from "@strapi/database/dist/types";
import { vatDecimal, vatIncludedDecimal, baseUrl, adminEmail } from "../../../../config/constants"; import { vatDecimal, vatIncludedDecimal, baseUrl, adminEmail, shopName, shopContactName, shopAddress, shopPhone, shopEmail, shopLogoUrl, shopSecondaryLogoUrl } from "../../../../config/constants";
import { calculateTotalProductPrice, productDefaultParams } from "../../product/services/product"; import { calculateTotalProductPrice, productDefaultParams } from "../../product/services/product";
import { CartProduct, Order, PdfBody } from "../../../../types"; import { CartProduct, Order, PdfBody } from "../../../../types";
import { pdfApi } from "../../../services/PdfApi"; import { pdfApi } from "../../../services/PdfApi";
@@ -81,12 +81,13 @@ export default factories.createCoreService("api::order.order", ({ strapi }) => (
strapi.log.info(`app:i:order-service: - Sending invoice for order ${uuid}`); 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 oderUnsafe = (await strapi.service("api::order.order").findOneByUuid(uuid)) as Partial<Order>;
const shopContext = { shopName, shopContactName, shopAddress, shopPhone, shopEmail, shopLogoUrl, shopSecondaryLogoUrl };
try { try {
const emailConfig: MessageBody = { const emailConfig: MessageBody = {
to_email: oderUnsafe.email, to_email: oderUnsafe.email,
subject: template(invoiceEmailTemplate.subject)(oderUnsafe), subject: template(invoiceEmailTemplate.subject)({ ...shopContext, ...oderUnsafe }),
message: template(invoiceEmailTemplate.text)({ baseUrl, ...oderUnsafe }), message: template(invoiceEmailTemplate.text)({ baseUrl, ...shopContext, ...oderUnsafe }),
html: template(invoiceEmailTemplate.html)({ baseUrl, ...oderUnsafe }) html: template(invoiceEmailTemplate.html)({ baseUrl, ...shopContext, ...oderUnsafe })
}; };
strapi.log.debug(`app:d:order-service: - Email config ${emailConfig.html}`); strapi.log.debug(`app:d:order-service: - Email config ${emailConfig.html}`);
@@ -118,12 +119,13 @@ export default factories.createCoreService("api::order.order", ({ strapi }) => (
sendDeliveryNote: async (uuid: string, blob?: ArrayBuffer) => { sendDeliveryNote: async (uuid: string, blob?: ArrayBuffer) => {
strapi.log.info(`app:i:order-service: - Sending delivery note for order ${uuid}`); 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 oderUnsafe = (await strapi.service("api::order.order").findOneByUuid(uuid)) as Partial<Order>;
const shopContext = { shopName, shopContactName, shopAddress, shopPhone, shopEmail, shopLogoUrl, shopSecondaryLogoUrl };
try { try {
const emailConfig: MessageBody = { const emailConfig: MessageBody = {
to_email: oderUnsafe.email, to_email: oderUnsafe.email,
subject: template(deliveryNoteEmailTemplate.subject)(oderUnsafe), subject: template(deliveryNoteEmailTemplate.subject)({ ...shopContext, ...oderUnsafe }),
message: template(deliveryNoteEmailTemplate.text)(oderUnsafe), message: template(deliveryNoteEmailTemplate.text)({ ...shopContext, ...oderUnsafe }),
html: template(deliveryNoteEmailTemplate.html)({ ...oderUnsafe, baseUrl }) html: template(deliveryNoteEmailTemplate.html)({ baseUrl, ...shopContext, ...oderUnsafe })
}; };
await mailApi.sendTextMessage(emailConfig); await mailApi.sendTextMessage(emailConfig);
strapi.log.info(`app:i:order-service: ✔ Sending delivery note for order ${uuid}`); strapi.log.info(`app:i:order-service: ✔ Sending delivery note for order ${uuid}`);
@@ -54,6 +54,10 @@
"price": { "price": {
"type": "decimal" "type": "decimal"
}, },
"standalone": {
"type": "boolean",
"default": false
},
"products": { "products": {
"type": "relation", "type": "relation",
"relation": "oneToMany", "relation": "oneToMany",
@@ -11,10 +11,26 @@ export default {
strapi.log.verbose("app:v:product-lifecycle: Before create", { strapi.log.verbose("app:v:product-lifecycle: Before create", {
params params
}); });
const cover = await strapi.entityService.findOne("api::product-cover.product-cover", params.data?.cover, { fields: ["price"] }); const cover = params.data?.cover
const pattern = await strapi.entityService.findOne("api::product-pattern.product-pattern", params.data?.pattern); ? await strapi.entityService.findOne("api::product-cover.product-cover", params.data.cover, { fields: ["price", "standalone"] })
const pages = await strapi.entityService.findOne("api::product-page.product-page", params.data?.pages, { fields: ["price"] }); : null;
const ruling = await strapi.entityService.findOne("api::product-ruling.product-ruling", params.data?.ruling, { fields: ["price"] });
// Standalone products (e.g. Journaling Booklet) have no pattern/pages/ruling.
// Price is the cover price alone — skip combinatorial component lookups.
if (params.data?.standalone || (cover as any)?.standalone) {
params.data.totalPrice = cover?.price ?? 0;
return;
}
const pattern = params.data?.pattern
? await strapi.entityService.findOne("api::product-pattern.product-pattern", params.data.pattern)
: null;
const pages = params.data?.pages
? await strapi.entityService.findOne("api::product-page.product-page", params.data.pages, { fields: ["price"] })
: null;
const ruling = params.data?.ruling
? await strapi.entityService.findOne("api::product-ruling.product-ruling", params.data.ruling, { fields: ["price"] })
: null;
const product = { const product = {
...params.data, ...params.data,
cover, cover,
+18 -1
View File
@@ -108,6 +108,12 @@ export default factories.createCoreController("api::product.product", ({ strapi
strapi.log.verbose(`Fetching variants for product ${id}`); strapi.log.verbose(`Fetching variants for product ${id}`);
const product = await getProductDetails(id as ID); const product = await getProductDetails(id as ID);
// Standalone products (e.g. Journaling Booklet) have no pattern — no variants.
if (!product.pattern?.id) {
return [];
}
const variants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>("api::product.product", { const variants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>("api::product.product", {
...productVariantParams, ...productVariantParams,
filters: { filters: {
@@ -146,6 +152,11 @@ export default factories.createCoreController("api::product.product", ({ strapi
const product = await getProductDetails(id as ID); const product = await getProductDetails(id as ID);
const { pattern, cover, ruling, pages } = product; const { pattern, cover, ruling, pages } = product;
// Standalone products (e.g. Journaling Booklet) have no pattern → no variants.
if (!pattern?.id) {
return { allProductPattern: [], productVariants: [], patterns: [] };
}
const productVariants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>("api::product.product", { const productVariants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>("api::product.product", {
...productVariantParams, ...productVariantParams,
filters: { filters: {
@@ -203,6 +214,11 @@ export default factories.createCoreController("api::product.product", ({ strapi
const product = await getProductDetails(id as ID); const product = await getProductDetails(id as ID);
// Standalone products (e.g. Journaling Booklet) have no pattern → no variant groups.
if (!product.pattern?.id) {
return { pages: [], cover: [], ruling: [] };
}
const allProductPatternVariants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>( const allProductPatternVariants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>(
"api::product.product", "api::product.product",
{ {
@@ -335,7 +351,8 @@ export default factories.createCoreController("api::product.product", ({ strapi
const productGroups = new Map(); const productGroups = new Map();
allProductsSanitized.forEach((product) => { allProductsSanitized.forEach((product) => {
const key = `${product.cover.id}-${product.pattern.id}`; // Standalone products (e.g. Journaling Booklet) have no pattern — never grouped.
const key = product.pattern?.id ? `${product.cover?.id}-${product.pattern.id}` : `standalone-${product.id}`;
if (!productGroups.has(key) || calculateTotalProductPrice(product) < calculateTotalProductPrice(productGroups.get(key))) { if (!productGroups.has(key) || calculateTotalProductPrice(product) < calculateTotalProductPrice(productGroups.get(key))) {
productGroups.set(key, product); productGroups.set(key, product);
} }
@@ -2,8 +2,8 @@
"openapi": "3.0.1", "openapi": "3.0.1",
"info": { "info": {
"version": "1.0.0", "version": "1.0.0",
"title": "MUELLERPTINTS. Paperwork", "title": "Paperwork API",
"description": "API Documentation for MUELLERPRINTS. Paperwork", "description": "Paperwork API",
"termsOfService": false, "termsOfService": false,
"contact": false, "contact": false,
"license": false "license": false
+31 -31
View File
@@ -1,5 +1,5 @@
export const invoiceEmailTemplate = { export const invoiceEmailTemplate = {
subject: "MUELLERPRINTS Rechnungsnummer <%= invoiceNumber %>", subject: "<%= shopName %> Rechnungsnummer <%= invoiceNumber %>",
text: `Rechnungsnummer: <%= invoiceNumber %> text: `Rechnungsnummer: <%= invoiceNumber %>
Ausstellungsdatum: <%= new Date(acceptedTermsAndConditionsAt).toLocaleDateString("de-DE") %> Ausstellungsdatum: <%= new Date(acceptedTermsAndConditionsAt).toLocaleDateString("de-DE") %>
@@ -7,7 +7,7 @@ Ausstellungsdatum: <%= new Date(acceptedTermsAndConditionsAt).toLocaleDateString
Hallo <%= invoiceAddressStructured.givenName %>, Hallo <%= invoiceAddressStructured.givenName %>,
vielen Dank für deinen Einkauf bei MUELLERPRINTS. vielen Dank für deinen Einkauf bei <%= shopName %>.
Deine Bestellnummer: <%= id %> Deine Bestellnummer: <%= id %>
Zur Bestellübersicht: <%= baseUrl %>/checkout/result/<%= uuid %> Zur Bestellübersicht: <%= baseUrl %>/checkout/result/<%= uuid %>
@@ -49,14 +49,14 @@ inkl. MwSt. (19%): <%= VAT.toFixed(2) %> EUR
Deine Rechnung kannst du jederzeit über diesen Link herunterladen: <%= baseUrl %><%= invoice.url %> Deine Rechnung kannst du jederzeit über diesen Link herunterladen: <%= baseUrl %><%= invoice.url %>
<% } %> <% } %>
Vielen Dank, dass du dich für MUELLERPRINTS entschieden hast. Vielen Dank, dass du dich für <%= shopName %> entschieden hast.
------------------ ------------------
MUELLERPRINTS <%= shopName %>
Max Müller <%= shopContactName %>
Rotenbergstraße 39, 70190 Stuttgart <%= shopAddress %>
T +49 (0)711/262 49 64 <%= shopPhone ? "T " + shopPhone : "" %>
paperwork@muellerprints.de`, <%= shopEmail %>`,
html: `<!DOCTYPE html> html: `<!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
@@ -74,7 +74,7 @@ paperwork@muellerprints.de`,
<!-- Header --> <!-- Header -->
<tr> <tr>
<td style="background-color: #E31E26; padding: 30px 40px;"> <td style="background-color: #E31E26; padding: 30px 40px;">
<img src="https://muellerprints-paperwork.com/paperwork-logo.png" alt="MUELLERPRINTS PAPERWORK" style="max-width: 200px; height: auto;"> <% if (shopLogoUrl) { %><img src="<%= shopLogoUrl %>" alt="<%= shopName %>" style="max-width: 200px; height: auto;"><% } %>
</td> </td>
</tr> </tr>
@@ -83,7 +83,7 @@ paperwork@muellerprints.de`,
<td style="padding: 40px;"> <td style="padding: 40px;">
<!-- Greeting --> <!-- Greeting -->
<h1 style="margin: 0 0 20px; color: #333; font-size: 24px; font-weight: normal;"> <h1 style="margin: 0 0 20px; color: #333; font-size: 24px; font-weight: normal;">
Vielen Dank für deinen Einkauf bei MUELLERPRINTS. Vielen Dank für deinen Einkauf bei <%= shopName %>.
</h1> </h1>
<!-- Order Info --> <!-- Order Info -->
@@ -197,7 +197,7 @@ paperwork@muellerprints.de`,
<% } %> <% } %>
<p style="color: #666; font-size: 14px; margin: 20px 0;"> <p style="color: #666; font-size: 14px; margin: 20px 0;">
Vielen Dank, dass du dich für MUELLERPRINTS entschieden hast. Vielen Dank, dass du dich für <%= shopName %> entschieden hast.
</p> </p>
</td> </td>
</tr> </tr>
@@ -209,15 +209,15 @@ paperwork@muellerprints.de`,
<tr> <tr>
<td style="color: #666; font-size: 12px; line-height: 1.5;"> <td style="color: #666; font-size: 12px; line-height: 1.5;">
<p style="margin: 0 0 10px;"> <p style="margin: 0 0 10px;">
MUELLERPRINTS<br> <%= shopName %><br>
Max Müller<br> <% if (shopContactName) { %><%= shopContactName %><br><% } %>
Rotenbergstraße 39, 70190 Stuttgart<br> <% if (shopAddress) { %><%= shopAddress %><br><% } %>
T +49 (0)711/262 49 64<br> <% if (shopPhone) { %>T <%= shopPhone %><br><% } %>
paperwork@muellerprints.de <% if (shopEmail) { %><%= shopEmail %><% } %>
</p> </p>
</td> </td>
<td style="text-align: right; vertical-align: bottom;"> <td style="text-align: right; vertical-align: bottom;">
<img src="https://www.muellerprints.de/images/digi_logo.png" alt="MUELLERPRINTS" style="max-width: 100px; height: auto;"> <% if (shopSecondaryLogoUrl) { %><img src="<%= shopSecondaryLogoUrl %>" alt="<%= shopName %>" style="max-width: 100px; height: auto;"><% } %>
</td> </td>
</tr> </tr>
</table> </table>
@@ -243,7 +243,7 @@ paperwork@muellerprints.de`,
}; };
export const deliveryNoteEmailTemplate = { export const deliveryNoteEmailTemplate = {
subject: "Your Delivery Note <%= deliveryNoteNumber %> from MUELLERPRINTS", subject: "Your Delivery Note <%= deliveryNoteNumber %> from <%= shopName %>",
text: `Dear Customer, text: `Dear Customer,
Thank you for your Your delivery note <%= deliveryNoteNumber %> is attached to this email. Thank you for your Your delivery note <%= deliveryNoteNumber %> is attached to this email.
@@ -272,14 +272,14 @@ Delivery Address:
If you have any questions about your delivery, please don't hesitate to contact us. If you have any questions about your delivery, please don't hesitate to contact us.
Best regards, Best regards,
MUELLERPRINTS PAPERWORK <%= shopName %>
------------------ ------------------
MUELLERPRINTS PAPERWORK <%= shopName %>
Max Müller <%= shopContactName %>
Rotenbergstraße 39, 70190 Stuttgart <%= shopAddress %>
T +49 (0)711/262 49 64 <%= shopPhone ? "T " + shopPhone : "" %>
paperwork@muellerprints.de`, <%= shopEmail %>`,
html: `<!DOCTYPE html> html: `<!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
@@ -295,7 +295,7 @@ paperwork@muellerprints.de`,
<!-- Header --> <!-- Header -->
<tr> <tr>
<td style="background-color: #E31E26; padding: 30px 40px;"> <td style="background-color: #E31E26; padding: 30px 40px;">
<img src="https://muellerprints-paperwork.com/paperwork-logo.png" alt="MUELLERPRINTS PAPERWORK" style="max-width: 200px; height: auto;"> <% if (shopLogoUrl) { %><img src="<%= shopLogoUrl %>" alt="<%= shopName %>" style="max-width: 200px; height: auto;"><% } %>
</td> </td>
</tr> </tr>
@@ -407,15 +407,15 @@ paperwork@muellerprints.de`,
<tr> <tr>
<td style="color: #666; font-size: 12px; line-height: 1.5;"> <td style="color: #666; font-size: 12px; line-height: 1.5;">
<p style="margin: 0 0 10px;"> <p style="margin: 0 0 10px;">
MUELLERPRINTS PAPERWORK<br> <%= shopName %><br>
Max Müller<br> <% if (shopContactName) { %><%= shopContactName %><br><% } %>
Rotenbergstraße 39, 70190 Stuttgart<br> <% if (shopAddress) { %><%= shopAddress %><br><% } %>
T +49 (0)711/262 49 64<br> <% if (shopPhone) { %>T <%= shopPhone %><br><% } %>
paperwork@muellerprints.de <% if (shopEmail) { %><%= shopEmail %><% } %>
</p> </p>
</td> </td>
<td style="text-align: right; vertical-align: bottom;"> <td style="text-align: right; vertical-align: bottom;">
<img src="https://www.muellerprints.de/images/digi_logo.png" alt="MUELLERPRINTS" style="max-width: 100px; height: auto;"> <% if (shopSecondaryLogoUrl) { %><img src="<%= shopSecondaryLogoUrl %>" alt="<%= shopName %>" style="max-width: 100px; height: auto;"><% } %>
</td> </td>
</tr> </tr>
</table> </table>
+11 -1
View File
@@ -110,8 +110,12 @@ async function generateNewProducts(strapi: Strapi, componentType: string, compon
strapi.log.debug(`app:d:lifecycle-factory ${JSON.stringify({ covers, patterns, pages, rulings })}`); strapi.log.debug(`app:d:lifecycle-factory ${JSON.stringify({ covers, patterns, pages, rulings })}`);
// Standalone covers (e.g. the Journaling Booklet) are not combinatorial —
// their product is created manually and must never be fanned out.
const sellableCovers = covers.filter((cover) => cover && !(cover as ProductCover).standalone);
// Generate new combinations // Generate new combinations
for (const cover of covers) { for (const cover of sellableCovers) {
for (const pattern of patterns) { for (const pattern of patterns) {
for (const page of pages) { for (const page of pages) {
for (const ruling of rulings) { for (const ruling of rulings) {
@@ -181,6 +185,12 @@ async function updateRelatedProducts(strapi: Strapi, componentType: string, comp
for (const product of products) { for (const product of products) {
try { try {
// Standalone products (cover-only, e.g. Journaling Booklet) are not
// factory-managed — never rename/reslug/delete them.
if (!product.pattern?.id) {
continue;
}
// Get the current components for this product // Get the current components for this product
const cover = product.cover?.id; const cover = product.cover?.id;
const pattern = product.pattern?.id; const pattern = product.pattern?.id;
+1
View File
@@ -453,6 +453,7 @@ export interface ApiProductCoverProductCover extends Schema.CollectionType {
number number
> & > &
Attribute.DefaultTo<0>; Attribute.DefaultTo<0>;
standalone: Attribute.Boolean & Attribute.DefaultTo<false>;
updatedAt: Attribute.DateTime; updatedAt: Attribute.DateTime;
updatedBy: Attribute.Relation<"api::product-cover.product-cover", "oneToOne", "admin::user"> & Attribute.Private; updatedBy: Attribute.Relation<"api::product-cover.product-cover", "oneToOne", "admin::user"> & Attribute.Private;
}; };