feat: extract cms from mp/cms — initial libreshop/cms
Some checks failed
Build and publish / build (push) Failing after 17s

Source moved verbatim from mp/cms/ on 2026-04-29; mp was the first
concrete adapter consuming the libreshop toolkit. Builds and publishes
git.librete.ch/libreshop/cms on every main / v* push via the standard
.gitea/workflows/build.yml shared across libreshop components.
This commit is contained in:
Michael Czechowski
2026-04-29 17:48:30 +02:00
commit 32a296baf2
127 changed files with 44618 additions and 0 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
.tmp
.cache
.git
build
node_modules
.env
public/uploads/*
package-lock.json

16
.editorconfig Normal file
View File

@@ -0,0 +1,16 @@
root = true
[*]
indent_style = tab
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{package.json,*.yml}]
indent_style = tab
indent_size = 4
[*.md]
trim_trailing_whitespace = false

View File

@@ -0,0 +1,55 @@
name: Build and publish
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
# Required secrets:
# REGISTRY git.librete.ch
# REGISTRY_USER libretech-bot
# REGISTRY_PASS bot PAT (write:package; bot is in libreshop Owners team)
# Required variable:
# PUBLISH_ENABLED "true" to actually push (off = build-only on PRs)
#
# Image: git.librete.ch/libreshop/cms
# main pushes → :main + :sha-<short>
# tag pushes → :<tag> + :latest
jobs:
build:
runs-on: ubuntu-latest
container:
image: git.librete.ch/libretech/runner-image:v1
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Login (only when publishing)
if: ${{ vars.PUBLISH_ENABLED == 'true' }}
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASS }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.REGISTRY }}/libreshop/cms
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,format=short
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
- uses: docker/build-push-action@v6
with:
context: .
push: ${{ vars.PUBLISH_ENABLED == 'true' && github.event_name == 'push' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

130
.gitignore vendored Normal file
View File

@@ -0,0 +1,130 @@
############################
# OS X
############################
.DS_Store
.AppleDouble
.LSOverride
Icon
.Spotlight-V100
.Trashes
._*
############################
# Linux
############################
*~
############################
# Windows
############################
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msm
*.msp
############################
# Packages
############################
*.7z
*.csv
*.dat
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
*.com
*.class
*.dll
*.exe
*.o
*.seed
*.so
*.swo
*.swp
*.swn
*.swm
*.out
*.pid
############################
# Logs and databases
############################
.tmp
*.log
*.sql
*.sqlite
*.sqlite3
database/dump.json
############################
# Misc.
############################
*#
ssl
.idea
nbproject
public/uploads/*
!public/uploads/.gitkeep
.strapi
############################
# Node.js
############################
lib-cov
lcov.info
pids
logs
results
node_modules
.node_history
############################
# Tests
############################
coverage
############################
# Strapi
############################
.env*
license.txt
exports
*.cache
dist
build
.strapi-updater.json
# libreshop additions
.env
.env.local
.env.*.local
node_modules/
.nuxt/
.output/
.cache/
.parcel-cache/
dist/
build/
coverage/
logs/
tmp/

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v22.14.0

9
.prettierrc.json Normal file
View File

@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": true,
"tabWidth": 4,
"singleQuote": false,
"printWidth": 150,
"trailingComma": "none",
"useTabs": true
}

8
CHANGELOG.md Normal file
View File

@@ -0,0 +1,8 @@
# Changelog
All notable changes to libreshop/cms are documented here.
## Unreleased
- Extracted from `mp/cms/` (2026-04-29). The component history before
the extraction lives in the `muellerprints` repository.

30
Dockerfile Normal file
View File

@@ -0,0 +1,30 @@
FROM node:22.14.0-alpine
RUN apk update && \
apk add --no-cache build-base gcc autoconf automake \
zlib-dev libpng-dev nasm bash vips-dev
ARG NODE_ENV
ENV NODE_ENV ${NODE_ENV}
ENV PORT 5555
WORKDIR /app/
COPY ./package.json ./
ENV PATH=/node_modules/.bin:$PATH
RUN npm i --no-audit --no-fund --progress=false --no-warnings --log-level=error
RUN npm i --ignore-scripts=false --foreground-scripts --verbose sharp
COPY ./ ./
RUN chmod +x ./docker-entrypoint.sh
RUN npm run build
ENTRYPOINT ["/app/docker-entrypoint.sh"]
#USER node
EXPOSE 5555

25
README.md Normal file
View File

@@ -0,0 +1,25 @@
# libreshop/cms
Strapi-based content backend (catalogue, orders, customers).
Part of the [libreshop](https://git.librete.ch/libreshop) toolkit. Image
published at `git.librete.ch/libreshop/cms` on every push to `main`
and on `v*` tags.
## Source
This repo was extracted from `mp/cms/` on 2026-04-29; mp was the
first concrete adapter consuming the toolkit. mp's `compose.yml` now
pulls `git.librete.ch/libreshop/cms:<pin>` instead of building locally.
## Build locally
```
docker build -t libreshop/cms:dev .
```
## Adapter contract
See `docker-entrypoint.sh` and `Dockerfile` for the runtime surface.
Adapters configure the component via env vars and bind-mounted volumes;
do not patch the running container or rely on internal paths.

22
config/admin.ts Normal file
View File

@@ -0,0 +1,22 @@
export default ({ env }) => ({
auth: {
secret: env("ADMIN_JWT_SECRET")
},
apiToken: {
salt: env("API_TOKEN_SALT")
},
transfer: {
token: {
salt: env("TRANSFER_TOKEN_SALT")
}
},
autoOpen: false,
// watchIgnoreFiles: [
// "**/controllers/**",
// "**/database/**",
// ],
flags: {
nps: false,
promoteEE: false
}
});

7
config/api.ts Normal file
View File

@@ -0,0 +1,7 @@
export default {
rest: {
defaultLimit: 100,
maxLimit: 1_000,
withCount: true
}
};

19
config/constants.ts Normal file
View File

@@ -0,0 +1,19 @@
import { Environment } from "@paypal/paypal-server-sdk";
export const pdfApiUrl = process.env.PDF_API_ADDRESS!;
export const mailApiUrl = process.env.MAIL_API_ADDRESS!;
export const baseUrl = process.env.BASE_URL!;
export const paypalClientId = process.env.PAYPAL_CLIENT_ID!;
export const paypalClientSecret = process.env.PAYPAL_CLIENT_SECRET!;
export const paypalEnvironment = process.env.PAYPAL_ENVIRONMENT! === "production" ? Environment.Production : Environment.Sandbox;
export const adminEmail = process.env.ADMIN_EMAIL_ADDRESS!;
// TODO: Should be retrieved from DepotApi
export const vatIncludedDecimal = 1.19;
// TODO: Should be retrieved from DepotApi
export const vatDecimal = 0.19;
export const vatDecimalExcluded = 1 - vatDecimal;
export const maxProductsSitemap = 500;

79
config/database.ts Normal file
View File

@@ -0,0 +1,79 @@
import path from "path";
export default ({ env }) => {
const client = env("DATABASE_CLIENT", "sqlite");
const connections = {
mysql: {
connection: {
connectionString: env("DATABASE_URL"),
host: env("DATABASE_HOST", "localhost"),
port: env.int("DATABASE_PORT", 3306),
database: env("DATABASE_NAME", "strapi"),
user: env("DATABASE_USERNAME", "strapi"),
password: env("DATABASE_PASSWORD", "strapi"),
ssl: env.bool("DATABASE_SSL", false) && {
key: env("DATABASE_SSL_KEY", undefined),
cert: env("DATABASE_SSL_CERT", undefined),
ca: env("DATABASE_SSL_CA", undefined),
capath: env("DATABASE_SSL_CAPATH", undefined),
cipher: env("DATABASE_SSL_CIPHER", undefined),
rejectUnauthorized: env.bool("DATABASE_SSL_REJECT_UNAUTHORIZED", true)
}
},
pool: { min: env.int("DATABASE_POOL_MIN", 2), max: env.int("DATABASE_POOL_MAX", 10) }
},
mysql2: {
connection: {
host: env("DATABASE_HOST", "localhost"),
port: env.int("DATABASE_PORT", 3306),
database: env("DATABASE_NAME", "strapi"),
user: env("DATABASE_USERNAME", "strapi"),
password: env("DATABASE_PASSWORD", "strapi"),
ssl: env.bool("DATABASE_SSL", false) && {
key: env("DATABASE_SSL_KEY", undefined),
cert: env("DATABASE_SSL_CERT", undefined),
ca: env("DATABASE_SSL_CA", undefined),
capath: env("DATABASE_SSL_CAPATH", undefined),
cipher: env("DATABASE_SSL_CIPHER", undefined),
rejectUnauthorized: env.bool("DATABASE_SSL_REJECT_UNAUTHORIZED", true)
}
},
pool: { min: env.int("DATABASE_POOL_MIN", 2), max: env.int("DATABASE_POOL_MAX", 10) }
},
postgres: {
connection: {
connectionString: env("DATABASE_URL"),
host: env("DATABASE_HOST", "localhost"),
port: env.int("DATABASE_PORT", 5432),
database: env("DATABASE_NAME", "strapi"),
user: env("DATABASE_USERNAME", "strapi"),
password: env("DATABASE_PASSWORD", "strapi"),
ssl: env.bool("DATABASE_SSL", false) && {
key: env("DATABASE_SSL_KEY", undefined),
cert: env("DATABASE_SSL_CERT", undefined),
ca: env("DATABASE_SSL_CA", undefined),
capath: env("DATABASE_SSL_CAPATH", undefined),
cipher: env("DATABASE_SSL_CIPHER", undefined),
rejectUnauthorized: env.bool("DATABASE_SSL_REJECT_UNAUTHORIZED", true)
},
schema: env("DATABASE_SCHEMA", "public")
},
pool: { min: env.int("DATABASE_POOL_MIN", 2), max: env.int("DATABASE_POOL_MAX", 10) }
},
sqlite: {
connection: {
filename: path.join(__dirname, "..", "..", env("DATABASE_FILENAME", ".tmp/data.db"))
},
useNullAsDefault: true
}
};
return {
connection: {
client,
...connections[client],
acquireConnectionTimeout: env.int("DATABASE_CONNECTION_TIMEOUT", 60000)
}
};
};

16
config/logger.ts Normal file
View File

@@ -0,0 +1,16 @@
import { winston } from "@strapi/logger";
export default {
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.printf(({ timestamp, level, message, ...rest }) => {
let restString = JSON.stringify(rest, undefined, 2);
restString = restString === "{}" ? "" : restString;
return `${timestamp}Z ${level}: ${message} ${restString}`;
})
)
})
]
};

12
config/middlewares.ts Normal file
View File

@@ -0,0 +1,12 @@
export default [
"strapi::errors",
"strapi::security",
"strapi::cors",
// 'strapi::poweredBy',
"strapi::logger",
"strapi::query",
"strapi::body",
"strapi::session",
"strapi::favicon",
"strapi::public"
];

749
config/plugins.ts Normal file
View File

@@ -0,0 +1,749 @@
export default ({ env }) => ({
upload: {
config: {
sizeLimit: 50 * 1024 * 1024
}
},
documentation: {
enabled: false,
config: {
openapi: "3.0.1",
info: {
version: "1.0.0",
title: "MUELLERPTINTS. Paperwork",
description: "API Documentation for MUELLERPRINTS. Paperwork",
termsOfService: false,
contact: {
name: "Michael W. Czechowski",
email: "mail@dailysh.it",
url: "https://dailysh.it"
},
license: "Copyright (C) 2024 Michael W. Czechowski",
externalDocs: false
},
"x-strapi-config": {
plugins: [],
path: "/documentation",
mutateDocumentation: (draft: any) => {
// Order endpoints - maintain existing modifications
// draft.paths["/orders/{uuid}/cart"].get.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/cart"].put.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/generate-delivery-note"].put.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/generate-invoice"].put.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/add-product/{productId}"].put.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/remove-product/{productId}"].put.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/checkout"].post.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/send-invoice"].put.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/send-delivery-note"].put.parameters[0].schema.type = "string";
//
// // Order request body modifications - maintain existing
// delete draft.paths["/orders/{uuid}/cart"].put.requestBody;
// delete draft.paths["/orders/{uuid}/add-product/{productId}"].put.requestBody;
// delete draft.paths["/orders/{uuid}/remove-product/{productId}"].put.requestBody;
// delete draft.paths["/orders/{uuid}/checkout"].post.requestBody;
//
// // Product publish endpoint - existing documentation
// draft.paths["/products/publish"].post.description = "Publish or unpublish products by filter";
// draft.paths["/products/publish"].post.requestBody = {
// required: true,
// content: {
// "application/json": {
// schema: {
// type: "object",
// required: ["filters"],
// properties: {
// filters: {
// type: "object",
// description: "Filters to select products (supports pattern, cover, ruling, pages)"
// },
// publish: {
// type: "boolean",
// description: "Whether to publish (true) or unpublish (false)",
// default: true
// },
// dryRun: {
// type: "boolean",
// description: "If true, no changes will be made but count will be returned",
// default: false
// }
// }
// }
// }
// }
// };
//
// draft.paths["/products/publish"].post.responses = {
// "200": {
// description: "Successfully published/unpublished products",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// published: {
// type: "integer",
// description: "Number of products published/unpublished"
// },
// dryRun: {
// type: "boolean",
// description: "Whether this was a dry run"
// }
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// error: {
// type: "string",
// description: "Error message"
// }
// }
// }
// }
// }
// }
// };
// // Product allVariants endpoint
// if (draft.paths["/products/{id}/variants/all"]) {
// draft.paths["/products/{id}/variants/all"].get.description = "Get all variants for a product";
// draft.paths["/products/{id}/variants/all"].get.responses = {
// "200": {
// description: "Returns all variants of the product",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/Product"
// }
// }
// }
// }
// },
// "404": {
// description: "Product not found",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// error: {
// type: "string",
// description: "Error message"
// }
// }
// }
// }
// }
// }
// };
// }
//
// // Product variantsByPattern endpoint
// if (draft.paths["/products/{id}/variants/pattern"]) {
// draft.paths["/products/{id}/variants/pattern"].get.description = "Get product variants grouped by pattern";
// draft.paths["/products/{id}/variants/pattern"].get.responses = {
// "200": {
// description: "Returns product variants grouped by pattern",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// allProductPattern: {
// type: "array",
// items: {
// $ref: "#/components/schemas/ProductPattern"
// }
// },
// productVariants: {
// type: "array",
// items: {
// $ref: "#/components/schemas/Product"
// }
// },
// patterns: {
// type: "array",
// items: {
// type: "object",
// properties: {
// id: {
// type: "string"
// },
// name: {
// type: "string"
// },
// description: {
// type: "string"
// },
// productVariant: {
// oneOf: [
// {
// $ref: "#/components/schemas/Product"
// },
// {
// type: "null"
// }
// ]
// }
// }
// }
// }
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// error: {
// type: "string",
// description: "Error message"
// }
// }
// }
// }
// }
// }
// };
// }
//
// // Product variants endpoint
// if (draft.paths["/products/{id}/variants"]) {
// draft.paths["/products/{id}/variants"].get.description = "Get product variants grouped by pages, cover, and ruling";
// draft.paths["/products/{id}/variants"].get.responses = {
// "200": {
// description: "Returns product variants grouped by pages, cover, and ruling",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// pages: {
// type: "array",
// items: {
// type: "object",
// properties: {
// id: {
// type: "string"
// },
// name: {
// type: "string"
// },
// productVariant: {
// oneOf: [
// {
// $ref: "#/components/schemas/Product"
// },
// {
// type: "null"
// }
// ]
// }
// }
// }
// },
// cover: {
// type: "array",
// items: {
// type: "object",
// properties: {
// id: {
// type: "string"
// },
// name: {
// type: "string"
// },
// binding: {
// type: "string"
// },
// price: {
// type: "number"
// },
// productVariant: {
// oneOf: [
// {
// $ref: "#/components/schemas/Product"
// },
// {
// type: "null"
// }
// ]
// }
// }
// }
// },
// ruling: {
// type: "array",
// items: {
// type: "object",
// properties: {
// id: {
// type: "string"
// },
// name: {
// type: "string"
// },
// productVariant: {
// oneOf: [
// {
// $ref: "#/components/schemas/Product"
// },
// {
// type: "null"
// }
// ]
// }
// }
// }
// }
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// error: {
// type: "string",
// description: "Error message"
// }
// }
// }
// }
// }
// }
// };
// }
//
// // Product Category endpoints
// if (draft.paths["/product-category"]) {
// draft.paths["/product-category"].get.description = "Get all product categories";
// draft.paths["/product-category"].get.responses = {
// "200": {
// description: "Returns all product categories",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/ProductCategory"
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request"
// }
// };
// }
//
// if (draft.paths["/product-category/corrupt"]) {
// draft.paths["/product-category/corrupt"].get.description = "Get all corrupted product categories";
// draft.paths["/product-category/corrupt"].get.responses = {
// "200": {
// description: "Returns all corrupted product categories",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/ProductCategory"
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request"
// }
// };
// }
//
// if (draft.paths["/product-category/{id}"]) {
// draft.paths["/product-category/{id}"].get.description = "Get a single product category";
// draft.paths["/product-category/{id}"].get.responses = {
// "200": {
// description: "Returns a single product category",
// content: {
// "application/json": {
// schema: {
// $ref: "#/components/schemas/ProductCategory"
// }
// }
// }
// },
// "400": {
// description: "Bad request"
// }
// };
//
// draft.paths["/product-category/{id}"].put.description = "Update a product category";
// draft.paths["/product-category/{id}"].put.requestBody = {
// required: true,
// content: {
// "application/json": {
// schema: {
// type: "object",
// description: "The data to update the category with"
// }
// }
// }
// };
// draft.paths["/product-category/{id}"].put.responses = {
// "200": {
// description: "Returns the updated product category",
// content: {
// "application/json": {
// schema: {
// $ref: "#/components/schemas/ProductCategory"
// }
// }
// }
// },
// "400": {
// description: "Bad request"
// }
// };
// }
//
// // Update endpoint to match the actual route from product-category.ts
// if (draft.paths["/product-category/bulk"]) {
// draft.paths["/product-category/bulk"].put.description = "Update multiple product categories";
// draft.paths["/product-category/bulk"].put.requestBody = {
// required: true,
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// categories: {
// type: "array",
// description: "Array of categories to update"
// }
// }
// }
// }
// }
// };
// draft.paths["/product-category/bulk"].put.responses = {
// "200": {
// description: "Returns the updated product categories",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/ProductCategory"
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request"
// }
// };
// }
// // Remove the incorrect "/product-category/update" documentation since it doesn't exist in routes
//
// if (draft.paths["/product-category/fix"]) {
// draft.paths["/product-category/fix"].put.description = "Fix categories with missing cover or pattern";
// draft.paths["/product-category/fix"].put.responses = {
// "200": {
// description: "Returns information about fixed and failed categories",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// fixed: {
// type: "array",
// items: {
// $ref: "#/components/schemas/ProductCategory"
// }
// },
// failed: {
// type: "array",
// items: {
// $ref: "#/components/schemas/ProductCategory"
// }
// }
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request"
// }
// };
// }
//
// if (draft.paths["/product-category/fix-images"]) {
// draft.paths["/product-category/fix-images"].put.description = "Fix product images in categories";
// draft.paths["/product-category/fix-images"].put.responses = {
// "200": {
// description: "Returns information about fixed and failed product images",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// fixed: {
// type: "array",
// items: {
// type: "object",
// properties: {
// categoryId: {
// type: "string"
// },
// productCount: {
// type: "integer"
// },
// products: {
// type: "array",
// items: {
// type: "string"
// }
// }
// }
// }
// },
// failed: {
// type: "array",
// items: {
// type: "object",
// properties: {
// categoryId: {
// type: "string"
// },
// reason: {
// type: "string"
// },
// error: {
// type: "string"
// }
// }
// }
// }
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request"
// }
// };
// }
//
// // Content controller endpoints
// if (draft.paths["/content"]) {
// draft.paths["/content"].get.description = "Get all content entries";
// draft.paths["/content"].get.responses = {
// "200": {
// description: "Returns all content entries",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/Content"
// }
// }
// }
// }
// }
// };
// }
//
// // Customer controller endpoints
// if (draft.paths["/customers"]) {
// draft.paths["/customers"].get.description = "Get all customers";
// draft.paths["/customers"].get.responses = {
// "200": {
// description: "Returns all customers",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/Customer"
// }
// }
// }
// }
// }
// };
// }
//
// // Legal controller endpoints
// if (draft.paths["/legal"]) {
// draft.paths["/legal"].get.description = "Get all legal documents";
// draft.paths["/legal"].get.responses = {
// "200": {
// description: "Returns all legal documents",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/Legal"
// }
// }
// }
// }
// }
// };
// }
//
// // Delivery controller endpoints
// if (draft.paths["/deliveries"]) {
// draft.paths["/deliveries"].get.description = "Get all deliveries";
// draft.paths["/deliveries"].get.responses = {
// "200": {
// description: "Returns all deliveries",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/Delivery"
// }
// }
// }
// }
// }
// };
// }
//
// // Payment controller endpoints
// if (draft.paths["/payments"]) {
// draft.paths["/payments"].get.description = "Get all payments";
// draft.paths["/payments"].get.responses = {
// "200": {
// description: "Returns all payments",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/Payment"
// }
// }
// }
// }
// }
// };
// }
//
// if (draft.paths["/orders/{uuid}"]) {
// draft.paths["/orders/{uuid}"].get.description = "Get order by UUID";
// draft.paths["/orders/{uuid}"].get.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}"].get.responses = {
// "200": {
// description: "Returns order details",
// content: {
// "application/json": {
// schema: {
// $ref: "#/components/schemas/Order"
// }
// }
// }
// },
// "404": {
// description: "Order not found"
// }
// };
// }
//
// if (draft.paths["/orders/webhook"]) {
// draft.paths["/orders/webhook"].post.description = "Handle PayPal webhook events";
// draft.paths["/orders/webhook"].post.requestBody = {
// required: true,
// content: {
// "application/json": {
// schema: {
// type: "object",
// description: "PayPal webhook event payload"
// }
// }
// }
// };
// draft.paths["/orders/webhook"].post.responses = {
// "200": {
// description: "Webhook processed successfully"
// },
// "500": {
// description: "Internal server error"
// }
// };
// }
}
},
servers: [
{
url: "/api",
description: "API server"
}
],
security: [
{
bearerAuth: []
}
]
}
},
"strapi-prometheus": {
enabled: false,
config: {
// add prefix to all the prometheus metrics names.
prefix: "cms",
// use full url instead of matched url
// true => path label: `/api/models/1`
// false => path label: `/api/models/:id`
fullURL: false,
// include url query in the url label
// true => path label: `/api/models?limit=1`
// false => path label: `/api/models`
includeQuery: false,
// metrics that will be enabled, by default they are all enabled.
enabledMetrics: {
koa: true, // koa metrics
process: true, // metrics regarding the running process
http: true, // http metrics like response time and size
apollo: false // metrics regarding graphql
},
// interval at which rate metrics are collected in ms
interval: 30_000,
// set custom/default labels to all the prometheus metrics
customLabels: {
name: "strapi-prometheus"
}
}
}
});

13
config/server.ts Normal file
View File

@@ -0,0 +1,13 @@
export default ({ env }) => ({
host: env("HOST", "0.0.0.0"),
port: env.int("PORT", 5555),
app: {
keys: env.array("APP_KEYS")
},
webhooks: {
populateRelations: env.bool("WEBHOOKS_POPULATE_RELATIONS", false)
},
logger: {
level: "info"
}
});

View File

7
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
if [ "$NODE_ENV" = "production" ]; then
npm run start
else
npm run develop
fi

BIN
favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

40
package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "cms",
"version": "1.0.0",
"scripts": {
"develop": "DEBUG=app:* npm run strapi develop -- --debug --bundler vite",
"start": "strapi start",
"build": "strapi build --debug",
"strapi": "strapi",
"lint:fix": "prettier --write \"{src,types,config}/**/*.{js,jsx,ts,tsx}\"",
"generate:types": "strapi ts:generate-types",
"import": "npm run strapi import -- -f database/export.tar.gz --force",
"export": "npm run strapi export -- --no-encrypt --file database/export_$(date +'%Y%m%d%H%M%S')",
"transfer-pull": "npm run strapi transfer -- --from $TRANSFER_URL --from-token $TRANSFER_FROM_TOKEN",
"transfer-push": "npm run strapi transfer -- --to $TRANSFER_URL --to-token $TRANSFER_TO_TOKEN --force"
},
"dependencies": {
"@paypal/paypal-server-sdk": "^0.6.1",
"@strapi/plugin-documentation": "4.25.21",
"@strapi/plugin-i18n": "4.25.21",
"@strapi/plugin-users-permissions": "4.25.21",
"@strapi/strapi": "4.25.21",
"@strapi/utils": "4.25.21",
"lodash": "^4.17.21",
"pg": "^8.11.3",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-router-dom": "^5.2.0",
"sharp": "^0.33.5",
"strapi-plugin-populate-deep": "^3.0.1",
"strapi-prometheus": "^1.9.2",
"styled-components": "^5.2.1",
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/lodash": "^4.17.15",
"@types/node-fetch": "^2.6.11",
"@types/uuid": "^10.0.0",
"prettier": "^3.0.0"
}
}

