feat(product): price standalone products from the cover alone
build / build (push) Successful in 8m52s
build / build (push) Successful in 8m52s
Recovered work in progress from 2026-05. A product-cover can be marked standalone, and a standalone product (Journaling Booklet) prices as the cover price without the pattern/pages/ruling lookups, which do not exist for it. The remaining component lookups are now guarded so a missing relation yields null instead of calling findOne with undefined.
This commit is contained in:
@@ -54,6 +54,10 @@
|
|||||||
"price": {
|
"price": {
|
||||||
"type": "decimal"
|
"type": "decimal"
|
||||||
},
|
},
|
||||||
|
"standalone": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"products": {
|
"products": {
|
||||||
"type": "relation",
|
"type": "relation",
|
||||||
"relation": "oneToMany",
|
"relation": "oneToMany",
|
||||||
|
|||||||
@@ -11,10 +11,26 @@ export default {
|
|||||||
strapi.log.verbose("app:v:product-lifecycle: Before create", {
|
strapi.log.verbose("app:v:product-lifecycle: Before create", {
|
||||||
params
|
params
|
||||||
});
|
});
|
||||||
const cover = await strapi.entityService.findOne("api::product-cover.product-cover", params.data?.cover, { fields: ["price"] });
|
const cover = params.data?.cover
|
||||||
const pattern = await strapi.entityService.findOne("api::product-pattern.product-pattern", params.data?.pattern);
|
? await strapi.entityService.findOne("api::product-cover.product-cover", params.data.cover, { fields: ["price", "standalone"] })
|
||||||
const pages = await strapi.entityService.findOne("api::product-page.product-page", params.data?.pages, { fields: ["price"] });
|
: null;
|
||||||
const ruling = await strapi.entityService.findOne("api::product-ruling.product-ruling", params.data?.ruling, { fields: ["price"] });
|
|
||||||
|
// Standalone products (e.g. Journaling Booklet) have no pattern/pages/ruling.
|
||||||
|
// Price is the cover price alone — skip combinatorial component lookups.
|
||||||
|
if (params.data?.standalone || (cover as any)?.standalone) {
|
||||||
|
params.data.totalPrice = cover?.price ?? 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern = params.data?.pattern
|
||||||
|
? await strapi.entityService.findOne("api::product-pattern.product-pattern", params.data.pattern)
|
||||||
|
: null;
|
||||||
|
const pages = params.data?.pages
|
||||||
|
? await strapi.entityService.findOne("api::product-page.product-page", params.data.pages, { fields: ["price"] })
|
||||||
|
: null;
|
||||||
|
const ruling = params.data?.ruling
|
||||||
|
? await strapi.entityService.findOne("api::product-ruling.product-ruling", params.data.ruling, { fields: ["price"] })
|
||||||
|
: null;
|
||||||
const product = {
|
const product = {
|
||||||
...params.data,
|
...params.data,
|
||||||
cover,
|
cover,
|
||||||
|
|||||||
@@ -108,6 +108,12 @@ export default factories.createCoreController("api::product.product", ({ strapi
|
|||||||
strapi.log.verbose(`Fetching variants for product ${id}`);
|
strapi.log.verbose(`Fetching variants for product ${id}`);
|
||||||
|
|
||||||
const product = await getProductDetails(id as ID);
|
const product = await getProductDetails(id as ID);
|
||||||
|
|
||||||
|
// Standalone products (e.g. Journaling Booklet) have no pattern — no variants.
|
||||||
|
if (!product.pattern?.id) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const variants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>("api::product.product", {
|
const variants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>("api::product.product", {
|
||||||
...productVariantParams,
|
...productVariantParams,
|
||||||
filters: {
|
filters: {
|
||||||
@@ -146,6 +152,11 @@ export default factories.createCoreController("api::product.product", ({ strapi
|
|||||||
const product = await getProductDetails(id as ID);
|
const product = await getProductDetails(id as ID);
|
||||||
const { pattern, cover, ruling, pages } = product;
|
const { pattern, cover, ruling, pages } = product;
|
||||||
|
|
||||||
|
// Standalone products (e.g. Journaling Booklet) have no pattern → no variants.
|
||||||
|
if (!pattern?.id) {
|
||||||
|
return { allProductPattern: [], productVariants: [], patterns: [] };
|
||||||
|
}
|
||||||
|
|
||||||
const productVariants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>("api::product.product", {
|
const productVariants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>("api::product.product", {
|
||||||
...productVariantParams,
|
...productVariantParams,
|
||||||
filters: {
|
filters: {
|
||||||
@@ -203,6 +214,11 @@ export default factories.createCoreController("api::product.product", ({ strapi
|
|||||||
|
|
||||||
const product = await getProductDetails(id as ID);
|
const product = await getProductDetails(id as ID);
|
||||||
|
|
||||||
|
// Standalone products (e.g. Journaling Booklet) have no pattern → no variant groups.
|
||||||
|
if (!product.pattern?.id) {
|
||||||
|
return { pages: [], cover: [], ruling: [] };
|
||||||
|
}
|
||||||
|
|
||||||
const allProductPatternVariants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>(
|
const allProductPatternVariants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>(
|
||||||
"api::product.product",
|
"api::product.product",
|
||||||
{
|
{
|
||||||
@@ -335,7 +351,8 @@ export default factories.createCoreController("api::product.product", ({ strapi
|
|||||||
const productGroups = new Map();
|
const productGroups = new Map();
|
||||||
|
|
||||||
allProductsSanitized.forEach((product) => {
|
allProductsSanitized.forEach((product) => {
|
||||||
const key = `${product.cover.id}-${product.pattern.id}`;
|
// Standalone products (e.g. Journaling Booklet) have no pattern — never grouped.
|
||||||
|
const key = product.pattern?.id ? `${product.cover?.id}-${product.pattern.id}` : `standalone-${product.id}`;
|
||||||
if (!productGroups.has(key) || calculateTotalProductPrice(product) < calculateTotalProductPrice(productGroups.get(key))) {
|
if (!productGroups.has(key) || calculateTotalProductPrice(product) < calculateTotalProductPrice(productGroups.get(key))) {
|
||||||
productGroups.set(key, product);
|
productGroups.set(key, product);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,8 +110,12 @@ async function generateNewProducts(strapi: Strapi, componentType: string, compon
|
|||||||
|
|
||||||
strapi.log.debug(`app:d:lifecycle-factory ${JSON.stringify({ covers, patterns, pages, rulings })}`);
|
strapi.log.debug(`app:d:lifecycle-factory ${JSON.stringify({ covers, patterns, pages, rulings })}`);
|
||||||
|
|
||||||
|
// Standalone covers (e.g. the Journaling Booklet) are not combinatorial —
|
||||||
|
// their product is created manually and must never be fanned out.
|
||||||
|
const sellableCovers = covers.filter((cover) => cover && !(cover as ProductCover).standalone);
|
||||||
|
|
||||||
// Generate new combinations
|
// Generate new combinations
|
||||||
for (const cover of covers) {
|
for (const cover of sellableCovers) {
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns) {
|
||||||
for (const page of pages) {
|
for (const page of pages) {
|
||||||
for (const ruling of rulings) {
|
for (const ruling of rulings) {
|
||||||
@@ -181,6 +185,12 @@ async function updateRelatedProducts(strapi: Strapi, componentType: string, comp
|
|||||||
|
|
||||||
for (const product of products) {
|
for (const product of products) {
|
||||||
try {
|
try {
|
||||||
|
// Standalone products (cover-only, e.g. Journaling Booklet) are not
|
||||||
|
// factory-managed — never rename/reslug/delete them.
|
||||||
|
if (!product.pattern?.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Get the current components for this product
|
// Get the current components for this product
|
||||||
const cover = product.cover?.id;
|
const cover = product.cover?.id;
|
||||||
const pattern = product.pattern?.id;
|
const pattern = product.pattern?.id;
|
||||||
|
|||||||
Vendored
+1
@@ -453,6 +453,7 @@ export interface ApiProductCoverProductCover extends Schema.CollectionType {
|
|||||||
number
|
number
|
||||||
> &
|
> &
|
||||||
Attribute.DefaultTo<0>;
|
Attribute.DefaultTo<0>;
|
||||||
|
standalone: Attribute.Boolean & Attribute.DefaultTo<false>;
|
||||||
updatedAt: Attribute.DateTime;
|
updatedAt: Attribute.DateTime;
|
||||||
updatedBy: Attribute.Relation<"api::product-cover.product-cover", "oneToOne", "admin::user"> & Attribute.Private;
|
updatedBy: Attribute.Relation<"api::product-cover.product-cover", "oneToOne", "admin::user"> & Attribute.Private;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user