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

9
src/admin/app.ts Normal file
View File

@@ -0,0 +1,9 @@
export default {
config: {
// Disable video tutorials
tutorials: false,
// Disable notifications about new Strapi releases
notifications: { releases: false }
},
bootstrap() {}
};

5
src/admin/tsconfig.json Normal file
View File

@@ -0,0 +1,5 @@
{
"extends": "@strapi/typescript-utils/tsconfigs/admin",
"include": ["../plugins/**/admin/src/**/*", "./"],
"exclude": ["node_modules/", "build/", "dist/", "**/*.test.ts"]
}

0
src/api/.gitkeep Normal file
View File

View File

@@ -0,0 +1,28 @@
{
"kind": "collectionType",
"collectionName": "customers",
"info": {
"singularName": "customer",
"pluralName": "customers",
"displayName": "Customer",
"description": ""
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string",
"required": true
},
"address": {
"type": "text"
},
"addressStructured": {
"type": "component",
"component": "address.structured-address",
"required": false
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* customer controller
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreController("api::customer.customer");

View File

@@ -0,0 +1,508 @@
{
"/customers": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CustomerListResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Customer"
],
"parameters": [
{
"name": "sort",
"in": "query",
"description": "Sort by attributes ascending (asc) or descending (desc)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "pagination[withCount]",
"in": "query",
"description": "Return page/pageSize (default: true)",
"deprecated": false,
"required": false,
"schema": {
"type": "boolean"
}
},
{
"name": "pagination[page]",
"in": "query",
"description": "Page number (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[pageSize]",
"in": "query",
"description": "Page size (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[start]",
"in": "query",
"description": "Offset value (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[limit]",
"in": "query",
"description": "Number of entities to return (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "fields",
"in": "query",
"description": "Fields to return (ex: title,author)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "populate",
"in": "query",
"description": "Relations to return",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "filters",
"in": "query",
"description": "Filters to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "object",
"additionalProperties": true
},
"style": "deepObject"
},
{
"name": "locale",
"in": "query",
"description": "Locale to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
}
],
"operationId": "get/customers"
},
"post": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CustomerResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Customer"
],
"parameters": [],
"operationId": "post/customers",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CustomerRequest"
}
}
}
}
}
},
"/customers/{id}": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CustomerResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Customer"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "get/customers/{id}"
},
"put": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CustomerResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Customer"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "put/customers/{id}",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CustomerRequest"
}
}
}
}
},
"delete": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "integer",
"format": "int64"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Customer"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "delete/customers/{id}"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* customer router
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreRouter("api::customer.customer");

View File

@@ -0,0 +1,108 @@
import { factories } from "@strapi/strapi";
import { ID } from "@strapi/database/dist/types";
import { StructuredAddress } from "../../../../types";
// Define a detailed type for customer creation
interface CustomerCreateData {
name: string;
address: string;
email?: string;
}
export default factories.createCoreService("api::customer.customer", ({ strapi }) => ({
/**
* Extract customer name from address
* @param address Full address string
* @returns Object with name and cleaned address
*/
extractCustomerDetails(address: string): { name: string; cleanedAddress: string } {
// If address is empty or null, use default values
if (!address || address.trim() === "") {
return {
name: "Unknown Customer",
cleanedAddress: address || ""
};
}
// Split address by newline and take the first line as name
const addressLines = address.split("\n").map((line) => line.trim());
const name = addressLines[0] || "Unknown Customer";
// Remove the first line to get the cleaned address
const cleanedAddress = addressLines.slice(1).join("\n").trim();
return {
name,
cleanedAddress: cleanedAddress || address
};
},
/**
* Find or create a customer based on address
* @param address Full address string
* @param structuredAddress Optional structured address object
* @returns Customer ID
*/
/**
* Create or update customer with both legacy and structured address
*/
async findOrCreateCustomer(address: string, structuredAddress?: StructuredAddress): Promise<ID> {
try {
// Extract name and cleaned address
const { name, cleanedAddress } = this.extractCustomerDetails(address);
// Try to find by structured address first, then fallback to legacy
let existingCustomers = [];
if (structuredAddress) {
existingCustomers = await strapi.entityService.findMany("api::customer.customer", {
filters: {
$or: [
{
addressStructured: {
streetAddress: structuredAddress.streetAddress,
postalCode: structuredAddress.postalCode,
addressLevel2: structuredAddress.addressLevel2
}
},
{ address: cleanedAddress }
]
}
});
} else {
existingCustomers = await strapi.entityService.findMany("api::customer.customer", {
filters: { address: cleanedAddress }
});
}
if (existingCustomers && existingCustomers.length > 0) {
const customer = existingCustomers[0];
// Update with structured data if provided
if (structuredAddress) {
await strapi.entityService.update("api::customer.customer", customer.id, {
data: {
addressStructured: structuredAddress
}
});
}
return customer.id;
}
// Create new customer with both formats
const newCustomer = await strapi.entityService.create("api::customer.customer", {
data: {
name,
address: cleanedAddress,
...(structuredAddress && { addressStructured: structuredAddress })
}
});
return newCustomer.id;
} catch (error) {
strapi.log.error("app:e:customer-service: Error managing customer", { error, address });
throw error;
}
}
}));

View File

@@ -0,0 +1,27 @@
{
"kind": "collectionType",
"collectionName": "deliveries",
"info": {
"singularName": "delivery",
"pluralName": "deliveries",
"displayName": "Delivery",
"description": ""
},
"options": {
"privateAttributes": ["created_at", "updated_at", "created_by", "updated_by", "createdAt", "updatedAt"],
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string",
"required": true
},
"price": {
"type": "decimal"
},
"description": {
"type": "text"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* delivery controller
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreController("api::delivery.delivery");

View File

@@ -0,0 +1,508 @@
{
"/deliveries": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeliveryListResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Delivery"
],
"parameters": [
{
"name": "sort",
"in": "query",
"description": "Sort by attributes ascending (asc) or descending (desc)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "pagination[withCount]",
"in": "query",
"description": "Return page/pageSize (default: true)",
"deprecated": false,
"required": false,
"schema": {
"type": "boolean"
}
},
{
"name": "pagination[page]",
"in": "query",
"description": "Page number (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[pageSize]",
"in": "query",
"description": "Page size (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[start]",
"in": "query",
"description": "Offset value (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[limit]",
"in": "query",
"description": "Number of entities to return (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "fields",
"in": "query",
"description": "Fields to return (ex: title,author)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "populate",
"in": "query",
"description": "Relations to return",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "filters",
"in": "query",
"description": "Filters to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "object",
"additionalProperties": true
},
"style": "deepObject"
},
{
"name": "locale",
"in": "query",
"description": "Locale to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
}
],
"operationId": "get/deliveries"
},
"post": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeliveryResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Delivery"
],
"parameters": [],
"operationId": "post/deliveries",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeliveryRequest"
}
}
}
}
}
},
"/deliveries/{id}": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeliveryResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Delivery"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "get/deliveries/{id}"
},
"put": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeliveryResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Delivery"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "put/deliveries/{id}",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeliveryRequest"
}
}
}
}
},
"delete": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "integer",
"format": "int64"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Delivery"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "delete/deliveries/{id}"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* delivery router
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreRouter("api::delivery.delivery");

View File

@@ -0,0 +1,7 @@
/**
* delivery service
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreService("api::delivery.delivery");

View File

@@ -0,0 +1,35 @@
{
"kind": "singleType",
"collectionName": "legals",
"info": {
"singularName": "legal",
"pluralName": "legals",
"displayName": "Legal",
"description": ""
},
"options": {
"privateAttributes": ["created_at", "updated_at", "created_by", "updated_by"],
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"imprint": {
"type": "richtext"
},
"contact": {
"type": "richtext"
},
"terms": {
"type": "richtext"
},
"delivery": {
"type": "richtext"
},
"payment": {
"type": "richtext"
},
"privacy": {
"type": "richtext"
}
}
}

View File

@@ -0,0 +1,38 @@
/**
* legal controller
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreController("api::legal.legal", ({ strapi }) => ({
/**
* Get all legal page paths for sitemap generation
* @returns {Promise<string[]>} Array of legal page paths
*/
sitemap: async (ctx): Promise<string[]> => {
try {
strapi.log.verbose("Generating legal sitemap");
const legal = await strapi.entityService.findMany("api::legal.legal");
if (!legal) {
return [];
}
strapi.log.silly(`Legal pages response: ${JSON.stringify(legal)}`);
const internalFields = ["id", "createdAt", "updatedAt", "createdBy", "updatedBy"];
const legalPages = Object.keys(legal).filter((key) => !internalFields.includes(key));
const legalPaths = legalPages.map((page) => `/legal/${page}`);
strapi.log.verbose(`Generated legal sitemap with ${legalPaths.length} entries`);
strapi.log.silly(`Legal sitemap: ${JSON.stringify(legalPaths)}`);
return legalPaths;
} catch (error) {
strapi.log.error(error);
return ctx.badRequest("Could not generate legal sitemap");
}
}
}));

View File

@@ -0,0 +1,325 @@
{
"/legal": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegalResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Legal"
],
"parameters": [
{
"name": "sort",
"in": "query",
"description": "Sort by attributes ascending (asc) or descending (desc)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "pagination[withCount]",
"in": "query",
"description": "Return page/pageSize (default: true)",
"deprecated": false,
"required": false,
"schema": {
"type": "boolean"
}
},
{
"name": "pagination[page]",
"in": "query",
"description": "Page number (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[pageSize]",
"in": "query",
"description": "Page size (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[start]",
"in": "query",
"description": "Offset value (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[limit]",
"in": "query",
"description": "Number of entities to return (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "fields",
"in": "query",
"description": "Fields to return (ex: title,author)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "populate",
"in": "query",
"description": "Relations to return",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "filters",
"in": "query",
"description": "Filters to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "object",
"additionalProperties": true
},
"style": "deepObject"
},
{
"name": "locale",
"in": "query",
"description": "Locale to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
}
],
"operationId": "get/legal"
},
"put": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegalResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Legal"
],
"parameters": [],
"operationId": "put/legal",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LegalRequest"
}
}
}
}
},
"delete": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "integer",
"format": "int64"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Legal"
],
"parameters": [],
"operationId": "delete/legal"
}
}
}

View File

@@ -0,0 +1,13 @@
export default {
routes: [
{
method: "GET",
path: "/sitemap/legal",
handler: "legal.sitemap",
config: {
auth: false,
policies: []
}
}
]
};

View File