3
public/robots.txt Executable file
View File

@@ -0,0 +1,3 @@
# To prevent search engines from seeing the site altogether, uncomment the next two lines:
# User-Agent: *
# Disallow: /

0
public/uploads/.gitkeep Executable file
View File

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

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

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

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

0
src/api/.gitkeep Normal file
View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,269 @@
import path from "path";
import fs from "fs";
import { factories } from "@strapi/strapi";
import { sanitize } from "@strapi/utils";
import { ID } from "@strapi/database/dist/types";
import { vatDecimal, vatIncludedDecimal, baseUrl, adminEmail } from "../../../../config/constants";
import { calculateTotalProductPrice, productDefaultParams } from "../../product/services/product";
import { CartProduct, Order, PdfBody } from "../../../../types";
import { pdfApi } from "../../../services/PdfApi";
import { invoiceEmailTemplate, deliveryNoteEmailTemplate } from "../../../templates/emailTemplates";
import _, { template } from "lodash";
import { PdfMessageBody, MessageBody, mailApi } from "../../../services/MailApi";
/**
* This service is responsible for handling the order logic.
*/
export default factories.createCoreService("api::order.order", ({ strapi }) => ({
findOne: async (id: ID) => {
return await strapi.entityService.findOne("api::order.order", id, orderDefaultParams);
},
findOneByUuid: async (uuid: string) => {
const params = {
filters: {
uuid: {
$eq: uuid
}
},
...orderDefaultParams
};
const orderUnsafe = await strapi.db.query("api::order.order").findOne({
where: { uuid },
...orderDefaultParams
});
orderUnsafe.cart = orderUnsafe?.cart.map((item: CartProduct) => ({
...item,
product: {
...item.product,
totalProductPrice: calculateTotalProductPrice(item.product)
}
}));
return (await sanitize.contentAPI.output(orderUnsafe, strapi.getModel("api::order.order"))) as Order;
},
uploadPdf: async (orderUnsafe: Order, field: "invoice" | "deliveryNote", name = "Upload") => {
strapi.log.verbose(`app:v:order-service: Generating ${field} pdf`);
const pdfBody = field === "invoice" ? invoicePdfBody(orderUnsafe) : deliveryNotePdfBody(orderUnsafe);
strapi.log.debug(`app:d:order-service: - PDF body`, pdfBody);
const blob = field === "invoice" ? await pdfApi.generateInvoice(pdfBody) : await pdfApi.generateDeliveryNote(pdfBody);
strapi.log.verbose(`app:v:order-service: ✔ Generating ${field} pdf`);
// Save blob to a temp folder
const tempFolder = path.join(__dirname, "temp");
if (!fs.existsSync(tempFolder)) {
fs.mkdirSync(tempFolder);
}
const deliveryNotePath = path.join(tempFolder, `${name}.pdf`);
fs.writeFileSync(deliveryNotePath, Buffer.from(blob));
const uploadData = {
data: {
ref: "api::order.order",
refId: orderUnsafe.id,
field
},
files: {
path: deliveryNotePath,
name: `${name}_${pdfBody.date.replace(/[^a-z0-9]/gi, "_").toLowerCase()}`,
type: "application/pdf",
size: fs.statSync(deliveryNotePath).size
}
};
strapi.log.verbose(`app:v:order-service: - Uploading ${field} pdf`);
await strapi.plugins.upload.services.upload.upload(uploadData);
strapi.log.verbose(`app:v:order-service: ✔ Uploading ${field} pdf`);
return await strapi.service("api::order.order").findOne(orderUnsafe.id);
},
sendInvoice: async (uuid: string, blob?: any) => {
strapi.log.info(`app:i:order-service: - Sending invoice for order ${uuid}`);
const oderUnsafe = (await strapi.service("api::order.order").findOneByUuid(uuid)) as Partial<Order>;
try {
const emailConfig: MessageBody = {
to_email: oderUnsafe.email,
subject: template(invoiceEmailTemplate.subject)(oderUnsafe),
message: template(invoiceEmailTemplate.text)({ baseUrl, ...oderUnsafe }),
html: template(invoiceEmailTemplate.html)({ baseUrl, ...oderUnsafe })
};
strapi.log.debug(`app:d:order-service: - Email config ${emailConfig.html}`);
// Send email to customer
await mailApi.sendTextMessage(emailConfig);
// Send email to administator
await mailApi.sendTextMessage({
...emailConfig,
to_email: adminEmail
});
strapi.log.info(`app:i:order-service: ✔ Sending invoice for order ${uuid}`);
return await strapi.db.query("api::order.order").update({
where: {
id: oderUnsafe.id
},
data: {
invoiceSent: true
}
});
} catch (error) {
strapi.log.error(`app:e:order-service: χ Sending invoice for order ${uuid}`);
strapi.log.debug(`app:d:order-service: Error: ${JSON.stringify({ error })}`);
return new Error("Could not send invoice");
}
},
sendDeliveryNote: async (uuid: string, blob?: ArrayBuffer) => {
strapi.log.info(`app:i:order-service: - Sending delivery note for order ${uuid}`);
const oderUnsafe = (await strapi.service("api::order.order").findOneByUuid(uuid)) as Partial<Order>;
try {
const emailConfig: MessageBody = {
to_email: oderUnsafe.email,
subject: template(deliveryNoteEmailTemplate.subject)(oderUnsafe),
message: template(deliveryNoteEmailTemplate.text)(oderUnsafe),
html: template(deliveryNoteEmailTemplate.html)({ ...oderUnsafe, baseUrl })
};
await mailApi.sendTextMessage(emailConfig);
strapi.log.info(`app:i:order-service: ✔ Sending delivery note for order ${uuid}`);
return await strapi.db.query("api::order.order").update({
where: {
id: oderUnsafe.id
},
data: {
deliveryNoteSent: true
}
});
} catch (error) {
strapi.log.error(`app:e:order-service: χ Sending delivery note for order ${uuid}`);
strapi.log.debug(`app:d:order-service: Error: ${JSON.stringify({ error })}`);
return new Error("Could not send delivery note");
}
},
update: async (id: ID, params: Record<string, any>): Promise<Order> => {
const dataUnsafe = (await strapi.entityService.update("api::order.order", id, params)) as Order;
strapi.log.verbose(`app:v:order-service: Updated order ${id} with initial params`);
let data: { subtotal: number; total: number; VAT: number };
if (dataUnsafe?.cart) {
const productsTotal = dataUnsafe.cart.reduce((v, p) => v + (calculateTotalProductPrice(p.product) ?? 0) * p.count, 0);
const productsVAT = dataUnsafe.cart.reduce((v, p) => {
const totalProductPrice = calculateTotalProductPrice(p.product) ?? 0;
const amount = totalProductPrice / vatIncludedDecimal;
const tax = totalProductPrice - amount;
return v + tax * p.count;
}, 0);
const deliveryPrice = dataUnsafe.delivery?.price ?? 0;
const paymentPrice = dataUnsafe.payment?.price ?? 0;
strapi.log.debug(`app:d:order-service: Calculating totals`, { productsTotal, deliveryPrice, paymentPrice });
const subtotal = productsTotal;
const total = Math.round((subtotal + deliveryPrice + paymentPrice) * 100) / 100;
const VAT = Math.round((subtotal / vatIncludedDecimal) * vatDecimal * 100) / 100;
strapi.log.verbose(`app:v:order-service: Calculated totals`, { productsTotal, total, VAT, subtotal });
data = {
subtotal,
total,
VAT
};
}
const orderUnsafe = await strapi.entityService.update("api::order.order", id, { data, ...orderDefaultParams });
strapi.log.verbose(`app:v:order-service: Updated order ${id} with calculated totals`);
orderUnsafe.cart = orderUnsafe?.cart.map((item: CartProduct) => ({
...item,
product: {
...item.product,
totalProductPrice: calculateTotalProductPrice(item.product)
}
}));
return (await sanitize.contentAPI.output(orderUnsafe, strapi.getModel("api::order.order"))) as Order;
}
}));
export const orderDefaultParams = {
populate: {
invoice: true,
deliveryNote: true,
delivery: true,
payment: true,
customer: {
populate: {
addressStructured: true
}
},
invoiceAddressStructured: true,
deliveryAddressStructured: true,
cart: {
populate: {
product: productDefaultParams
}
}
}
};
const invoicePdfBody = (order: Order): PdfBody => {
const pdfBody = defaultPdfBody(order);
return {
...pdfBody,
subject: "RECHNUNG",
to: {
name: `${order.customer.addressStructured?.givenName} ${order.customer.addressStructured?.familyName}` || "",
address: [
order.customer.addressStructured?.streetAddress || "",
`${order.customer.addressStructured?.postalCode} ${order.customer.addressStructured?.addressLevel2}` || ""
]
},
nr: {
...pdfBody.nr,
invoice: order.invoiceNumber
}
} as PdfBody;
};
const deliveryNotePdfBody = (order: Order): PdfBody => {
const pdfBody = defaultPdfBody(order);
return {
...pdfBody,
subject: "LIEFERSCHEIN",
to: {
name: "",
address: order.deliveryAddress.split("\n")
},
nr: {
...pdfBody.nr,
shipping: order.deliveryNoteNumber
}
} as PdfBody;
};
const defaultPdfBody = (order: Order): Partial<PdfBody> => ({
date: new Date(order.acceptedTermsAndConditionsAt).toLocaleDateString("de-DE"),
nr: {
customer: `${order.customer.id}`,
order: `${order.id}`
},
service:
order.cart?.map((cartProduct) => ({
description: cartProduct.product.name,
price: {
per_unit: calculateTotalProductPrice(cartProduct.product) || 0,
total: (calculateTotalProductPrice(cartProduct.product) || 0) * cartProduct.count
},
count: cartProduct.count,
nr: `P${cartProduct.product.id}`
})) || [],
currency: "\\euro",
body: "Vielen Dank für Ihren Einkauf und Ihr Vertrauen.",
total: order.total,
subtotal: order.subtotal,
VAT: {
amount: order.VAT,
rate: vatDecimal * 100
},
shipping: order.delivery?.price ?? 0
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More