feat: extract cms from mp/cms — initial libreshop/cms
Some checks failed
Build and publish / build (push) Failing after 17s
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:
9
src/admin/app.ts
Normal file
9
src/admin/app.ts
Normal 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
5
src/admin/tsconfig.json
Normal 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
0
src/api/.gitkeep
Normal file
28
src/api/customer/content-types/customer/schema.json
Normal file
28
src/api/customer/content-types/customer/schema.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/customer/controllers/customer.ts
Normal file
7
src/api/customer/controllers/customer.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* customer controller
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreController("api::customer.customer");
|
||||
508
src/api/customer/documentation/1.0.0/customer.json
Normal file
508
src/api/customer/documentation/1.0.0/customer.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/customer/routes/customer.ts
Normal file
7
src/api/customer/routes/customer.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* customer router
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreRouter("api::customer.customer");
|
||||
108
src/api/customer/services/customer.ts
Normal file
108
src/api/customer/services/customer.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}));
|
||||
27
src/api/delivery/content-types/delivery/schema.json
Normal file
27
src/api/delivery/content-types/delivery/schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/delivery/controllers/delivery.ts
Normal file
7
src/api/delivery/controllers/delivery.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* delivery controller
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreController("api::delivery.delivery");
|
||||
508
src/api/delivery/documentation/1.0.0/delivery.json
Normal file
508
src/api/delivery/documentation/1.0.0/delivery.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/delivery/routes/delivery.ts
Normal file
7
src/api/delivery/routes/delivery.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* delivery router
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreRouter("api::delivery.delivery");
|
||||
7
src/api/delivery/services/delivery.ts
Normal file
7
src/api/delivery/services/delivery.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* delivery service
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreService("api::delivery.delivery");
|
||||
35
src/api/legal/content-types/legal/schema.json
Normal file
35
src/api/legal/content-types/legal/schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/api/legal/controllers/legal.ts
Normal file
38
src/api/legal/controllers/legal.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
}));
|
||||
325
src/api/legal/documentation/1.0.0/legal.json
Normal file
325
src/api/legal/documentation/1.0.0/legal.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/api/legal/routes/legal-sitemap.ts
Normal file
13
src/api/legal/routes/legal-sitemap.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
method: "GET",
|
||||
path: "/sitemap/legal",
|
||||
handler: "legal.sitemap",
|
||||
config: {
|
||||
auth: false,
|
||||
policies: []
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
7
src/api/legal/routes/legal.ts
Normal file
7
src/api/legal/routes/legal.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* legal router
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreRouter("api::legal.legal");
|
||||
7
src/api/legal/services/legal.ts
Normal file
7
src/api/legal/services/legal.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* legal service
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreService("api::legal.legal");
|
||||
170
src/api/order/content-types/order/lifecycles.ts
Normal file
170
src/api/order/content-types/order/lifecycles.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
128
src/api/order/content-types/order/schema.json
Normal file
128
src/api/order/content-types/order/schema.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
307
src/api/order/controllers/order.ts
Normal file
307
src/api/order/controllers/order.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}));
|
||||
1344
src/api/order/documentation/1.0.0/order.json
Normal file
1344
src/api/order/documentation/1.0.0/order.json
Normal file
File diff suppressed because it is too large
Load Diff
124
src/api/order/routes/order.ts
Normal file
124
src/api/order/routes/order.ts
Normal 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: []
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
269
src/api/order/services/order.ts
Normal file
269
src/api/order/services/order.ts
Normal 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
|
||||
});
|
||||
26
src/api/payment/content-types/payment/schema.json
Normal file
26
src/api/payment/content-types/payment/schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/payment/controllers/payment.ts
Normal file
7
src/api/payment/controllers/payment.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* payment controller
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreController("api::payment.payment");
|
||||
508
src/api/payment/documentation/1.0.0/payment.json
Normal file
508
src/api/payment/documentation/1.0.0/payment.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/payment/routes/payment.ts
Normal file
7
src/api/payment/routes/payment.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* payment router
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreRouter("api::payment.payment");
|
||||
7
src/api/payment/services/payment.ts
Normal file
7
src/api/payment/services/payment.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* payment service
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreService("api::payment.payment");
|
||||
213
src/api/product-category/controllers/product-category.ts
Normal file
213
src/api/product-category/controllers/product-category.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
73
src/api/product-category/routes/product-category.ts
Normal file
73
src/api/product-category/routes/product-category.ts
Normal 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: []
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
80
src/api/product-category/services/product-category.ts
Normal file
80
src/api/product-category/services/product-category.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ProductCover } from "../../../../../types";
|
||||
import { createComponentLifecycle } from "../../../../utils/createComponentLifecycle";
|
||||
export default createComponentLifecycle<ProductCover>("cover");
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/product-cover/controllers/product-cover.ts
Normal file
7
src/api/product-cover/controllers/product-cover.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* product-cover controller
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreController("api::product-cover.product-cover");
|
||||
508
src/api/product-cover/documentation/1.0.0/product-cover.json
Normal file
508
src/api/product-cover/documentation/1.0.0/product-cover.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/product-cover/routes/product-cover.ts
Normal file
7
src/api/product-cover/routes/product-cover.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* product-cover router
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreRouter("api::product-cover.product-cover");
|
||||
7
src/api/product-cover/services/product-cover.ts
Normal file
7
src/api/product-cover/services/product-cover.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* product-cover service
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreService("api::product-cover.product-cover");
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ProductImage } from "../../../../../types";
|
||||
import { createComponentLifecycle } from "../../../../utils/createComponentLifecycle";
|
||||
export default createComponentLifecycle<ProductImage>("image");
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/product-image/controllers/product-image.ts
Normal file
7
src/api/product-image/controllers/product-image.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* product-image controller
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreController("api::product-image.product-image");
|
||||
508
src/api/product-image/documentation/1.0.0/product-image.json
Normal file
508
src/api/product-image/documentation/1.0.0/product-image.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/product-image/routes/product-image.ts
Normal file
7
src/api/product-image/routes/product-image.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* product-image router
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreRouter("api::product-image.product-image");
|
||||
7
src/api/product-image/services/product-image.ts
Normal file
7
src/api/product-image/services/product-image.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* product-image service
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreService("api::product-image.product-image");
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ProductPages } from "../../../../../types";
|
||||
import { createComponentLifecycle } from "../../../../utils/createComponentLifecycle";
|
||||
export default createComponentLifecycle<ProductPages>("pages");
|
||||
42
src/api/product-page/content-types/product-page/schema.json
Normal file
42
src/api/product-page/content-types/product-page/schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/product-page/controllers/product-page.ts
Normal file
7
src/api/product-page/controllers/product-page.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* product-page controller
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreController("api::product-page.product-page");
|
||||
508
src/api/product-page/documentation/1.0.0/product-page.json
Normal file
508
src/api/product-page/documentation/1.0.0/product-page.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/product-page/routes/product-page.ts
Normal file
7
src/api/product-page/routes/product-page.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* product-page router
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreRouter("api::product-page.product-page");
|
||||
7
src/api/product-page/services/product-page.ts
Normal file
7
src/api/product-page/services/product-page.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* product-page service
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreService("api::product-page.product-page");
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ProductPattern } from "../../../../../types";
|
||||
import { createComponentLifecycle } from "../../../../utils/createComponentLifecycle";
|
||||
export default createComponentLifecycle<ProductPattern>("pattern");
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/product-pattern/controllers/product-pattern.ts
Normal file
7
src/api/product-pattern/controllers/product-pattern.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* product-pattern controller
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreController("api::product-pattern.product-pattern");
|
||||
508
src/api/product-pattern/documentation/1.0.0/product-pattern.json
Normal file
508
src/api/product-pattern/documentation/1.0.0/product-pattern.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/product-pattern/routes/product-pattern.ts
Normal file
7
src/api/product-pattern/routes/product-pattern.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* product-pattern router
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreRouter("api::product-pattern.product-pattern");
|
||||
7
src/api/product-pattern/services/product-pattern.ts
Normal file
7
src/api/product-pattern/services/product-pattern.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* product-pattern service
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreService("api::product-pattern.product-pattern");
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ProductRuling } from "../../../../../types";
|
||||
import { createComponentLifecycle } from "../../../../utils/createComponentLifecycle";
|
||||
export default createComponentLifecycle<ProductRuling>("ruling");
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/product-ruling/controllers/product-ruling.ts
Normal file
7
src/api/product-ruling/controllers/product-ruling.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* product-ruling controller
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreController("api::product-ruling.product-ruling");
|
||||
508
src/api/product-ruling/documentation/1.0.0/product-ruling.json
Normal file
508
src/api/product-ruling/documentation/1.0.0/product-ruling.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/product-ruling/routes/product-ruling.ts
Normal file
7
src/api/product-ruling/routes/product-ruling.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* product-ruling router
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreRouter("api::product-ruling.product-ruling");
|
||||
7
src/api/product-ruling/services/product-ruling.ts
Normal file
7
src/api/product-ruling/services/product-ruling.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* product-ruling service
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreService("api::product-ruling.product-ruling");
|
||||
83
src/api/product/content-types/product/lifecycles.ts
Normal file
83
src/api/product/content-types/product/lifecycles.ts
Normal 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;
|
||||
// }
|
||||
};
|
||||
59
src/api/product/content-types/product/schema.json
Normal file
59
src/api/product/content-types/product/schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
442
src/api/product/controllers/product.ts
Normal file
442
src/api/product/controllers/product.ts
Normal 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>;
|
||||
}
|
||||
754
src/api/product/documentation/1.0.0/product.json
Normal file
754
src/api/product/documentation/1.0.0/product.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/api/product/routes/product.ts
Normal file
90
src/api/product/routes/product.ts
Normal 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: []
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
153
src/api/product/services/product.ts
Normal file
153
src/api/product/services/product.ts
Normal 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"
|
||||
};
|
||||
36
src/api/vat/content-types/vat/schema.json
Normal file
36
src/api/vat/content-types/vat/schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/vat/controllers/vat.ts
Normal file
7
src/api/vat/controllers/vat.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* vat controller
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreController("api::vat.vat");
|
||||
325
src/api/vat/documentation/1.0.0/vat.json
Normal file
325
src/api/vat/documentation/1.0.0/vat.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/vat/routes/vat.ts
Normal file
7
src/api/vat/routes/vat.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* vat router
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreRouter("api::vat.vat");
|
||||
7
src/api/vat/services/vat.ts
Normal file
7
src/api/vat/services/vat.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* vat service
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreService("api::vat.vat");
|
||||
22
src/api/website/content-types/website/schema.json
Normal file
22
src/api/website/content-types/website/schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/api/website/controllers/website.ts
Normal file
7
src/api/website/controllers/website.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* website controller
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreController("api::website.website");
|
||||
7
src/api/website/routes/website.ts
Normal file
7
src/api/website/routes/website.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* website router
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreRouter("api::website.website");
|
||||
7
src/api/website/services/website.ts
Normal file
7
src/api/website/services/website.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* website service
|
||||
*/
|
||||
|
||||
import { factories } from "@strapi/strapi";
|
||||
|
||||
export default factories.createCoreService("api::website.website");
|
||||
28
src/components/address/structured-address.json
Normal file
28
src/components/address/structured-address.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/components/products/cart.json
Normal file
21
src/components/products/cart.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/components/websites/website-in-menu.json
Normal file
26
src/components/websites/website-in-menu.json
Normal 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
0
src/extensions/.gitkeep
Normal file
30
src/extensions/documentation/config/settings.json
Normal file
30
src/extensions/documentation/config/settings.json
Normal 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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
30776
src/extensions/documentation/documentation/1.0.0/full_documentation.json
Normal file
30776
src/extensions/documentation/documentation/1.0.0/full_documentation.json
Normal file
File diff suppressed because it is too large
Load Diff
70
src/extensions/documentation/public/index.html
Normal file
70
src/extensions/documentation/public/index.html
Normal file
File diff suppressed because one or more lines are too long
18
src/index.ts
Normal file
18
src/index.ts
Normal 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
92
src/services/MailApi.ts
Normal 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
165
src/services/PayPalApi.ts
Normal 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
56
src/services/PdfApi.ts
Normal 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"
|
||||
}
|
||||
});
|
||||
441
src/templates/emailTemplates.ts
Normal file
441
src/templates/emailTemplates.ts
Normal 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>`
|
||||
};
|
||||
273
src/utils/createComponentLifecycle.ts
Normal file
273
src/utils/createComponentLifecycle.ts
Normal 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
18
src/utils/formatSlug.ts
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user