@@ -0,0 +1,7 @@
/**
* legal router
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreRouter("api::legal.legal");

View File

@@ -0,0 +1,7 @@
/**
* legal service
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreService("api::legal.legal");

View File

@@ -0,0 +1,170 @@
import { Order } from "../../../../../types";
import { ID } from "@strapi/database/dist/types";
import { v4 as uuidv4 } from "uuid";
// In-memory processing state tracker
// Using order UUIDs as keys to track which orders are currently being processed
const processingOrders = new Map<
string,
{
invoiceInProgress: boolean;
lastProcessingStarted: number;
}
>();
// Cleanup function to prevent memory leaks
const cleanupStaleProcessingEntries = () => {
const now = Date.now();
const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes timeout
for (const [uuid, state] of processingOrders.entries()) {
if (now - state.lastProcessingStarted > TIMEOUT_MS) {
processingOrders.delete(uuid);
strapi.log.verbose(`app:v:order-lifecycles: Cleaned up stale processing state for order ${uuid}`);
}
}
};
export default {
beforeCreate(event: { params: { data: { uuid: any } } }) {
// Generate UUID for new orders
if (!event.params.data.uuid) {
event.params.data.uuid = uuidv4();
}
},
async beforeUpdate(event: {
params: {
data: Order;
where: { id: ID };
};
}) {
const { data, where } = event.params;
const orderId = where.id;
strapi.log.debug(`app:d:order-lifecycles: Before order update ${JSON.stringify({ orderId, data })}`);
// Track what changed for smarter processing
const changes = {
paymentAuthorised: "paymentAuthorised" in data,
addressUpdated: "invoiceAddress" in data
};
// Handle address update and customer assignment
if (changes.addressUpdated && data.invoiceAddress) {
try {
const customerService = strapi.service("api::customer.customer");
// Find or create customer using the address string only
// The structured address component will be handled by the controller
const customer = await customerService.findOrCreateCustomer(data.invoiceAddress);
strapi.log.verbose(
"app:d:order-lifecycles: Attaching customer to order",
JSON.stringify({
order: orderId,
customer
})
);
// Attach customer to order
data.customer = customer;
// Generate invoice and delivery number in format R-[customer.id]-[order.id]
// Only generate if they don't already exist
if (!data.invoiceNumber) {
data.invoiceNumber = `R-${customer}-${orderId}`;
}
if (!data.deliveryNoteNumber) {
data.deliveryNoteNumber = `L-${customer}-${orderId}`;
}
strapi.log.debug("app:d:order-lifecycles: Generated invoice number", {
invoiceNumber: data.invoiceNumber,
deliveryNoteNumber: data.deliveryNoteNumber,
order: orderId
});
} catch (error) {
strapi.log.error("app:e:order-lifecycles: Error in beforeUpdate", {
error
});
throw error;
}
}
},
async afterCreate(event: { result: Order }) {
const { result } = event;
strapi.log.verbose(
"app:v:order-lifecycles: Order created",
JSON.stringify({
id: result.id,
uuid: result.uuid
})
);
},
async afterUpdate(event: { result: Order }) {
const { result } = event;
// Periodically clean up stale processing entries
cleanupStaleProcessingEntries();
// Log order update with relevant fields
strapi.log.debug("app:d:order-lifecycles: ", {
order: {
id: result.id,
uuid: result.uuid,
paymentAuthorised: result.paymentAuthorised,
invoice: result.invoice,
invoiceSent: result.invoiceSent
}
});
// Check processing conditions
const shouldProcessInvoice = result.paymentAuthorised && !result.invoiceSent;
if (!shouldProcessInvoice) {
return; // Early exit if conditions aren't met
}
// Get order UUID to track processing state
const orderUUID = result.uuid;
// Check if this order is already being processed
const processingState = processingOrders.get(orderUUID);
if (processingState && processingState.invoiceInProgress) {
strapi.log.verbose("app:v:order-lifecycles: Skipping duplicate invoice generation", {
orderId: result.id,
uuid: orderUUID,
processingStarted: new Date(processingState.lastProcessingStarted).toISOString()
});
return;
}
// Mark this order as being processed
processingOrders.set(orderUUID, {
invoiceInProgress: true,
lastProcessingStarted: Date.now()
});
try {
strapi.log.info("app:i:order-lifecycles: Starting invoice generation", { orderId: result.id, uuid: orderUUID });
// Generate and upload the invoice PDF
const order = await strapi.service("api::order.order").uploadPdf(result, "invoice", "Rechnung");
// Send the invoice email
await strapi.service("api::order.order").sendInvoice(orderUUID);
strapi.log.info("app:i:order-lifecycles: Successfully processed invoice for order", { orderId: result.id, uuid: orderUUID });
} catch (error) {
strapi.log.error("app:e:order-lifecycles: Error processing invoice", { error, orderId: result.id, uuid: orderUUID });
// Don't rethrow to prevent transaction failure
} finally {
// Remove the processing marker once complete
processingOrders.delete(orderUUID);
}
}
};

View File

@@ -0,0 +1,128 @@
{
"kind": "collectionType",
"collectionName": "orders",
"info": {
"singularName": "order",
"pluralName": "orders",
"displayName": "Order",
"description": ""
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"date": {
"type": "date"
},
"customer": {
"type": "relation",
"relation": "oneToOne",
"target": "api::customer.customer"
},
"invoice": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"files"
]
},
"deliveryNote": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images",
"files",
"videos",
"audios"
]
},
"hash": {
"type": "text",
"unique": true,
"private": true
},
"email": {
"type": "string"
},
"delivery": {
"type": "relation",
"relation": "oneToOne",
"target": "api::delivery.delivery"
},
"payment": {
"type": "relation",
"relation": "oneToOne",
"target": "api::payment.payment"
},
"VAT": {
"type": "decimal"
},
"subtotal": {
"type": "decimal"
},
"total": {
"type": "decimal"
},
"uuid": {
"type": "string",
"unique": true
},
"cart": {
"displayName": "cart",
"type": "component",
"repeatable": true,
"component": "products.cart"
},
"paymentAuthorised": {
"type": "boolean",
"default": false
},
"paymentStatus": {
"type": "string"
},
"emailSent": {
"type": "boolean",
"default": false
},
"acceptedTermsAndConditionsAt": {
"type": "datetime"
},
"invoiceSent": {
"type": "boolean",
"default": false
},
"deliveryNoteSent": {
"type": "boolean"
},
"invoiceNumber": {
"type": "string",
"unique": true
},
"deliveryNoteNumber": {
"type": "string",
"unique": false
},
"deliveryTrackingNumber": {
"type": "string"
},
"deliveryAddress": {
"type": "text"
},
"invoiceAddress": {
"type": "text"
},
"invoiceAddressStructured": {
"type": "component",
"component": "address.structured-address",
"required": false
},
"deliveryAddressStructured": {
"type": "component",
"component": "address.structured-address",
"required": false
}
}
}

View File

@@ -0,0 +1,307 @@
import { factories } from "@strapi/strapi";
import { sanitize } from "@strapi/utils";
import { Order } from "../../../../types";
import { paypalApi } from "../../../services/PayPalApi";
import { orderDefaultParams } from "../services/order";
import { calculateTotalProductPrice } from "../../product/services/product";
/**
* The order controller is a funnel for the client to interact with the order service.
* From creating until finalizing an order, the controller is responsible for handling
* the business logic.
*
* ---------------------------------------
* INIT
* ---------------------------------------
* 1. Create order
* ---------------------------------------
* PRE-ORDER (must have order uuid)
* ---------------------------------------
* 1.1. Find order
* 2. Add/remove product from order
* ---------------------------------------
* ORDER VALIDATION (must have at least one product)
* ---------------------------------------
* 3. Update order
* 3.1. Contact info (email)
* 3.2. Confirm terms and conditions
* 3.3. Address (invoice, delivery)
* 3.4. Delivery method
* ---------------------------------------
* ORDER PAYMENT (order must include all above properties)
* ---------------------------------------
* 4. Checkout order (create adyen payment session)
* ---------------------------------------
* POST-ORDER (payment must be authorised)
* ---------------------------------------
* 5. Finalize order (generate invoice, delivery note)
* 6. Send invoice and delivery note via email
*/
export default factories.createCoreController("api::order.order", ({ strapi }) => ({
create: async (_) => {
const order = await strapi.entityService.create("api::order.order", { data: {} });
strapi.log.debug(`app:d:order-controller: Order created: ${JSON.stringify({ order })}`);
return await sanitize.contentAPI.output(order, strapi.getModel("api::order.order"));
},
update: async (ctx) => {
const { id } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
strapi.log.verbose(`app:v:order-controller: - Updating order ID${id}`, orderDefaultParams);
const order = await strapi.db.query("api::order.order").findOne({ where: { id }, ...orderDefaultParams });
if (!order) return ctx.notFound("Order not found");
const sanitizedBody = await sanitize.contentAPI.input(ctx.request.body, strapi.getModel("api::order.order"));
strapi.log.verbose("app:v:order-controller: - Updating order", { sanitizedBody });
const updatedOrder = await strapi.entityService.update("api::order.order", order.id, {
// @ts-expect-error
data: sanitizedBody?.data,
...orderDefaultParams
});
strapi.log.verbose("app:v:order-controller: ✔ Updating order");
return await sanitize.contentAPI.output(updatedOrder, strapi.getModel("api::order.order"));
},
findOne: async (ctx) => {
const { id } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
const order: Order = await strapi.db.query("api::order.order").findOne({
where: { id },
...orderDefaultParams
});
order.cart = order?.cart.map((item) => ({
...item,
product: {
...item.product,
totalProductPrice: calculateTotalProductPrice(item.product)
}
}));
if (!order) return ctx.notFound("Order not found");
return await sanitize.contentAPI.output(order, strapi.getModel("api::order.order"));
},
findOneByUuid: async (ctx) => {
const { uuid } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
const order: Order = await strapi.db.query("api::order.order").findOne({
where: { uuid },
...orderDefaultParams
});
order.cart = order?.cart.map((item) => ({
...item,
product: {
...item.product,
totalProductPrice: calculateTotalProductPrice(item.product)
}
}));
if (!order) return ctx.notFound("Order not found");
return await sanitize.contentAPI.output(order, strapi.getModel("api::order.order"));
},
updateByUuid: async (ctx) => {
const { uuid } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
const order = await strapi.db.query("api::order.order").findOne({ where: { uuid } });
if (!order) return ctx.notFound("Order not found");
const sanitizedBody = await sanitize.contentAPI.input(ctx.request.body, strapi.getModel("api::order.order"));
// Handle customer creation/update if structured addresses are provided
// @ts-expect-error
if (sanitizedBody?.data?.invoiceAddressStructured) {
const customerAddress =
// @ts-expect-error
sanitizedBody.data.invoiceAddress ||
// @ts-expect-error
`${sanitizedBody.data.invoiceAddressStructured.givenName} ${sanitizedBody.data.invoiceAddressStructured.familyName}\n${sanitizedBody.data.invoiceAddressStructured.streetAddress}\n${sanitizedBody.data.invoiceAddressStructured.postalCode} ${sanitizedBody.data.invoiceAddressStructured.addressLevel2}`;
// @ts-expect-error
sanitizedBody.data.customer = await strapi
.service("api::customer.customer")
// @ts-expect-error
.findOrCreateCustomer(customerAddress, sanitizedBody.data.invoiceAddressStructured);
}
const orderUnsafe = await strapi.service("api::order.order").update(order.id, {
// @ts-expect-error
data: sanitizedBody?.data,
...orderDefaultParams
});
return await sanitize.contentAPI.output(orderUnsafe, strapi.getModel("api::order.order"));
},
addProduct: async (ctx) => {
const { uuid, productId } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
const order = (await strapi.service("api::order.order").findOneByUuid(uuid)) as Order;
if (!order) return ctx.notFound("Order not found");
const product = parseInt(productId as string);
const count = parseInt(ctx.query.count as string) || 1;
const cart = order.cart || [];
const existingIndex = cart.findIndex((item) => item.product.id === product);
if (existingIndex !== -1) {
// Product already in cart
cart[existingIndex].count += count;
} else {
// @ts-expect-error
cart.push({ count, product });
}
strapi.log.info(`app:v:order-controller: - Adding product to cart ${order.id}`, { totalBefore: order.total });
const orderUnsafe = await strapi.service("api::order.order").update(order.id, {
data: { cart: cart },
...orderDefaultParams
});
strapi.log.info("app:v:order-controller: ✔ Adding product to cart", { totalAfter: orderUnsafe.total });
return await sanitize.contentAPI.output(orderUnsafe, strapi.getModel("api::order.order"));
},
removeProduct: async (ctx) => {
const { uuid, productId } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
const count = parseInt(ctx.query.count as string) || 1;
const order = (await strapi.service("api::order.order").findOneByUuid(uuid)) as Order;
const product = parseInt(productId as string);
if (!order) return ctx.notFound("Order not found");
strapi.log.info("app:v:order-controller: - Removing product from cart", { totalBefore: order.total });
const updatedCart = [...(order.cart || [])];
let existingIndex = -1;
if (typeof productId === "string") {
existingIndex = updatedCart.findIndex((item) => item.product.id === parseInt(productId));
}
if (existingIndex === -1) return ctx.badRequest("Product not found in cart");
if (updatedCart[existingIndex].count <= count) {
updatedCart.splice(existingIndex, 1);
} else {
updatedCart[existingIndex].count -= count;
}
const orderUnsafe = await strapi.service("api::order.order").update(order.id, {
data: { cart: updatedCart },
...orderDefaultParams
});
strapi.log.info("app:v:order-controller: ✔ Removing product from cart", { totalAfter: order.total });
return await sanitize.contentAPI.output(orderUnsafe, strapi.getModel("api::order.order"));
},
checkout: async (ctx) => {
const { uuid } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
const { returnUrl } = await sanitize.contentAPI.query(ctx.query, strapi.getModel("api::order.order"));
const order = await strapi.db.query("api::order.order").findOne({
where: { uuid },
...orderDefaultParams
});
if (!order) return ctx.notFound("Order not found");
return await paypalApi.createSessionOrThrow(returnUrl as string, order);
},
capturePayment: async (ctx) => {
const { uuid, paypalOrderId } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
strapi.log.info(`app:i:order-controller: Capturing payment for order ${uuid} with PayPal order ${paypalOrderId}`);
const order = await strapi.db.query("api::order.order").findOne({
where: { uuid },
...orderDefaultParams
});
if (!order) return ctx.notFound("Order not found");
if (order.paymentAuthorised) {
strapi.log.info(`app:i:order-controller: Payment already authorised for order ${uuid}`);
return { success: true, alreadyCaptured: true };
}
try {
// Capture the payment via PayPal server SDK
const captureResult = await paypalApi.checkPaymentStatus({ orderID: paypalOrderId as string });
if (captureResult.status === "COMPLETED") {
// Update order with payment info
const updatedOrder = await strapi.entityService.update("api::order.order", order.id, {
data: {
paymentAuthorised: true,
paymentStatus: "authorised",
paypalOrderId: paypalOrderId as string
},
...orderDefaultParams
});
strapi.log.info(`app:i:order-controller: Payment captured successfully for order ${uuid}`);
return await sanitize.contentAPI.output(updatedOrder, strapi.getModel("api::order.order"));
} else {
strapi.log.error(`app:e:order-controller: Payment capture failed for order ${uuid}`, { captureResult });
return ctx.badRequest("Payment capture failed", { status: captureResult.status });
}
} catch (error) {
strapi.log.error(`app:e:order-controller: Error capturing payment for order ${uuid}`, { error });
return ctx.internalServerError("Payment capture failed");
}
},
generateInvoice: async (ctx) => {
const { uuid } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
strapi.log.info(`app:i:order-controller: Started invoice generation`);
const orderUnsafe = (await strapi.service("api::order.order").findOneByUuid(uuid)) as Order;
if (!orderUnsafe.paymentAuthorised) {
return ctx.locked("Payment not authorised, cannot generate invoice");
}
try {
const updatedOrderUnsafe = await strapi.service("api::order.order").uploadPdf(orderUnsafe, "invoice", "Rechnung");
return await sanitize.contentAPI.output(updatedOrderUnsafe, strapi.getModel("api::order.order"));
} catch (error) {
strapi.log.error("app:e:order-controller: Error uploading delivery note pdf", { error });
return ctx.internalServerError("Could not upload delivery note");
}
},
generateDeliveryNote: async (ctx) => {
const { uuid } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
strapi.log.info(`app:i:order-controller: Started delivery note generation`);
const orderUnsafe = (await strapi.service("api::order.order").findOneByUuid(uuid)) as Order;
if (!orderUnsafe.paymentAuthorised) {
return ctx.locked("Payment not authorised, cannot generate delivery note");
}
try {
const updatedOrderUnsafe = await strapi.service("api::order.order").uploadPdf(orderUnsafe, "deliveryNote", "Lieferschein");
return await sanitize.contentAPI.output(updatedOrderUnsafe, strapi.getModel("api::order.order"));
} catch (error) {
strapi.log.error("app:e:order-controller: Error uploading delivery note pdf", { error });
return ctx.internalServerError("Could not upload delivery note");
}
},
sendInvoice: async (ctx) => {
const { uuid } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
strapi.log.info("app:i:order-controller: Sending invoice for order", { uuid });
try {
const updatedOrderUnsafe = await strapi.service("api::order.order").sendInvoice(uuid, undefined);
return await sanitize.contentAPI.output(updatedOrderUnsafe, strapi.getModel("api::order.order"));
} catch (error) {
strapi.log.error("app:e:order-controller: Error sending invoice", { error });
return ctx.internalServerError("Could not send invoice");
}
},
sendDeliveryNote: async (ctx) => {
const { uuid } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::order.order"));
strapi.log.info("app:i:order-controller: Sending delivery note for", { uuid });
return await strapi.service("api::order.order").sendDeliveryNote(uuid, undefined);
},
webhook: async (ctx) => {
try {
return await paypalApi.handleWebhook(ctx.request.body);
} catch (error) {
strapi.log.error(`paypal-api: ${JSON.stringify({ error })}`);
return ctx.internalServerError();
}
}
}));

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,124 @@
export default {
routes: [
{
method: "POST",
path: "/orders",
handler: "order.create",
config: {
policies: []
}
},
{
method: "POST",
path: "/orders/webhook",
handler: "order.webhook",
config: {
policies: []
}
},
{
method: "GET",
path: "/orders",
handler: "order.find",
config: {
policies: []
}
},
{
method: "GET",
path: "/orders/:id",
handler: "order.findOne",
config: {
policies: []
}
},
{
method: "GET",
path: "/orders/:uuid/cart",
handler: "order.findOneByUuid",
config: {
policies: []
}
},
{
method: "PUT",
path: "/orders/:id",
handler: "order.update",
config: {
policies: []
}
},
{
method: "PUT",
path: "/orders/:uuid/cart",
handler: "order.updateByUuid",
config: {
policies: []
}
},
{
method: "PUT",
path: "/orders/:uuid/add-product/:productId",
handler: "order.addProduct",
config: {
policies: []
}
},
{
method: "PUT",
path: "/orders/:uuid/remove-product/:productId",
handler: "order.removeProduct",
config: {
policies: []
}
},
{
method: "POST",
path: "/orders/:uuid/checkout",
handler: "order.checkout",
config: {
policies: []
}
},
{
method: "POST",
path: "/orders/:uuid/capture/:paypalOrderId",
handler: "order.capturePayment",
config: {
policies: []
}
},
{
method: "PUT",
path: "/orders/:uuid/generate-invoice",
handler: "order.generateInvoice",
config: {
policies: []
}
},
{
method: "PUT",
path: "/orders/:uuid/generate-delivery-note",
handler: "order.generateDeliveryNote",
config: {
policies: []
}
},
{
method: "PUT",
path: "/orders/:uuid/send-invoice",
handler: "order.sendInvoice",
config: {
policies: []
}
},
{
method: "PUT",
path: "/orders/:uuid/send-delivery-note",
handler: "order.sendDeliveryNote",
config: {
policies: []
}
}
]
};

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
});

View File

@@ -0,0 +1,26 @@
{
"kind": "collectionType",
"collectionName": "payments",
"info": {
"singularName": "payment",
"pluralName": "payments",
"displayName": "Payment",
"description": ""
},
"options": {
"privateAttributes": ["created_at", "updated_at", "created_by", "updated_by", "createdAt", "updatedAt"],
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string"
},
"price": {
"type": "decimal"
},
"description": {
"type": "text"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* payment controller
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreController("api::payment.payment");

View File

@@ -0,0 +1,508 @@
{
"/payments": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentListResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Payment"
],
"parameters": [
{
"name": "sort",
"in": "query",
"description": "Sort by attributes ascending (asc) or descending (desc)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "pagination[withCount]",
"in": "query",
"description": "Return page/pageSize (default: true)",
"deprecated": false,
"required": false,
"schema": {
"type": "boolean"
}
},
{
"name": "pagination[page]",
"in": "query",
"description": "Page number (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[pageSize]",
"in": "query",
"description": "Page size (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[start]",
"in": "query",
"description": "Offset value (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[limit]",
"in": "query",
"description": "Number of entities to return (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "fields",
"in": "query",
"description": "Fields to return (ex: title,author)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "populate",
"in": "query",
"description": "Relations to return",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "filters",
"in": "query",
"description": "Filters to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "object",
"additionalProperties": true
},
"style": "deepObject"
},
{
"name": "locale",
"in": "query",
"description": "Locale to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
}
],
"operationId": "get/payments"
},
"post": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Payment"
],
"parameters": [],
"operationId": "post/payments",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRequest"
}
}
}
}
}
},
"/payments/{id}": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Payment"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "get/payments/{id}"
},
"put": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Payment"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "put/payments/{id}",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRequest"
}
}
}
}
},
"delete": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "integer",
"format": "int64"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Payment"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "delete/payments/{id}"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* payment router
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreRouter("api::payment.payment");

View File

@@ -0,0 +1,7 @@
/**
* payment service
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreService("api::payment.payment");

View File

@@ -0,0 +1,213 @@
/**
* Product category controller
*/
import { ProductCategory, ProductImage } from "../../../../types";
export default () => ({
// Get all categories
categories: async (ctx, next) => {
try {
ctx.body = await strapi.service("api::product-category.product-category").categories();
} catch (err) {
ctx.badRequest("Product category controller error", { moreDetails: err });
}
},
corruptCategories: async (ctx, next) => {
try {
const categories = await strapi.service("api::product-category.product-category").categories();
const corruptedCategories = categories.filter((category) => !category?.product_cover || !category?.product_pattern);
strapi.log.info(`app:i:product-category-controller: Found ${corruptedCategories?.length} categories with missing cover or pattern`);
strapi.log.silly("app:d:product-category-controller:", { corruptedCategories });
ctx.body = corruptedCategories;
} catch (err) {
ctx.badRequest("Product category controller error", { moreDetails: err });
}
},
// Get single category
category: async (ctx, next) => {
try {
const { id } = ctx.params;
ctx.body = await strapi.service("api::product-category.product-category").category(id);
} catch (err) {
ctx.badRequest("Product category controller error", { moreDetails: err });
}
},
// Update single category
updateCategory: async (ctx, next) => {
try {
const { id } = ctx.params;
const data = ctx.request.body;
ctx.body = await strapi.service("api::product-category.product-category").updateCategory(id, data);
} catch (err) {
ctx.badRequest("Product category controller error", { moreDetails: err });
}
},
// Update multiple categories
updateCategories: async (ctx, next) => {
try {
const { where, data } = ctx.request.body;
if (!where || !data) {
return ctx.badRequest("Request body must include 'where' and 'data' properties");
}
ctx.body = await strapi.service("api::product-category.product-category").updateCategories({ where, data });
} catch (err) {
ctx.badRequest("Product category controller error", { moreDetails: err });
}
},
fixCategories: async (ctx, next) => {
try {
// find categories with empty cover or pattern
const categories: ProductCategory[] = await strapi.service("api::product-category.product-category").categories({
filters: {
$or: [{ product_cover: null }, { product_pattern: null }]
},
populate: {
product_cover: true,
product_pattern: true
}
});
strapi.log.info(`app:i:product-category-controller: Found ${categories?.length ?? 0} categories with missing cover or pattern`);
strapi.log.silly("app:d:product-category-controller:", { categories });
const result = {
fixed: [],
failed: []
};
// fix categories with empty cover or pattern by name, e.g. "Notizheft Geometrische Muster #03"
for (const category of categories) {
const [coverName, patternName] = (category.name as string).split(" ");
const patterns = await strapi.entityService.findMany("api::product-pattern.product-pattern", {
filters: {
name: patternName
}
});
const coverNameChunks = coverName.split(" ");
const covers = await strapi.entityService.findMany("api::product-cover.product-cover", {
filters: {
$or: coverNameChunks.map(($containsi: string) => ({ name: { $containsi } }))
}
});
const pattern = patterns.pop();
const cover = covers.pop();
strapi.log.silly("app:d:product-category-controller:", { cover, pattern });
if (!pattern || !cover) {
result.failed.push(category);
strapi.log.warn(`app:w:product-category-controller: Could not find pattern or cover for category ${category?.id}`);
continue;
}
const updatedCategory: ProductCategory = await strapi.service("api::product-category.product-category").updateCategory(category?.id, {
product_pattern: category?.product_pattern ?? pattern?.id,
product_cover: category?.product_cover ?? cover?.id
});
result.fixed.push(updatedCategory);
strapi.log.info(`app:i:product-category-controller: Updated category ${category?.id} with pattern ${pattern?.id}`);
strapi.log.silly("app:d:product-category-controller:", { updatedCategory });
}
strapi.log.info(
`app:i:product-category-controller: Fixed ${result.fixed.length} / Failed ${result.failed.length} categories with missing cover or pattern`
);
ctx.body = result;
} catch (err) {
ctx.badRequest("Product category controller error", { moreDetails: err });
}
},
fixProductImages: async (ctx, next) => {
try {
// Find categories/product-images that have no products but have both cover and pattern
const categories = await strapi.service("api::product-category.product-category").categories({
populate: {
products: true,
product_cover: true,
product_pattern: true
},
filters: {
products: { $eq: null },
product_cover: { $ne: null },
product_pattern: { $ne: null }
}
});
const result = {
fixed: [],
failed: []
};
strapi.log.info(`app:i:product-category-controller: Found ${categories?.length ?? 0} categories with missing products`);
strapi.log.silly("app:d:product-category-controller:", { categories });
for (const category of categories) {
try {
const coverId = category.product_cover?.id;
const patternId = category.product_pattern?.id;
if (!coverId || !patternId) {
result.failed.push({
categoryId: category.id,
reason: "Missing cover or pattern ID"
});
continue;
}
// Find all products that match both the cover and pattern IDs
const matchingProducts = await strapi.entityService.findMany("api::product.product", {
filters: {
$and: [{ cover: { id: coverId } }, { pattern: { id: patternId } }]
}
});
if (!matchingProducts?.length) {
result.failed.push({
categoryId: category.id,
coverId,
patternId,
reason: "No matching products found"
});
continue;
}
// Update the category with the found products
const updatedCategory = await strapi.service("api::product-category.product-category").updateCategory(category.id, {
products: matchingProducts.map((product) => product.id)
});
result.fixed.push({
categoryId: category.id,
productCount: matchingProducts.length,
products: matchingProducts.map((p) => p.id)
});
strapi.log.info(`app:i:product-category-controller: Updated category ${category.id} with ${matchingProducts.length} products`);
strapi.log.silly("app:d:product-category-controller: Updated category details:", {
categoryId: category.id,
products: matchingProducts.map((p) => p.id)
});
} catch (categoryError) {
result.failed.push({
categoryId: category.id,
reason: "Error processing category",
error: categoryError.message
});
strapi.log.error(`app:e:product-category-controller: Error processing category ${category.id}:`, categoryError);
}
}
strapi.log.info(`app:i:product-category-controller: Fixed ${result.fixed.length} / Failed ${result.failed.length} categories`);
ctx.body = result;
} catch (err) {
ctx.badRequest("Product category controller error", { moreDetails: err });
}
}
});

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,73 @@
export default {
routes: [
// Get all categories
{
method: "GET",
path: "/product-category",
handler: "product-category.categories",
config: {
policies: [],
middlewares: []
}
},
// Get single category
{
method: "GET",
path: "/product-category/:id",
handler: "product-category.category",
config: {
policies: [],
middlewares: []
}
},
{
method: "GET",
path: "/product-category/corrupt",
handler: "product-category.corruptCategories",
config: {
policies: [],
middlewares: []
}
},
// Bulk update categories
{
method: "PUT",
path: "/product-category/bulk",
handler: "product-category.updateCategories",
config: {
policies: [],
middlewares: []
}
},
// Bulk fix categories
{
method: "PUT",
path: "/product-category/fix",
handler: "product-category.fixCategories",
config: {
policies: [],
middlewares: []
}
},
// Bulk fix product images
{
method: "PUT",
path: "/product-category/fix-images",
handler: "product-category.fixProductImages",
config: {
policies: [],
middlewares: []
}
},
// Update single category
{
method: "PUT",
path: "/product-category/:id",
handler: "product-category.updateCategory",
config: {
policies: [],
middlewares: []
}
}
]
};

View File

@@ -0,0 +1,80 @@
/**
* Extended product-category service
*/
interface CategoryUpdate {
where: any;
data: any;
}
export default () => ({
// Get all categories
categories: async (params?: Record<string, any>) => {
try {
return await strapi.entityService.findMany("api::product-image.product-image", {
populate: {
product_cover: true,
product_pattern: true,
images: true
},
...params
});
} catch (error) {
return error;
}
},
// Get single category by ID
category: async (id: number) => {
try {
return await strapi.entityService.findOne("api::product-image.product-image", id, {
populate: {
product_cover: true,
product_pattern: true,
images: true
}
});
} catch (error) {
return error;
}
},
// Update single category
updateCategory: async (id: number, data: any) => {
try {
return await strapi.entityService.update("api::product-image.product-image", id, {
data,
populate: {
product_cover: true,
product_pattern: true,
images: true
}
});
} catch (error) {
return error;
}
},
// Update multiple categories using updateMany
updateCategories: async (updates: CategoryUpdate) => {
try {
const result = await strapi.db.query("api::product-image.product-image").updateMany(updates);
// If you need the updated records with populated relations
if (result.count > 0) {
return await strapi.entityService.findMany("api::product-image.product-image", {
filters: updates.where,
populate: {
product_cover: true,
product_pattern: true,
images: true
}
});
}
return result;
} catch (error) {
return error;
}
}
});

View File

@@ -0,0 +1,3 @@
import { ProductCover } from "../../../../../types";
import { createComponentLifecycle } from "../../../../utils/createComponentLifecycle";
export default createComponentLifecycle<ProductCover>("cover");

View File

@@ -0,0 +1,69 @@
{
"kind": "collectionType",
"collectionName": "product_covers",
"info": {
"singularName": "product-cover",
"pluralName": "product-covers",
"displayName": "Product Covers",
"description": ""
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string",
"required": true
},
"binding": {
"type": "enumeration",
"enum": [
"Fadenheftung",
"Steppstich",
"Wire-O-Bindung"
]
},
"slides": {
"type": "media",
"multiple": true,
"required": false,
"allowedTypes": [
"images"
]
},
"copyText": {
"type": "json"
},
"image": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
]
},
"icon": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
]
},
"price": {
"type": "decimal"
},
"products": {
"type": "relation",
"relation": "oneToMany",
"target": "api::product.product",
"mappedBy": "cover"
},
"sort": {
"type": "integer",
"default": 0,
"min": 0
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* product-cover controller
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreController("api::product-cover.product-cover");

View File

@@ -0,0 +1,508 @@
{
"/product-covers": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductCoverListResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-cover"
],
"parameters": [
{
"name": "sort",
"in": "query",
"description": "Sort by attributes ascending (asc) or descending (desc)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "pagination[withCount]",
"in": "query",
"description": "Return page/pageSize (default: true)",
"deprecated": false,
"required": false,
"schema": {
"type": "boolean"
}
},
{
"name": "pagination[page]",
"in": "query",
"description": "Page number (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[pageSize]",
"in": "query",
"description": "Page size (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[start]",
"in": "query",
"description": "Offset value (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[limit]",
"in": "query",
"description": "Number of entities to return (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "fields",
"in": "query",
"description": "Fields to return (ex: title,author)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "populate",
"in": "query",
"description": "Relations to return",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "filters",
"in": "query",
"description": "Filters to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "object",
"additionalProperties": true
},
"style": "deepObject"
},
{
"name": "locale",
"in": "query",
"description": "Locale to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
}
],
"operationId": "get/product-covers"
},
"post": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductCoverResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-cover"
],
"parameters": [],
"operationId": "post/product-covers",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductCoverRequest"
}
}
}
}
}
},
"/product-covers/{id}": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductCoverResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-cover"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "get/product-covers/{id}"
},
"put": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductCoverResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-cover"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "put/product-covers/{id}",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductCoverRequest"
}
}
}
}
},
"delete": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "integer",
"format": "int64"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-cover"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "delete/product-covers/{id}"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* product-cover router
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreRouter("api::product-cover.product-cover");

View File

@@ -0,0 +1,7 @@
/**
* product-cover service
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreService("api::product-cover.product-cover");

View File

@@ -0,0 +1,3 @@
import { ProductImage } from "../../../../../types";
import { createComponentLifecycle } from "../../../../utils/createComponentLifecycle";
export default createComponentLifecycle<ProductImage>("image");

View File

@@ -0,0 +1,43 @@
{
"kind": "collectionType",
"collectionName": "product_images",
"info": {
"singularName": "product-image",
"pluralName": "product-images",
"displayName": "Product Images",
"description": ""
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"images": {
"type": "media",
"multiple": true,
"required": false,
"allowedTypes": [
"images"
]
},
"name": {
"type": "string"
},
"products": {
"type": "relation",
"relation": "oneToMany",
"target": "api::product.product",
"mappedBy": "images"
},
"product_cover": {
"type": "relation",
"relation": "oneToOne",
"target": "api::product-cover.product-cover"
},
"product_pattern": {
"type": "relation",
"relation": "oneToOne",
"target": "api::product-pattern.product-pattern"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* product-image controller
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreController("api::product-image.product-image");

View File

@@ -0,0 +1,508 @@
{
"/product-images": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductImageListResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-image"
],
"parameters": [
{
"name": "sort",
"in": "query",
"description": "Sort by attributes ascending (asc) or descending (desc)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "pagination[withCount]",
"in": "query",
"description": "Return page/pageSize (default: true)",
"deprecated": false,
"required": false,
"schema": {
"type": "boolean"
}
},
{
"name": "pagination[page]",
"in": "query",
"description": "Page number (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[pageSize]",
"in": "query",
"description": "Page size (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[start]",
"in": "query",
"description": "Offset value (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[limit]",
"in": "query",
"description": "Number of entities to return (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "fields",
"in": "query",
"description": "Fields to return (ex: title,author)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "populate",
"in": "query",
"description": "Relations to return",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "filters",
"in": "query",
"description": "Filters to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "object",
"additionalProperties": true
},
"style": "deepObject"
},
{
"name": "locale",
"in": "query",
"description": "Locale to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
}
],
"operationId": "get/product-images"
},
"post": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductImageResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-image"
],
"parameters": [],
"operationId": "post/product-images",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductImageRequest"
}
}
}
}
}
},
"/product-images/{id}": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductImageResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-image"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "get/product-images/{id}"
},
"put": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductImageResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-image"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "put/product-images/{id}",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductImageRequest"
}
}
}
}
},
"delete": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "integer",
"format": "int64"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-image"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "delete/product-images/{id}"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* product-image router
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreRouter("api::product-image.product-image");

View File

@@ -0,0 +1,7 @@
/**
* product-image service
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreService("api::product-image.product-image");

View File

@@ -0,0 +1,3 @@
import { ProductPages } from "../../../../../types";
import { createComponentLifecycle } from "../../../../utils/createComponentLifecycle";
export default createComponentLifecycle<ProductPages>("pages");

View File

@@ -0,0 +1,42 @@
{
"kind": "collectionType",
"collectionName": "product_pages",
"info": {
"singularName": "product-page",
"pluralName": "product-pages",
"displayName": "Product Pages",
"description": ""
},
"options": {
"privateAttributes": [
"createdAt",
"updatedAt",
"created_at",
"updated_at",
"created_by",
"updated_by",
"published_at",
"publishedAt",
"published_by",
"publishedBy"
],
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string",
"required": true,
"default": "name"
},
"price": {
"type": "decimal"
},
"products": {
"type": "relation",
"relation": "oneToMany",
"target": "api::product.product",
"mappedBy": "pages"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* product-page controller
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreController("api::product-page.product-page");

View File

@@ -0,0 +1,508 @@
{
"/product-pages": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductPageListResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-page"
],
"parameters": [
{
"name": "sort",
"in": "query",
"description": "Sort by attributes ascending (asc) or descending (desc)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "pagination[withCount]",
"in": "query",
"description": "Return page/pageSize (default: true)",
"deprecated": false,
"required": false,
"schema": {
"type": "boolean"
}
},
{
"name": "pagination[page]",
"in": "query",
"description": "Page number (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[pageSize]",
"in": "query",
"description": "Page size (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[start]",
"in": "query",
"description": "Offset value (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[limit]",
"in": "query",
"description": "Number of entities to return (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "fields",
"in": "query",
"description": "Fields to return (ex: title,author)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "populate",
"in": "query",
"description": "Relations to return",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "filters",
"in": "query",
"description": "Filters to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "object",
"additionalProperties": true
},
"style": "deepObject"
},
{
"name": "locale",
"in": "query",
"description": "Locale to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
}
],
"operationId": "get/product-pages"
},
"post": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductPageResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-page"
],
"parameters": [],
"operationId": "post/product-pages",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductPageRequest"
}
}
}
}
}
},
"/product-pages/{id}": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductPageResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-page"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "get/product-pages/{id}"
},
"put": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductPageResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-page"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "put/product-pages/{id}",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductPageRequest"
}
}
}
}
},
"delete": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "integer",
"format": "int64"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-page"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "delete/product-pages/{id}"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* product-page router
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreRouter("api::product-page.product-page");

View File

@@ -0,0 +1,7 @@
/**
* product-page service
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreService("api::product-page.product-page");

View File

@@ -0,0 +1,3 @@
import { ProductPattern } from "../../../../../types";
import { createComponentLifecycle } from "../../../../utils/createComponentLifecycle";
export default createComponentLifecycle<ProductPattern>("pattern");

View File

@@ -0,0 +1,49 @@
{
"kind": "collectionType",
"collectionName": "product_patterns",
"info": {
"singularName": "product-pattern",
"pluralName": "product-patterns",
"displayName": "Product Patterns",
"description": ""
},
"options": {
"privateAttributes": [
"createdAt",
"updatedAt",
"created_at",
"updated_at",
"created_by",
"updated_by",
"published_at",
"publishedAt",
"published_by",
"publishedBy"
],
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string",
"required": true
},
"image": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
]
},
"description": {
"type": "string"
},
"products": {
"type": "relation",
"relation": "oneToMany",
"target": "api::product.product",
"mappedBy": "pattern"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* product-pattern controller
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreController("api::product-pattern.product-pattern");

View File

@@ -0,0 +1,508 @@
{
"/product-patterns": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductPatternListResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-pattern"
],
"parameters": [
{
"name": "sort",
"in": "query",
"description": "Sort by attributes ascending (asc) or descending (desc)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "pagination[withCount]",
"in": "query",
"description": "Return page/pageSize (default: true)",
"deprecated": false,
"required": false,
"schema": {
"type": "boolean"
}
},
{
"name": "pagination[page]",
"in": "query",
"description": "Page number (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[pageSize]",
"in": "query",
"description": "Page size (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[start]",
"in": "query",
"description": "Offset value (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[limit]",
"in": "query",
"description": "Number of entities to return (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "fields",
"in": "query",
"description": "Fields to return (ex: title,author)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "populate",
"in": "query",
"description": "Relations to return",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "filters",
"in": "query",
"description": "Filters to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "object",
"additionalProperties": true
},
"style": "deepObject"
},
{
"name": "locale",
"in": "query",
"description": "Locale to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
}
],
"operationId": "get/product-patterns"
},
"post": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductPatternResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-pattern"
],
"parameters": [],
"operationId": "post/product-patterns",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductPatternRequest"
}
}
}
}
}
},
"/product-patterns/{id}": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductPatternResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-pattern"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "get/product-patterns/{id}"
},
"put": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductPatternResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-pattern"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "put/product-patterns/{id}",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductPatternRequest"
}
}
}
}
},
"delete": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "integer",
"format": "int64"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-pattern"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "delete/product-patterns/{id}"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* product-pattern router
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreRouter("api::product-pattern.product-pattern");

View File

@@ -0,0 +1,7 @@
/**
* product-pattern service
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreService("api::product-pattern.product-pattern");

View File

@@ -0,0 +1,3 @@
import { ProductRuling } from "../../../../../types";
import { createComponentLifecycle } from "../../../../utils/createComponentLifecycle";
export default createComponentLifecycle<ProductRuling>("ruling");

View File

@@ -0,0 +1,48 @@
{
"kind": "collectionType",
"collectionName": "product_rulings",
"info": {
"singularName": "product-ruling",
"pluralName": "product-rulings",
"displayName": "Product Ruling",
"description": ""
},
"options": {
"privateAttributes": [
"createdAt",
"updatedAt",
"created_at",
"updated_at",
"created_by",
"updated_by",
"published_at",
"publishedAt",
"published_by",
"publishedBy"
],
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string",
"required": true
},
"price": {
"type": "decimal"
},
"products": {
"type": "relation",
"relation": "oneToMany",
"target": "api::product.product",
"mappedBy": "ruling"
},
"icon": {
"allowedTypes": [
"images"
],
"type": "media",
"multiple": false
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* product-ruling controller
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreController("api::product-ruling.product-ruling");

View File

@@ -0,0 +1,508 @@
{
"/product-rulings": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductRulingListResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-ruling"
],
"parameters": [
{
"name": "sort",
"in": "query",
"description": "Sort by attributes ascending (asc) or descending (desc)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "pagination[withCount]",
"in": "query",
"description": "Return page/pageSize (default: true)",
"deprecated": false,
"required": false,
"schema": {
"type": "boolean"
}
},
{
"name": "pagination[page]",
"in": "query",
"description": "Page number (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[pageSize]",
"in": "query",
"description": "Page size (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[start]",
"in": "query",
"description": "Offset value (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[limit]",
"in": "query",
"description": "Number of entities to return (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "fields",
"in": "query",
"description": "Fields to return (ex: title,author)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "populate",
"in": "query",
"description": "Relations to return",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "filters",
"in": "query",
"description": "Filters to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "object",
"additionalProperties": true
},
"style": "deepObject"
},
{
"name": "locale",
"in": "query",
"description": "Locale to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
}
],
"operationId": "get/product-rulings"
},
"post": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductRulingResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-ruling"
],
"parameters": [],
"operationId": "post/product-rulings",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductRulingRequest"
}
}
}
}
}
},
"/product-rulings/{id}": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductRulingResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-ruling"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "get/product-rulings/{id}"
},
"put": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductRulingResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-ruling"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "put/product-rulings/{id}",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductRulingRequest"
}
}
}
}
},
"delete": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "integer",
"format": "int64"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product-ruling"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "delete/product-rulings/{id}"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* product-ruling router
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreRouter("api::product-ruling.product-ruling");

View File

@@ -0,0 +1,7 @@
/**
* product-ruling service
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreService("api::product-ruling.product-ruling");

View File

@@ -0,0 +1,83 @@
import { Product } from "../../../../../types";
import { calculateTotalProductPrice } from "../../services/product";
export default {
async beforeCreate(event: {
params: {
data: any;
};
}) {
const { params } = event;
strapi.log.verbose("app:v:product-lifecycle: Before create", {
params
});
const cover = await strapi.entityService.findOne("api::product-cover.product-cover", params.data?.cover, { fields: ["price"] });
const pattern = await strapi.entityService.findOne("api::product-pattern.product-pattern", params.data?.pattern);
const pages = await strapi.entityService.findOne("api::product-page.product-page", params.data?.pages, { fields: ["price"] });
const ruling = await strapi.entityService.findOne("api::product-ruling.product-ruling", params.data?.ruling, { fields: ["price"] });
const product = {
...params.data,
cover,
pattern,
pages,
ruling
} as Partial<Product>;
strapi.log.verbose("app:v:product-lifecycle: Created product", product);
params.data.totalPrice = calculateTotalProductPrice(product);
},
async afterCreate(event: { result: Product }) {
const { result } = event;
strapi.log.verbose("app:v:product-lifecycle: Created", {
id: result.id
});
},
async beforeUpdate(event: { params: any }) {
const { params } = event;
strapi.log.verbose("app:v:product-lifecycle: Before update", {
params
});
},
async afterUpdate(event: { result: Product }) {
const { result } = event;
strapi.log.verbose("app:v:product-lifecycle: Updated", {
id: result.id
});
},
async beforeDelete(event: { params: Product }) {
const { params } = event;
strapi.log.verbose("app:v:product-lifecycle: Before delete", {
params
});
}
// async afterFindOne(event: { result: any; }) {
// const { result } = event;
// strapi.log.verbose("app:v:product-lifecycle: Found one", {
// result
// });
// },
// async afterFindMany(event: any) {
// strapi.log.verbose("app:v:product-lifecycle: Before found many", {
// event
// });
//
// const resultWithTotal = event.result.map((product: Product) => {
// return {
// ...product,
// totalProductPrice: calculateTotalProductPrice(product)
// };
// });
//
// strapi.log.verbose("app:v:product-lifecycle: Added total product prices", resultWithTotal.map((p: Product) => p.totalProductPrice));
//
// event.result = resultWithTotal;
//
// return event;
// }
};

View File

@@ -0,0 +1,59 @@
{
"kind": "collectionType",
"collectionName": "products",
"info": {
"singularName": "product",
"pluralName": "products",
"displayName": "Product",
"description": ""
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string"
},
"description": {
"type": "text"
},
"totalPrice": {
"type": "decimal"
},
"cover": {
"type": "relation",
"relation": "manyToOne",
"target": "api::product-cover.product-cover",
"inversedBy": "products"
},
"pattern": {
"type": "relation",
"relation": "manyToOne",
"target": "api::product-pattern.product-pattern",
"inversedBy": "products"
},
"pages": {
"type": "relation",
"relation": "manyToOne",
"target": "api::product-page.product-page",
"inversedBy": "products"
},
"ruling": {
"type": "relation",
"relation": "manyToOne",
"target": "api::product-ruling.product-ruling",
"inversedBy": "products"
},
"slug": {
"type": "uid",
"targetField": "name"
},
"images": {
"type": "relation",
"relation": "manyToOne",
"target": "api::product-image.product-image",
"inversedBy": "products"
}
}
}

View File

@@ -0,0 +1,442 @@
import { factories } from "@strapi/strapi";
import { sanitize } from "@strapi/utils";
import { Result, PaginatedResult } from "@strapi/types/dist/modules/entity-service/result";
import { ID } from "@strapi/database/dist/types";
import { Product, ProductPattern } from "../../../../types";
import { productDefaultParams, calculateTotalProductPrice } from "../services/product";
import { maxProductsSitemap } from "../../../../config/constants";
export interface ProductVariantParams {
populate: {
pattern: any;
cover: any;
ruling: any;
pages: any;
images: any;
};
pagination: {
page: number;
pageSize: number;
};
filters?: any;
publicationState?: "live" | "preview";
}
const productVariantParams: ProductVariantParams = {
populate: {
pattern: { fields: ["id"] },
cover: { fields: ["id"] },
ruling: { fields: ["id"] },
pages: { fields: ["id"] },
images: {
populate: {
images: {
fields: ["formats", "url"]
}
}
}
},
pagination: { page: 1, pageSize: 999 },
publicationState: "live"
};
/**
* Product controller with custom actions
*/
export default factories.createCoreController("api::product.product", ({ strapi }) => ({
/**
* Find a single product by ID
*/
findOne: async (ctx) => {
const { id } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::product.product"));
const productUnsafe = await strapi.service("api::product.product").findOne(id, productDefaultParams);
const product = (await sanitize.contentAPI.output(productUnsafe, strapi.getModel("api::product.product"))) as Product;
return {
...product,
totalProductPrice: calculateTotalProductPrice(product)
};
},
/**
* Find products with the lowest price ruling and pages
*/
find: async (ctx): Promise<{ data: Result<"api::product.product">[]; meta: { pagination: any } }> => {
try {
const params = await sanitize.contentAPI.query(ctx.query, strapi.getModel("api::product.product"));
const filtersParams = (params?.filters as Record<string, any>) ?? {};
const populateParams = (params?.populate as Record<string, any>) ?? {};
const paginationParams = (params?.pagination as Record<string, any>) ?? {};
const mergedParams = {
...productDefaultParams,
filters: filtersParams,
populate: {
...productDefaultParams.populate,
...populateParams
},
pagination: paginationParams
};
strapi.log.debug(JSON.stringify({ mergedParams }));
const response = await strapi.service("api::product.product").find(mergedParams);
const { pagination } = response;
const dataUnsafe = (await sanitize.contentAPI.output(
response.results,
strapi.getModel("api::product.product")
)) as Result<"api::product.product">[];
const data = dataUnsafe.map((product: Product) => {
return {
...product,
totalProductPrice: calculateTotalProductPrice(product)
};
});
return { data, meta: { pagination } };
} catch (error) {
strapi.log.error(error);
return ctx.badRequest("Could not fetch products");
}
},
/**
* Get all variants for a product
* @returns {Promise<Product[]>} All variants of the product
*/
allVariants: async (ctx): Promise<Product[]> => {
try {
const { id } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::product.product"));
strapi.log.verbose(`Fetching variants for product ${id}`);
const product = await getProductDetails(id as ID);
const variants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>("api::product.product", {
...productVariantParams,
filters: {
pattern: { id: { $eq: product.pattern.id } }
}
});
if (!variants) {
return ctx.notFound(`Could not find variants for product ${id}`);
}
return Promise.all(variants.map((variant) => sanitize.contentAPI.output(variant, strapi.getModel("api::product.product")))) as Promise<
Product[]
>;
} catch (error) {
strapi.log.error(error);
return ctx.badRequest("Could not fetch product variants");
}
},
/**
* Get variants by pattern for a product
* @returns {Promise<{ allProductPattern: ProductPattern[], productVariants: Product[], patterns: Array<ProductPattern & { productVariant: Product | undefined }> }>} Variants grouped by pattern
*/
variantsByPattern: async (
ctx
): Promise<{
allProductPattern: ProductPattern[];
productVariants: Product[];
patterns: Array<ProductPattern & { productVariant: Product | undefined }>;
}> => {
try {
const { id } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::product.product"));
strapi.log.verbose(`Fetching variants by pattern for product ${id}`);
const product = await getProductDetails(id as ID);
const { pattern, cover, ruling, pages } = product;
const productVariants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>("api::product.product", {
...productVariantParams,
filters: {
$and: [
{ id: { $ne: id } },
{ pattern: { id: { $ne: pattern.id } } },
{ cover: { id: { $eq: cover.id } } },
{ ruling: { id: { $eq: ruling.id } } },
{ pages: { id: { $eq: pages.id } } }
]
}
});
const sanitizedVariants = (await Promise.all(
productVariants.map((variant) => sanitize.contentAPI.output(variant, strapi.getModel("api::product.product")))
)) as Product[];
const allProductPatternResponse = (await strapi.service("api::product-pattern.product-pattern").find({
fields: ["id", "name", "description"],
populate: { image: { fields: ["url"] } },
filters: { id: { $ne: pattern.id } },
publicationState: "live"
})) as PaginatedResult<"api::product-pattern.product-pattern">;
const allProductPattern = allProductPatternResponse.results;
return {
allProductPattern,
productVariants: sanitizedVariants,
patterns: allProductPattern.map((pattern) => ({
...pattern,
productVariant: sanitizedVariants.find((variant) => variant?.pattern?.id === pattern.id)
}))
};
} catch (error) {
strapi.log.error(error);
return ctx.badRequest("Could not fetch variants by pattern");
}
},
/**
* Get variants for a product
* @returns {Promise<{ pages: any[], cover: any[], ruling: any[] }>} Variants grouped by pages, cover, and ruling
*/
variants: async (
ctx
): Promise<{
pages: any;
cover: any;
ruling: any;
}> => {
try {
const { id } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::product.product"));
strapi.log.verbose(`Fetching variants for product ${id}`);
const product = await getProductDetails(id as ID);
const allProductPatternVariants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>(
"api::product.product",
{
...productVariantParams,
filters: {
$and: [{ pattern: { id: { $eq: product.pattern.id } } }, { id: { $ne: id } }]
}
}
);
const [allProductPages, allProductRulings, allProductCovers] = await Promise.all([
strapi
.service("api::product-page.product-page")
.find({ fields: ["id", "name"] }) as PaginatedResult<"api::product-pattern.product-pattern">,
strapi.service("api::product-ruling.product-ruling").find({
fields: ["id", "name"],
populate: { icon: { fields: ["url"] } }
}) as PaginatedResult<"api::product-ruling.product-ruling">,
strapi.service("api::product-cover.product-cover").find({
fields: ["id", "name", "binding", "price"],
populate: { icon: { fields: ["url"] } }
}) as PaginatedResult<"api::product-cover.product-cover">
]);
const sanitizedVariants = (await Promise.all(
allProductPatternVariants.map((variant) => sanitize.contentAPI.output(variant, strapi.getModel("api::product.product")))
)) as Product[];
// For pages variants: find products with same pattern, cover, and ruling, but different pages
const pagesVariants = allProductPages.results.map((pages) => ({
...pages,
productVariant: sanitizedVariants.find(
(variant) =>
variant.id !== product.id &&
variant.pages.id === pages.id &&
variant.cover.id === product.cover.id &&
variant.ruling.id === product.ruling.id
)
}));
// For cover variants: find products with same pattern and ruling but different covers
const coverVariants = allProductCovers.results.map((cover) => {
// Find any product with this cover and the same pattern
const matchingVariant = sanitizedVariants.find(
(variant) =>
variant.id !== product.id &&
variant.cover.id === cover.id &&
variant.pattern.id === product.pattern.id &&
variant.ruling.id === product.ruling.id
);
return {
...cover,
productVariant: matchingVariant
};
});
// For ruling variants: find products with same pattern, cover, and pages, but different ruling
const rulingVariants = allProductRulings.results.map((ruling) => ({
...ruling,
productVariant: sanitizedVariants.find(
(variant) =>
variant.id !== product.id &&
variant.ruling.id === ruling.id &&
variant.pages.id === product.pages.id &&
variant.cover.id === product.cover.id
)
}));
return {
pages: pagesVariants,
cover: coverVariants,
ruling: rulingVariants
};
} catch (error) {
strapi.log.error(error);
return ctx.badRequest("Could not fetch product variants");
}
},
/**
* Publish or unpublish products by filter
* @returns {Promise<{published: number, dryRun: boolean}>} The number of products published/unpublished and dryRun status
*/
publishByFilter: async (ctx): Promise<{ published: number; dryRun: boolean }> => {
try {
const { filters, publish = true, dryRun = false } = ctx.request.body;
if (!filters) {
return ctx.badRequest("Filters are required");
}
strapi.log.debug(`Publishing products with filters: ${JSON.stringify(filters)}, publish: ${publish}, dryRun: ${dryRun}`);
return await strapi.service("api::product.product").publishByFilter(filters, publish, dryRun);
} catch (error) {
strapi.log.error(error);
return ctx.badRequest("Could not publish products");
}
},
/**
* Find products with only the cheapest variant per cover+pattern combination
*/
findCheapest: async (ctx): Promise<{ data: Result<"api::product.product">[]; meta: { pagination: any } }> => {
try {
const params = await sanitize.contentAPI.query(ctx.query, strapi.getModel("api::product.product"));
const filtersParams = (params?.filters as Record<string, any>) ?? {};
const populateParams = (params?.populate as Record<string, any>) ?? {};
const paginationParams = (params?.pagination as Record<string, any>) ?? {};
// First get all products
const mergedParams = {
...productDefaultParams,
filters: filtersParams,
populate: {
...productDefaultParams.populate,
...populateParams
},
pagination: { pageSize: 999 } // Temporarily get all to filter
};
const allProducts = await strapi.service("api::product.product").find(mergedParams);
const allProductsSanitized = (await sanitize.contentAPI.output(
allProducts.results,
strapi.getModel("api::product.product")
)) as Product[];
// Group products by cover+pattern combination
const productGroups = new Map();
allProductsSanitized.forEach((product) => {
const key = `${product.cover.id}-${product.pattern.id}`;
if (!productGroups.has(key) || calculateTotalProductPrice(product) < calculateTotalProductPrice(productGroups.get(key))) {
productGroups.set(key, product);
}
});
// Convert map values to array and sort by pattern.id
const cheapestProducts = Array.from(productGroups.values()).sort((a, b) => {
const idA = a.pattern?.id ?? 0;
const idB = b.pattern?.id ?? 0;
return idA - idB;
});
// Apply pagination manually
const page = parseInt(paginationParams.page) || 1;
const pageSize = parseInt(paginationParams.pageSize) || 24;
const total = cheapestProducts.length;
const pageCount = Math.ceil(total / pageSize);
const start = (page - 1) * pageSize;
const end = start + pageSize;
const paginatedProducts = cheapestProducts.slice(start, end);
// Add total price to each product
const data = paginatedProducts.map((product) => ({
...product,
totalProductPrice: calculateTotalProductPrice(product)
}));
return {
data,
meta: {
pagination: {
page,
pageSize,
pageCount,
total
}
}
};
} catch (error) {
strapi.log.error(error);
return ctx.badRequest("Could not fetch cheapest products");
}
},
/**
* Get all published product paths for sitemap generation
* @returns {Promise<string[]>} Array of product paths
*/
sitemap: async (ctx): Promise<string[]> => {
try {
strapi.log.verbose("Generating product sitemap");
const products = await strapi.entityService.findMany("api::product.product", {
fields: ["slug"],
filters: {
publishedAt: { $notNull: true }
},
pagination: {
page: 1,
pageSize: maxProductsSitemap
},
sort: { updatedAt: "desc" }
});
if (!products || !products.length) {
return [];
}
const productPaths = products.map((product) => `/details/${product.slug}`);
strapi.log.verbose(`Generated product sitemap with ${productPaths.length} entries`);
strapi.log.silly(`Product sitemap: ${JSON.stringify(productPaths)}`);
return productPaths;
} catch (error) {
strapi.log.error(error);
return ctx.badRequest("Could not generate product sitemap");
}
}
}));
/**
* Helper function to get product details
* @param {ID} id - The product ID
* @returns {Promise<Product>} The product details
* @throws {Error} If the product is not found
*/
async function getProductDetails(id: ID): Promise<Product> {
const product = await strapi.entityService.findOne("api::product.product", id, {
fields: ["id", "name"],
populate: {
pattern: { fields: ["id"] },
cover: { fields: ["id"] },
ruling: { fields: ["id"] },
pages: { fields: ["id"] }
}
});
if (!product) {
throw new Error(`Could not find product with ID ${id}`);
}
return (await sanitize.contentAPI.output(product, strapi.getModel("api::product.product"))) as Promise<Product>;
}

View File

@@ -0,0 +1,754 @@
{
"/products": {
"post": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product"
],
"parameters": [],
"operationId": "post/products",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductRequest"
}
}
}
}
},
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductListResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product"
],
"parameters": [
{
"name": "sort",
"in": "query",
"description": "Sort by attributes ascending (asc) or descending (desc)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "pagination[withCount]",
"in": "query",
"description": "Return page/pageSize (default: true)",
"deprecated": false,
"required": false,
"schema": {
"type": "boolean"
}
},
{
"name": "pagination[page]",
"in": "query",
"description": "Page number (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[pageSize]",
"in": "query",
"description": "Page size (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[start]",
"in": "query",
"description": "Offset value (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[limit]",
"in": "query",
"description": "Number of entities to return (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "fields",
"in": "query",
"description": "Fields to return (ex: title,author)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "populate",
"in": "query",
"description": "Relations to return",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "filters",
"in": "query",
"description": "Filters to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "object",
"additionalProperties": true
},
"style": "deepObject"
},
{
"name": "locale",
"in": "query",
"description": "Locale to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
}
],
"operationId": "get/products"
}
},
"/products/{id}": {
"put": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "put/products/{id}",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductRequest"
}
}
}
}
},
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "get/products/{id}"
}
},
"/products/{id}/variants/all": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "get/products/{id}/variants/all"
}
},
"/products/{id}/variants/pattern": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "get/products/{id}/variants/pattern"
}
},
"/products/{id}/variants": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"deprecated": false,
"required": true,
"schema": {
"type": "number"
}
}
],
"operationId": "get/products/{id}/variants"
}
},
"/products/publish": {
"post": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Product"
],
"parameters": [],
"operationId": "post/products/publish",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductRequest"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,90 @@
export default {
routes: [
{
method: "POST",
path: "/products",
handler: "product.create",
config: {
policies: []
}
},
{
method: "GET",
path: "/products",
handler: "product.find",
config: {
policies: []
}
},
{
method: "GET",
path: "/promo-products",
handler: "product.findCheapest",
config: {
auth: false,
policies: []
}
},
{
method: "GET",
path: "/sitemap/products",
handler: "product.sitemap",
config: {
auth: false,
policies: []
}
},
{
method: "PUT",
path: "/products/:id",
handler: "product.update",
config: {
policies: []
}
},
{
method: "GET",
path: "/products/:id",
handler: "product.findOne",
config: {
auth: false,
policies: []
}
},
{
method: "GET",
path: "/products/:id/variants/all",
handler: "product.allVariants",
config: {
auth: false,
policies: []
}
},
{
method: "GET",
path: "/products/:id/variants/pattern",
handler: "product.variantsByPattern",
config: {
auth: false,
policies: []
}
},
{
method: "GET",
path: "/products/:id/variants",
handler: "product.variants",
config: {
auth: false,
policies: []
}
},
{
method: "POST",
path: "/products/publish",
handler: "product.publishByFilter",
config: {
policies: []
}
}
]
};

View File

@@ -0,0 +1,153 @@
/**
* This service is responsible for handling the product logic.
*/
import { factories } from "@strapi/strapi";
import { Product } from "../../../../types";
export default factories.createCoreService("api::product.product", ({ strapi }) => ({
find: async (params: Record<string, any>) => {
const filtersParams = (params?.filters as Record<string, any>) ?? {};
const populateParams = (params?.populate as Record<string, any>) ?? {};
const paginationParams = (params?.pagination as Record<string, any>) ?? {};
return strapi.entityService.findPage("api::product.product", {
...productDefaultParams,
populate: {
...productDefaultParams.populate,
...populateParams
},
filters: filtersParams,
pageSize: 30,
...paginationParams
});
// const resultsUnsafe = response.results;
// const results = resultsUnsafe.map((product) => {
// return {
// ...product,
// totalProductPrice: calculateTotalProductPrice(product)
// };
// });
//
// return { ...response, results };
},
findOne: async (id: number) => {
const product = await strapi.entityService.findOne<"api::product.product", ProductParams>("api::product.product", id, productDefaultParams);
return {
...product,
totalProductPrice: calculateTotalProductPrice(product)
};
},
/**
* Publish or unpublish products by filter
* @param {Record<string, any>} filters - The filters to apply
* @param {boolean} publish - Whether to publish or unpublish
* @param {boolean} dryRun - Whether to perform a dry run
* @returns {Promise<{published: number, products: number[], dryRun: boolean}>} The number of products published/unpublished and dryRun status
*/
publishByFilter: async (
filters: Record<string, any>,
publish: boolean = true,
dryRun: boolean = false
): Promise<{ published: any; products: number[]; dryRun: boolean }> => {
const query = strapi.db.query("api::product.product");
// Build the filter query - complex filtering based on product relations
const dbFilters = {
$and: [
...Object.entries(filters).map(([key, value]) => {
// Handle relation filters
if (["pattern", "cover", "ruling", "pages"].includes(key) && typeof value === "object") {
return {
[key]: value
};
}
// Handle direct property filters
return {
[key]: value
};
})
]
};
// Find products matching the filters
const products = await query.findMany({
where: dbFilters,
populate: ["pattern", "cover", "ruling", "pages"]
});
strapi.log.info(`Found ${products.length} products matching filters${dryRun ? " (dry run)" : ""}`);
// If dry run, just return the count
if (dryRun) {
return {
published: products.length,
products: products.map((product) => product.id),
dryRun: true
};
}
// Update publication state for matching products
if (products.length > 0) {
const updatePromises = products.map((product) => {
return query.update({
where: { id: product.id },
data: {
publishedAt: publish ? new Date() : null
}
});
});
await Promise.all(updatePromises);
}
return {
published: products.length,
products: products.map((product) => product.id),
dryRun: false
};
}
}));
export const calculateTotalProductPrice = (product: Partial<Product>): number => {
return (product?.cover?.price ?? 0) + (product?.pages?.price ?? 0) + (product?.ruling?.price ?? 0);
};
export interface ProductParams {
populate?: {
pattern: any;
cover: any;
ruling: any;
pages: any;
images: {
populate: {
images: any;
};
};
};
publicationState?: "live" | "preview";
}
export const productDefaultParams: ProductParams = {
populate: {
pattern: true,
cover: {
populate: {
slides: {
fields: ["formats", "url"]
}
}
},
ruling: true,
pages: true,
images: {
populate: {
images: {
fields: ["formats", "url"]
}
}
}
},
publicationState: "live"
};

View File

@@ -0,0 +1,36 @@
{
"kind": "singleType",
"collectionName": "vats",
"info": {
"singularName": "vat",
"pluralName": "vats",
"displayName": "VAT"
},
"options": {
"privateAttributes": [
"createdAt",
"updatedAt",
"created_at",
"updated_at",
"created_by",
"updated_by",
"published_at",
"publishedAt",
"published_by",
"publishedBy"
],
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string"
},
"percent": {
"type": "integer"
},
"decimal": {
"type": "decimal"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* vat controller
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreController("api::vat.vat");

View File

@@ -0,0 +1,325 @@
{
"/vat": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VatResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Vat"
],
"parameters": [
{
"name": "sort",
"in": "query",
"description": "Sort by attributes ascending (asc) or descending (desc)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "pagination[withCount]",
"in": "query",
"description": "Return page/pageSize (default: true)",
"deprecated": false,
"required": false,
"schema": {
"type": "boolean"
}
},
{
"name": "pagination[page]",
"in": "query",
"description": "Page number (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[pageSize]",
"in": "query",
"description": "Page size (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[start]",
"in": "query",
"description": "Offset value (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[limit]",
"in": "query",
"description": "Number of entities to return (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "fields",
"in": "query",
"description": "Fields to return (ex: title,author)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "populate",
"in": "query",
"description": "Relations to return",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "filters",
"in": "query",
"description": "Filters to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "object",
"additionalProperties": true
},
"style": "deepObject"
},
{
"name": "locale",
"in": "query",
"description": "Locale to apply",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
}
],
"operationId": "get/vat"
},
"put": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VatResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Vat"
],
"parameters": [],
"operationId": "put/vat",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VatRequest"
}
}
}
}
},
"delete": {
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "integer",
"format": "int64"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"tags": [
"Vat"
],
"parameters": [],
"operationId": "delete/vat"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* vat router
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreRouter("api::vat.vat");

View File

@@ -0,0 +1,7 @@
/**
* vat service
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreService("api::vat.vat");

View File

@@ -0,0 +1,22 @@
{
"kind": "singleType",
"collectionName": "websites",
"info": {
"singularName": "website",
"pluralName": "websites",
"displayName": "Website",
"description": ""
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"website": {
"displayName": "Website in Menu",
"type": "component",
"repeatable": true,
"component": "websites.website-in-menu"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* website controller
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreController("api::website.website");

View File

@@ -0,0 +1,7 @@
/**
* website router
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreRouter("api::website.website");

View File

@@ -0,0 +1,7 @@
/**
* website service
*/
import { factories } from "@strapi/strapi";
export default factories.createCoreService("api::website.website");

View File

@@ -0,0 +1,28 @@
{
"collectionName": "components_address_structured_addresses",
"info": {
"displayName": "Structured Address",
"description": "Address fields matching HTML autocomplete attributes"
},
"attributes": {
"givenName": {
"type": "string"
},
"familyName": {
"type": "string"
},
"streetAddress": {
"type": "string"
},
"postalCode": {
"type": "string"
},
"addressLevel2": {
"type": "string"
},
"country": {
"type": "string",
"default": "DE"
}
}
}

View File

@@ -0,0 +1,21 @@
{
"collectionName": "components_products_carts",
"info": {
"displayName": "cart",
"icon": "stack",
"description": ""
},
"options": {},
"attributes": {
"product": {
"type": "relation",
"relation": "oneToOne",
"target": "api::product.product"
},
"count": {
"type": "integer",
"default": 1,
"min": 1
}
}
}

View File

@@ -0,0 +1,26 @@
{
"collectionName": "components_websites_website_in_menus",
"info": {
"displayName": "Website in Menu",
"icon": "file",
"description": ""
},
"options": {},
"attributes": {
"menu": {
"type": "string",
"required": true
},
"showInHeader": {
"type": "boolean",
"default": true
},
"showInFooter": {
"type": "boolean",
"default": true
},
"richtext": {
"type": "blocks"
}
}
}

0
src/extensions/.gitkeep Normal file
View File

View File

@@ -0,0 +1,30 @@
{
"openapi": "3.0.1",
"info": {
"version": "1.0.0",
"title": "MUELLERPTINTS. Paperwork",
"description": "API Documentation for MUELLERPRINTS. Paperwork",
"termsOfService": false,
"contact": false,
"license": false
},
"x-strapi-config": {
"plugins": [],
"path": "/documentation"
},
"servers": [
{
"url": "https://localhost:8443/api",
"description": "Docker Development"
},
{
"url": "http://localhost:5555/api",
"description": "Local Development"
}
],
"security": [
{
"bearerAuth": []
}
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

18
src/index.ts Normal file
View File

@@ -0,0 +1,18 @@
export default {
/**
* An asynchronous register function that runs before
* your application is initialized.
*
* This gives you an opportunity to extend code.
*/
register(/*{ strapi }*/) {},
/**
* An asynchronous bootstrap function that runs before
* your application gets started.
*
* This gives you an opportunity to set up your data model,
* run jobs, or perform some special logic.
*/
bootstrap(/*{ strapi }*/) {}
};

92
src/services/MailApi.ts Normal file
View File

@@ -0,0 +1,92 @@
import { mailApiUrl } from "../../config/constants";
import type { Response } from "node-fetch";
import FormData from "form-data";
const logger = strapi.log.info;
const verbose = strapi.log.verbose;
const debug = strapi.log.verbose;
class MailApi {
baseUrl: string;
headers: Headers;
constructor(options: MailApiOptions) {
this.baseUrl = options.baseUrl;
this.headers = options.defaultHeaders ?? {};
}
async sendTextMessage(body: MessageBody): Promise<void> {
strapi.log.debug(`app:d:mail-api: Sending text message`);
try {
await this.postRequest("/v1/send/message", body);
} catch (error) {
throw error;
}
}
async sendPdf(body: PdfMessageBody): Promise<void> {
strapi.log.debug(`app:d:mail-api: Sending PDF`);
try {
await this.postPdfRequest("/v1/send/pdf", body);
} catch (error) {
throw error;
}
}
private async postRequest(endpoint: string, body: Record<string, any>): Promise<void> {
const response = (await strapi.fetch(`${this.baseUrl}${endpoint}`, {
method: "POST",
headers: {
...this.headers,
"content-type": "application/json"
},
body: JSON.stringify(body)
})) as Response;
if (!response.ok) {
throw new Error(`Failed to send text message: ${response.statusText}`);
}
}
private async postPdfRequest(endpoint: string, body: PdfMessageBody): Promise<void> {
const formData = new FormData();
formData.append("subject", body.subject);
formData.append("message", body.message);
formData.append("to_email", body.to_email);
formData.append("pdf_blob", body.pdf_blob);
const response = (await strapi.fetch(`${this.baseUrl}${endpoint}`, {
method: "POST",
headers: {
...this.headers
},
body: formData
})) as Response;
if (!response.ok) {
throw new Error(`Failed to send PDF: ${response.statusText}`);
}
}
}
interface MailApiOptions {
baseUrl: string;
defaultHeaders?: Headers;
}
export interface MessageBody {
subject: string;
message: string;
html?: string;
to_email: string;
}
export interface PdfMessageBody extends MessageBody {
pdf_blob: Blob;
}
type Headers = Record<string, string>;
export const mailApi = new MailApi({
baseUrl: mailApiUrl
});

165
src/services/PayPalApi.ts Normal file
View File

@@ -0,0 +1,165 @@
import {
Client,
Environment,
LogLevel,
OrdersController,
CheckoutPaymentIntent,
OrderRequest,
OrderApplicationContextShippingPreference,
Item
} from "@paypal/paypal-server-sdk";
import { paypalClientId, paypalClientSecret, paypalEnvironment, vatIncludedDecimal, vatDecimalExcluded } from "../../config/constants";
import { Order } from "../../types";
import { calculateTotalProductPrice } from "../api/product/services/product";
class PayPalApi {
private readonly client: Client;
private readonly ordersController: OrdersController;
constructor(clientId: string, clientSecret: string, environment: Environment) {
this.client = new Client({
clientCredentialsAuthCredentials: {
oAuthClientId: clientId,
oAuthClientSecret: clientSecret
},
timeout: 0,
environment,
logging: {
logLevel: LogLevel.Info,
logRequest: { logBody: true },
logResponse: { logHeaders: true }
}
});
this.ordersController = new OrdersController(this.client);
}
async createSessionOrThrow(returnUrl: string, order: Order) {
if (!order.total) throw new Error("Cart is empty or has no total");
const items = order.cart.map((item) => {
const totalProductPrice = calculateTotalProductPrice(item.product);
const amount = totalProductPrice / vatIncludedDecimal;
const tax = totalProductPrice - amount;
return {
name: item.product.name,
unitAmount: {
currencyCode: "EUR",
value: amount.toFixed(2)
},
tax: {
currencyCode: "EUR",
value: tax.toFixed(2)
},
quantity: item.count.toString()
} as Item;
});
try {
const collect = {
body: {
intent: CheckoutPaymentIntent.Capture,
purchaseUnits: [
{
// items,
referenceId: order.uuid,
amount: {
currencyCode: "EUR",
value: order.total.toFixed(2),
breakdown: {
itemTotal: {
currencyCode: "EUR",
value: (Math.round((order.subtotal / vatIncludedDecimal) * 100) / 100).toFixed(2)
},
taxTotal: {
currencyCode: "EUR",
value: order.VAT.toFixed(2)
},
shipping: order.delivery
? {
currencyCode: "EUR",
value: order.delivery.price.toFixed(2)
}
: undefined
}
},
shipping: {
name: {
fullName: order.deliveryAddress.split("\n")[0]
},
address: {
addressLine1: order.deliveryAddress.split("\n")[1],
postalCode: order.deliveryAddress.split("\n")[2].slice(0, 5),
adminArea2: order.deliveryAddress.split("\n")[2].slice(6),
countryCode: "DE"
}
},
customId: order.uuid,
invoiceId: order.invoiceNumber
}
],
applicationContext: {
returnUrl: returnUrl,
cancelUrl: returnUrl,
shippingPreference: OrderApplicationContextShippingPreference.SetProvidedAddress
}
} as OrderRequest,
prefer: "return=minimal"
};
strapi.log.silly("app:d:paypal-api: Creating PayPal order " + JSON.stringify({ collect }));
const { body, ...httpResponse } = await this.ordersController.ordersCreate(collect);
const sessionData = JSON.parse(body as string);
strapi.log.info("app:i:paypal-api: " + JSON.stringify({ session: sessionData }));
return {
id: sessionData.id,
links: sessionData.links
};
} catch (error) {
strapi.log.error("app:e:paypal-api: " + JSON.stringify({ error }));
throw error;
}
}
async checkPaymentStatus(redirect: { orderID: string }) {
try {
const collect = {
id: redirect.orderID,
prefer: "return=minimal"
};
const { body } = await this.ordersController.ordersCapture(collect);
const captureResult = JSON.parse(body as string);
strapi.log.info("app:i:paypal-api: " + JSON.stringify({ captureResult }));
return captureResult;
} catch (error) {
return "/result/error";
}
}
async handleWebhook(req: any) {
// Implement PayPal webhook validation and processing
strapi.log.verbose("app:i:paypal-api: " + JSON.stringify({ req }));
// Note: PayPal webhook handling requires specific implementation
// This is a placeholder and should be replaced with actual PayPal webhook validation
return { message: "Webhook received" };
}
getClientKey() {
// PayPal doesn't use a client key like Adyen, so we'll return the client ID
return paypalClientId;
}
}
export const paypalApi = new PayPalApi(paypalClientId, paypalClientSecret, paypalEnvironment);
export type PayPalApiOptions = {
clientId: string;
clientSecret: string;
environment: Environment;
};

56
src/services/PdfApi.ts Normal file
View File

@@ -0,0 +1,56 @@
import type { Response } from "node-fetch";
import { pdfApiUrl } from "../../config/constants";
import { PdfBody } from "../../types";
const logger = strapi.log.info;
const verbose = strapi.log.verbose;
const debug = strapi.log.verbose;
class PdfApi {
baseUrl: string;
headers: Headers;
constructor(options: PdfApiOptions) {
this.baseUrl = options.baseUrl;
this.headers = options.defaultHeaders ?? {};
}
async generateInvoice(body: PdfBody) {
try {
return await this.postRequest("/v1/invoice", body);
} catch (error) {
throw error;
}
}
async generateDeliveryNote(body: PdfBody) {
try {
return await this.postRequest("/v1/shipping", body);
} catch (error) {
throw error;
}
}
private async postRequest(endpoint: string, body: Record<string, any>) {
const response = (await strapi.fetch(`${this.baseUrl}${endpoint}`, {
method: "POST",
headers: this.headers,
body: JSON.stringify(body)
})) as Response;
return await response.arrayBuffer();
}
}
interface PdfApiOptions {
baseUrl: string;
defaultHeaders?: Headers;
}
type Headers = Record<string, string>;
export const pdfApi = new PdfApi({
baseUrl: pdfApiUrl,
defaultHeaders: {
"content-type": "application/json"
}
});

View File

@@ -0,0 +1,441 @@
export const invoiceEmailTemplate = {
subject: "MUELLERPRINTS Rechnungsnummer <%= invoiceNumber %>",
text: `Rechnungsnummer: <%= invoiceNumber %>
Ausstellungsdatum: <%= new Date(acceptedTermsAndConditionsAt).toLocaleDateString("de-DE") %>
-------------
Hallo <%= invoiceAddressStructured.givenName %>,
vielen Dank für deinen Einkauf bei MUELLERPRINTS.
Deine Bestellnummer: <%= id %>
Zur Bestellübersicht: <%= baseUrl %>/checkout/result/<%= uuid %>
Bestellte Artikel:
-------------
<% cart.forEach(function(item) { %>
<%= item.product.name %>
<% if (item.product.images && item.product.images.images && item.product.images.images.length > 0) { %>
Bild: <%= baseUrl %><%= item.product.images.images[0].formats.thumbnail.url %>
<% } %>
Menge: <%= item.count %>
Einzelpreis: <%= item.product.totalProductPrice.toFixed(2) %> EUR
Gesamtpreis: <%= (item.count * item.product.totalProductPrice).toFixed(2) %> EUR
<% }); %>
<% if (deliveryAddressStructured) { %>
Lieferadresse:
<%= deliveryAddressStructured.givenName %> <%= deliveryAddressStructured.familyName %>
<%= deliveryAddressStructured.streetAddress %>
<%= deliveryAddressStructured.postalCode %> <%= deliveryAddressStructured.addressLevel2 %>
<% } %>
Rechnung und Zahlung:
-------------
Rechnungsadresse:
<%= invoiceAddressStructured.givenName %> <%= invoiceAddressStructured.familyName %>
<%= invoiceAddressStructured.streetAddress %>
<%= invoiceAddressStructured.postalCode %> <%= invoiceAddressStructured.addressLevel2 %>
Zwischensumme: <%= subtotal.toFixed(2) %> EUR
Versandkosten: <%= delivery && delivery.price > 0 ? delivery.price.toFixed(2) + " EUR" : "KOSTENFREI" %>
Gesamtbetrag: <%= total.toFixed(2) %> EUR
inkl. MwSt. (19%): <%= VAT.toFixed(2) %> EUR
<% if (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.
------------------
MUELLERPRINTS
Max Müller
Rotenbergstraße 39, 70190 Stuttgart
T +49 (0)711/262 49 64
paperwork@muellerprints.de`,
html: `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rechnung für Bestellung <%= invoiceNumber %></title>
</head>
<body style="margin: 0; padding: 0; background-color: #f4f4f4; font-family: Arial, sans-serif;">
<!-- Main Container -->
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4; padding: 40px 20px;">
<tr>
<td align="center">
<!-- Content Container -->
<table role="presentation" style="max-width: 600px; width: 100%; border-collapse: collapse; background-color: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<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;">
</td>
</tr>
<!-- Main Content -->
<tr>
<td style="padding: 40px;">
<!-- Greeting -->
<h1 style="margin: 0 0 20px; color: #333; font-size: 24px; font-weight: normal;">
Vielen Dank für deinen Einkauf bei MUELLERPRINTS.
</h1>
<!-- Order Info -->
<div style="margin-bottom: 30px;">
<p style="color: #666; font-size: 16px; margin: 0 0 8px;">
Rechnungsnummer: <%= invoiceNumber %>
</p>
<p style="color: #666; font-size: 16px; margin: 0 0 8px;">
Ausstellungsdatum: <%= new Date(acceptedTermsAndConditionsAt).toLocaleDateString("de-DE") %>
</p>
<p style="color: #666; font-size: 16px; margin: 0 0 8px;">
Deine Bestellnummer: <a href="<%= baseUrl %>/checkout/result/<%= uuid %>"><%= id %></a>
</p>
<% if (deliveryTrackingNumber) { %>
<p style="color: #666; font-size: 16px; margin: 0 0 8px;">
Sendungsverfolgung: <%= deliveryTrackingNumber %>
</p>
<% } %>
</div>
<!-- Addresses -->
<table role="presentation" style="width: 100%; border-collapse: collapse; margin-bottom: 30px;">
<tr>
<td style="width: 50%; vertical-align: top; padding-right: 15px;">
<h3 style="margin: 0 0 10px; color: #333; font-size: 16px;">Rechnungsadresse:</h3>
<p style="color: #666; font-size: 14px; line-height: 1.5; white-space: pre-line; margin: 0;">
<%= invoiceAddressStructured.givenName %> <%= invoiceAddressStructured.familyName %>
<%= invoiceAddressStructured.streetAddress %>
<%= invoiceAddressStructured.postalCode %> <%= invoiceAddressStructured.addressLevel2 %>
</p>
</td>
<% if (deliveryAddress) { %>
<td style="width: 50%; vertical-align: top; padding-left: 15px;">
<h3 style="margin: 0 0 10px; color: #333; font-size: 16px;">Lieferadresse:</h3>
<p style="color: #666; font-size: 14px; line-height: 1.5; white-space: pre-line; margin: 0;">
<%= deliveryAddress %>
</p>
</td>
<% } %>
</tr>
</table>
<!-- Order Details -->
<h3 style="margin: 0 0 10px; color: #333; font-size: 16px;">Bestellte Artikel:</h3>
<table role="presentation" style="width: 100%; border-collapse: collapse; margin-bottom: 30px;">
<thead>
<tr>
<th style="text-align: left; padding: 12px; background-color: #f8f8f8; border-bottom: 2px solid #eee; color: #333; font-size: 14px; width: 80px;"></th>
<th style="text-align: left; padding: 12px; background-color: #f8f8f8; border-bottom: 2px solid #eee; color: #333; font-size: 14px;">Artikel</th>
<th style="text-align: center; padding: 12px; background-color: #f8f8f8; border-bottom: 2px solid #eee; color: #333; font-size: 14px;">Menge</th>
<th style="text-align: right; padding: 12px; background-color: #f8f8f8; border-bottom: 2px solid #eee; color: #333; font-size: 14px;">Einzelpreis</th>
<th style="text-align: right; padding: 12px; background-color: #f8f8f8; border-bottom: 2px solid #eee; color: #333; font-size: 14px;">Gesamtpreis</th>
</tr>
</thead>
<tbody>
<% cart.forEach(function(item) { %>
<tr>
<td style="padding: 12px; border-bottom: 1px solid #eee; vertical-align: middle;">
<% if (item.product.images && item.product.images.images && item.product.images.images.length > 0) { %>
<img src="<%= baseUrl %><%= item.product.images.images[0].formats.thumbnail.url %>" alt="<%= item.product.name %>" style="width: 60px; height: auto; object-fit: cover;">
<% } else { %>
<div style="background-color: #f8f8f8; width: 60px; height: 60px;"></div>
<% } %>
</td>
<td style="padding: 12px; border-bottom: 1px solid #eee; color: #666; font-size: 14px;"><%= item.product.name %></td>
<td style="padding: 12px; border-bottom: 1px solid #eee; color: #666; font-size: 14px; text-align: center;"><%= item.count %></td>
<td style="padding: 12px; border-bottom: 1px solid #eee; color: #666; font-size: 14px; text-align: right;"><%= item.product.totalProductPrice.toFixed(2) %> EUR</td>
<td style="padding: 12px; border-bottom: 1px solid #eee; color: #666; font-size: 14px; text-align: right;"><%= (item.count * item.product.totalProductPrice).toFixed(2) %> EUR</td>
</tr>
<% }); %>
</tbody>
</table>
<!-- Totals -->
<h3 style="margin: 0 0 10px; color: #333; font-size: 16px;">Rechnung und Zahlung:</h3>
<table role="presentation" style="width: 100%; border-collapse: collapse; margin-bottom: 30px;">
<tr>
<td style="text-align: right; padding: 4px 0;">
<span style="color: #666; font-size: 14px;">Zwischensumme:</span>
<span style="color: #333; font-size: 14px; margin-left: 20px;"><%= subtotal.toFixed(2) %> EUR</span>
</td>
</tr>
<tr>
<td style="text-align: right; padding: 4px 0;">
<span style="color: #666; font-size: 14px;">Versandkosten:</span>
<span style="color: #333; font-size: 14px; margin-left: 20px;"><%= delivery && delivery.price > 0 ? delivery.price.toFixed(2) + " EUR" : "KOSTENFREI" %></span>
</td>
</tr>
<tr>
<td style="text-align: right; padding: 12px 0;">
<span style="color: #333; font-size: 18px; font-weight: bold;">Gesamtbetrag:</span>
<span style="color: #333; font-size: 18px; font-weight: bold; margin-left: 20px;"><%= total.toFixed(2) %> EUR</span>
</td>
</tr>
<tr>
<td style="text-align: right; padding: 4px 0;">
<span style="color: #666; font-size: 14px;">inkl. MwSt. (19%):</span>
<span style="color: #333; font-size: 14px; margin-left: 20px;"><%= VAT.toFixed(2) %> EUR</span>
</td>
</tr>
</table>
<% if (baseUrl && invoice.url) { %>
<!-- Attachment Note -->
<div style="background-color: #f8f8f8; border-radius: 4px; padding: 20px; margin-bottom: 30px;">
<p style="margin: 0; color: #666; font-size: 14px;">
Deine Rechnung kannst du jederzeit über diesen Link herunterladen:
<a href="<%= baseUrl %><%= invoice.url %>">Rechnung <%= invoiceNumber %> herunterladen</a>
</p>
</div>
<% } %>
<p style="color: #666; font-size: 14px; margin: 20px 0;">
Vielen Dank, dass du dich für MUELLERPRINTS entschieden hast.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="padding: 30px 40px; background-color: #f8f8f8; border-top: 1px solid #eee;">
<table role="presentation" style="width: 100%; border-collapse: collapse;">
<tr>
<td style="color: #666; font-size: 12px; line-height: 1.5;">
<p style="margin: 0 0 10px;">
MUELLERPRINTS<br>
Max Müller<br>
Rotenbergstraße 39, 70190 Stuttgart<br>
T +49 (0)711/262 49 64<br>
paperwork@muellerprints.de
</p>
</td>
<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;">
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- Email Preferences Footer -->
<table role="presentation" style="max-width: 600px; width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 20px 0; text-align: center; color: #999; font-size: 12px;">
<p style="margin: 0;">
Diese E-Mail wurde gesendet an <%= email %>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`
};
export const deliveryNoteEmailTemplate = {
subject: "Your Delivery Note <%= deliveryNoteNumber %> from MUELLERPRINTS",
text: `Dear Customer,
Thank you for your Your delivery note <%= deliveryNoteNumber %> is attached to this email.
Delivery Details:
----------------
<% if (deliveryTrackingNumber) { %>Tracking Number: <%= deliveryTrackingNumber %><% } %>
Ordered Items:
----------------
<% cart.forEach(function(item) { %>
<%= item.product.name %>
<% if (item.product.images && item.product.images.images && item.product.images.images.length > 0) { %>
Image: <%= baseUrl %><%= item.product.images.images[0].formats.thumbnail.url %>
<% } %>
Quantity: <%= item.count %>
Price: <%= item.product.totalProductPrice.toFixed(2) %> EUR
Total: <%= (item.count * item.product.totalProductPrice).toFixed(2) %> EUR
<% }); %>
Delivery Address:
<%= deliveryAddressStructured.givenName %> <%= deliveryAddressStructured.familyName %>
<%= deliveryAddressStructured.streetAddress %>
<%= deliveryAddressStructured.postalCode %> <%= deliveryAddressStructured.addressLevel2 %>
If you have any questions about your delivery, please don't hesitate to contact us.
Best regards,
MUELLERPRINTS PAPERWORK
------------------
MUELLERPRINTS PAPERWORK
Max Müller
Rotenbergstraße 39, 70190 Stuttgart
T +49 (0)711/262 49 64
paperwork@muellerprints.de`,
html: `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Delivery Note #<%= deliveryNoteNumber %></title>
</head>
<body style="margin: 0; padding: 0; background-color: #f4f4f4; font-family: Arial, sans-serif;">
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4; padding: 40px 0;">
<tr>
<td align="center">
<table role="presentation" style="max-width: 600px; width: 100%; border-collapse: collapse; background-color: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<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;">
</td>
</tr>
<!-- Main Content -->
<tr>
<td style="padding: 40px;">
<h1 style="margin: 0 0 20px; color: #333; font-size: 24px; font-weight: normal;">
Your Delivery Information
</h1>
<div style="margin-bottom: 30px;">
<p style="color: #666; font-size: 16px; margin: 0 0 8px;">
Delivery Note #<%= deliveryNoteNumber %>
</p>
<% if (deliveryTrackingNumber) { %>
<p style="color: #666; font-size: 16px; margin: 0 0 8px;">
Tracking Number: <strong><%= deliveryTrackingNumber %></strong>
</p>
<% } %>
</div>
<!-- Addresses -->
<table role="presentation" style="width: 100%; border-collapse: collapse; margin-bottom: 30px;">
<tr>
<td style="width: 50%; vertical-align: top; padding-right: 15px;">
<h3 style="margin: 0 0 10px; color: #333; font-size: 16px;">Billing Address:</h3>
<p style="color: #666; font-size: 14px; line-height: 1.5; white-space: pre-line; margin: 0;">
<%= invoiceAddressStructured.givenName %> <%= invoiceAddressStructured.familyName %>
<%= invoiceAddressStructured.streetAddress %>
<%= invoiceAddressStructured.postalCode %> <%= invoiceAddressStructured.addressLevel2 %>
</p>
</td>
<% if (deliveryAddress && deliveryAddress !== invoiceAddress) { %>
<td style="width: 50%; vertical-align: top; padding-left: 15px;">
<h3 style="margin: 0 0 10px; color: #333; font-size: 16px;">Delivery Address:</h3>
<p style="color: #666; font-size: 14px; line-height: 1.5; white-space: pre-line; margin: 0;">
<%= deliveryAddress %>
</p>
</td>
<% } %>
</tr>
</table>
<!-- Order Details -->
<table role="presentation" style="width: 100%; border-collapse: collapse; margin-bottom: 30px;">
<thead>
<tr>
<th style="text-align: left; padding: 12px; background-color: #f8f8f8; border-bottom: 2px solid #eee; color: #333; font-size: 14px; width: 80px;"></th>
<th style="text-align: left; padding: 12px; background-color: #f8f8f8; border-bottom: 2px solid #eee; color: #333; font-size: 14px;">Item</th>
<th style="text-align: center; padding: 12px; background-color: #f8f8f8; border-bottom: 2px solid #eee; color: #333; font-size: 14px;">Quantity</th>
<th style="text-align: right; padding: 12px; background-color: #f8f8f8; border-bottom: 2px solid #eee; color: #333; font-size: 14px;">Price</th>
<th style="text-align: right; padding: 12px; background-color: #f8f8f8; border-bottom: 2px solid #eee; color: #333; font-size: 14px;">Total</th>
</tr>
</thead>
<tbody>
<% cart.forEach(function(item) { %>
<tr>
<td style="padding: 12px; border-bottom: 1px solid #eee; vertical-align: middle;">
<% if (item.product.images && item.product.images.images && item.product.images.images.length > 0) { %>
<img src="<%= baseUrl %><%= item.product.images.images[0].formats.thumbnail.url %>" alt="<%= item.product.name %>" style="width: 60px; height: auto; object-fit: cover;">
<% } else { %>
<div style="background-color: #f8f8f8; width: 60px; height: 60px;"></div>
<% } %>
</td>
<td style="padding: 12px; border-bottom: 1px solid #eee; color: #666; font-size: 14px;"><%= item.product.name %></td>
<td style="padding: 12px; border-bottom: 1px solid #eee; color: #666; font-size: 14px; text-align: center;"><%= item.count %></td>
<td style="padding: 12px; border-bottom: 1px solid #eee; color: #666; font-size: 14px; text-align: right;"><%= item.product.totalProductPrice.toFixed(2) %> EUR</td>
<td style="padding: 12px; border-bottom: 1px solid #eee; color: #666; font-size: 14px; text-align: right;"><%= (item.count * item.product.totalProductPrice).toFixed(2) %> EUR</td>
</tr>
<% }); %>
</tbody>
</table>
<!-- Totals -->
<table role="presentation" style="width: 100%; border-collapse: collapse; margin-bottom: 30px;">
<tr>
<td style="text-align: right; padding: 4px 0;">
<span style="color: #666; font-size: 14px;">Subtotal:</span>
<span style="color: #333; font-size: 14px; margin-left: 20px;"><%= subtotal.toFixed(2) %> EUR</span>
</td>
</tr>
<tr>
<td style="text-align: right; padding: 4px 0;">
<span style="color: #666; font-size: 14px;">VAT (19%):</span>
<span style="color: #333; font-size: 14px; margin-left: 20px;"><%= VAT.toFixed(2) %> EUR</span>
</td>
</tr>
<tr>
<td style="text-align: right; padding: 12px 0;">
<span style="color: #333; font-size: 18px; font-weight: bold;">Total:</span>
<span style="color: #333; font-size: 18px; font-weight: bold; margin-left: 20px;"><%= total.toFixed(2) %> EUR</span>
</td>
</tr>
</table>
<!-- Attachment Note -->
<div style="background-color: #f8f8f8; border-radius: 4px; padding: 20px; margin-bottom: 30px;">
<p style="margin: 0; color: #666; font-size: 14px;">
Your invoice is attached to this email. Please keep it for your records.
</p>
</div>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="padding: 30px 40px; background-color: #f8f8f8; border-top: 1px solid #eee;">
<table role="presentation" style="width: 100%; border-collapse: collapse;">
<tr>
<td style="color: #666; font-size: 12px; line-height: 1.5;">
<p style="margin: 0 0 10px;">
MUELLERPRINTS PAPERWORK<br>
Max Müller<br>
Rotenbergstraße 39, 70190 Stuttgart<br>
T +49 (0)711/262 49 64<br>
paperwork@muellerprints.de
</p>
</td>
<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;">
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- Email Preferences Footer -->
<table role="presentation" style="max-width: 600px; width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 20px 0; text-align: center; color: #999; font-size: 12px;">
<p style="margin: 0;">
This email was sent to <%= email %>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`
};

View File

@@ -0,0 +1,273 @@
import { Strapi } from "@strapi/strapi";
import { productDefaultParams } from "../api/product/services/product";
import { ProductCover, ProductImage, ProductPages, ProductPattern, ProductRuling } from "../../types";
import { formatSlug } from "./formatSlug";
import { ID } from "@strapi/database/dist/types";
type ComponentEvent<T> = {
action: "create" | "update" | "delete";
params: {
data?: T;
where?: { id: number };
select?: string[];
populate?: string[];
};
state?: {
attribute?: string;
};
result?: T;
};
export const createComponentLifecycle = <T extends ProductCover | ProductPattern | ProductPages | ProductRuling | ProductImage>(
componentType: "cover" | "pattern" | "pages" | "ruling" | "image"
) => ({
async afterCreate(event: ComponentEvent<T>) {
const { result } = event;
strapi.log.verbose(`app:v:${componentType}-lifecycle: Created ${JSON.stringify({ componentType, id: result.id })}`);
if (componentType === "image") {
return;
}
try {
await generateNewProducts(strapi, componentType, parseInt(result.id as string));
} catch (error) {
strapi.log.error(`app:e:${componentType}-lifecycle: Error generating products ${JSON.stringify({ error, componentId: result.id })}`);
}
},
async afterUpdate(event: ComponentEvent<T>) {
const { result, params } = event;
// Only update products if price changed
try {
await updateRelatedProducts(strapi, componentType, parseInt(result.id as string));
} catch (error) {
strapi.log.error(
`app:e:${componentType}-lifecycle: Error updating products
${JSON.stringify({
error,
componentId: result.id
})}`
);
}
},
async beforeDelete(event: ComponentEvent<T>) {
const { id } = event.params.where;
try {
// Find all products using this component
strapi.log.debug(
`app:d:${componentType}-lifecycle: Deleting related products`,
JSON.stringify({
filters: {
[componentType]: { id }
}
})
);
const products = await strapi.entityService.findMany("api::product.product", {
filters: {
[componentType]: { id }
}
});
// Delete related products
// TODO: Deleting too many products
if (products.length) {
for (const product of products) {
await strapi.entityService.delete("api::product.product", product.id);
}
}
} catch (error) {
strapi.log.error(
`app:e:${componentType}-lifecycle: Error deleting related products`,
JSON.stringify({
error,
componentId: id
})
);
throw error; // Propagate error to prevent component deletion if products can't be deleted
}
}
});
async function generateNewProducts(strapi: Strapi, componentType: string, componentId: number) {
// Fetch all necessary components
const [covers, patterns, pages, rulings] = await Promise.all([
componentType === "cover"
? [await strapi.entityService.findOne("api::product-cover.product-cover", componentId)]
: strapi.entityService.findMany("api::product-cover.product-cover"),
componentType === "pattern"
? [await strapi.entityService.findOne("api::product-pattern.product-pattern", componentId)]
: strapi.entityService.findMany("api::product-pattern.product-pattern"),
componentType === "pages"
? [await strapi.entityService.findOne("api::product-page.product-page", componentId)]
: strapi.entityService.findMany("api::product-page.product-page"),
componentType === "ruling"
? [await strapi.entityService.findOne("api::product-ruling.product-ruling", componentId)]
: strapi.entityService.findMany("api::product-ruling.product-ruling")
]);
strapi.log.debug(`app:d:lifecycle-factory ${JSON.stringify({ covers, patterns, pages, rulings })}`);
// Generate new combinations
for (const cover of covers) {
for (const pattern of patterns) {
for (const page of pages) {
for (const ruling of rulings) {
const name = `${pattern.name} ${cover.name} · ${page.name} · ${ruling.name}`;
const slug = formatSlug(name);
const existing = await strapi.entityService.findMany("api::product.product", {
filters: { slug }
});
if (existing.length) {
strapi.log.warn(`app:w:lifecycle-factory: Product "${name}" already exists with ID ${existing[0]?.id}`);
strapi.log.debug(`app:d:lifecycle-factory: ${JSON.stringify({ existing })}`);
continue;
}
const categoriesFilter = {
filters: {
product_cover: cover.id,
product_pattern: pattern.id
}
};
const categories = await strapi.service("api::product-category.product-category").categories(categoriesFilter);
if (categories) {
strapi.log.debug(`app:d:lifecycle-factory: Attaching product-image with ID ${categories[0]?.id}`);
} else {
strapi.log.warn("app:w:lifecycle-factory: Product-image not found");
}
const images = categories ? categories[0]?.id : null;
const data = {
name,
slug,
images,
cover: cover.id,
pattern: pattern.id,
pages: page.id,
ruling: ruling.id,
// TODO: Might be strapi v5 syntax
// https://docs.strapi.io/cms/migration/v4-to-v5/breaking-changes/publication-state-removed
status: "draft",
publishedAt: null
};
await strapi.entityService.create("api::product.product", {
data
});
}
}
}
}
}
async function updateRelatedProducts(strapi: Strapi, componentType: string, componentId: number) {
// Find all related products with full population
const products = await strapi.entityService.findMany("api::product.product", {
filters: {
[componentType === "image" ? "images" : componentType]: { id: componentId }
},
...productDefaultParams,
publicationState: "preview"
});
// @ts-expect-error
const parent = await strapi.entityService.findOne(`api::product-${componentType}.product-${componentType}`, componentId);
strapi.log.info(`app:i:lifecycle-factory: Updating ${products.length} products related to ${componentType} with ID ${componentId}`);
for (const product of products) {
try {
// Get the current components for this product
const cover = product.cover?.id;
const pattern = product.pattern?.id;
const pages = product.pages?.id;
const ruling = product.ruling?.id;
// Determine if this product should still exist based on component availability
const componentsExist = await validateComponents(strapi, cover, pattern, pages, ruling);
if (!componentsExist) {
// Delete products with missing components
strapi.log.info(`app:i:lifecycle-factory: Deleting product ${product.id} due to missing components`);
await strapi.entityService.delete("api::product.product", product.id);
continue;
}
// Regenerate product name and slug based on current components
const [coverData, patternData, pagesData, rulingData] = await Promise.all([
strapi.entityService.findOne("api::product-cover.product-cover", cover),
strapi.entityService.findOne("api::product-pattern.product-pattern", pattern),
strapi.entityService.findOne("api::product-page.product-page", pages),
strapi.entityService.findOne("api::product-ruling.product-ruling", ruling)
]);
const name = `${patternData.name} ${coverData.name} · ${pagesData.name} · ${rulingData.name}`;
const slug = formatSlug(name);
// Find the correct category/product-image
const categoriesFilter = {
filters: {
product_cover: cover,
product_pattern: pattern
}
};
const categories = await strapi.service("api::product-category.product-category").categories(categoriesFilter);
const images = categories?.length ? categories[0]?.id : null;
// Check if a product with this name already exists (but is not this one)
const existing = await strapi.entityService.findMany("api::product.product", {
filters: {
slug,
id: { $ne: product.id }
}
});
if (existing.length) {
// Duplicate found - delete this one if it's newer
if (new Date(product.createdAt) > new Date(existing[0].createdAt)) {
strapi.log.warn(`app:w:lifecycle-factory: Deleting duplicate product ${product.id} in favor of existing ${existing[0].id}`);
await strapi.entityService.delete("api::product.product", product.id);
}
continue;
}
// Update the product with refreshed data
const updateData = {
name,
slug,
images
// Don't update the publishedAt status here, as it's handled by updateRelatedProductsStatus
};
strapi.log.verbose(`app:v:lifecycle-factory: Updating product ${product.id} with ${JSON.stringify(updateData)}`);
await strapi.entityService.update("api::product.product", product.id, {
data: updateData
});
} catch (error) {
strapi.log.error(`app:e:lifecycle-factory: Error updating product ${product.id}`, error);
}
}
strapi.log.info(`app:i:lifecycle-factory: ✔ Finished updating products related to ${componentType} ${componentId}`);
}
// Helper function to validate that all components of a product exist
async function validateComponents(strapi: Strapi, coverId: ID, patternId: ID, pagesId: ID, rulingId: ID): Promise<boolean> {
try {
const [cover, pattern, pages, ruling] = await Promise.all([
coverId ? strapi.entityService.findOne("api::product-cover.product-cover", coverId) : null,
patternId ? strapi.entityService.findOne("api::product-pattern.product-pattern", patternId) : null,
pagesId ? strapi.entityService.findOne("api::product-page.product-page", pagesId) : null,
rulingId ? strapi.entityService.findOne("api::product-ruling.product-ruling", rulingId) : null
]);
return !!(cover && pattern && pages && ruling);
} catch (error) {
strapi.log.error(`app:e:lifecycle-factory: Error validating components`, error);
return false;
}
}

18
src/utils/formatSlug.ts Normal file
View File

@@ -0,0 +1,18 @@
export const formatSlug = (str: string): string => {
str = str.replace(/^\s+|\s+$/g, ""); // trim
str = str.toLowerCase();
// remove accents, swap ñ for n, etc
var from = "àáäâèéëêìíïîòóöôùúüûñç·/_,:;";
var to = "aaaaeeeeiiiioooouuuunc------";
for (var i = 0, l = from.length; i < l; i++) {
str = str.replace(new RegExp(from.charAt(i), "g"), to.charAt(i));
}
str = str
.replace(/[^a-z0-9 -]/g, "") // remove invalid chars
.replace(/\s+/g, "-") // collapse whitespace and replace by -
.replace(/-+/g, "-"); // collapse dashes
return str;
};