Feature: Implement context-aware pricing for product types
Updates pricing logic to use correct price based on product type:
- Updated getEffectivePrice to accept priceType parameter ('base', 'plain', 'personalised')
- Updated toProductDTO to accept and use priceType when computing effective price
- Updated made-to-order pages to use 'personalised' price type
- Updated products catalog pages to use 'plain' price type
- Updated checkout logic to determine price type based on design
Pricing behavior:
- Personalised orders (with design): use personalisedPrice if set, else basePrice
- Plain/blank orders (no design): use plainPrice if set, else basePrice
- Ready-made orders (no design, no plainPrice): use basePrice
All prices now correctly reflect the product variant being purchased.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
e5fb22aab1
commit
5bd6f3de39
@@ -134,7 +134,8 @@
|
|||||||
"Bash(timeout 30 git push origin main --force)",
|
"Bash(timeout 30 git push origin main --force)",
|
||||||
"Bash(GIT_TRACE=1 git push origin main --force)",
|
"Bash(GIT_TRACE=1 git push origin main --force)",
|
||||||
"Bash(pkill -f \"node.exe\")",
|
"Bash(pkill -f \"node.exe\")",
|
||||||
"PowerShell(Stop-Process -Name node -Force -ErrorAction SilentlyContinue)"
|
"PowerShell(Stop-Process -Name node -Force -ErrorAction SilentlyContinue)",
|
||||||
|
"Bash(find \"D:\\\\Craft2Prints\\\\src\\\\app\" -path \"*/\\\\[slug\\\\]/*\" -name \"page.tsx\" | head -10)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,9 +110,19 @@ export async function POST(req: Request) {
|
|||||||
const pricedItems = items.map((i) => {
|
const pricedItems = items.map((i) => {
|
||||||
const design = typeof i.designJson === 'string' ? JSON.parse(i.designJson) : i.designJson;
|
const design = typeof i.designJson === 'string' ? JSON.parse(i.designJson) : i.designJson;
|
||||||
const isPersonalised = (design.front?.length ?? 0) > 0 || (design.back?.length ?? 0) > 0;
|
const isPersonalised = (design.front?.length ?? 0) > 0 || (design.back?.length ?? 0) > 0;
|
||||||
|
const product = productsById.get(i.productId)!;
|
||||||
|
|
||||||
|
// Determine which price to use
|
||||||
|
let priceType: 'base' | 'plain' | 'personalised' = 'base';
|
||||||
|
if (isPersonalised) {
|
||||||
|
priceType = 'personalised';
|
||||||
|
} else if (product.plainPrice != null) {
|
||||||
|
priceType = 'plain';
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...i,
|
...i,
|
||||||
unitPrice: getEffectivePrice(productsById.get(i.productId)!, activePromotions, new Date(), isPersonalised).price,
|
unitPrice: getEffectivePrice(product, activePromotions, new Date(), priceType).price,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ export default async function ProductDetailPage({
|
|||||||
const row = await prisma.product.findUnique({ where: { slug: params.slug } });
|
const row = await prisma.product.findUnique({ where: { slug: params.slug } });
|
||||||
if (!row || !row.showOnPersonalised) notFound();
|
if (!row || !row.showOnPersonalised) notFound();
|
||||||
|
|
||||||
const product = toProductDTO(row, activePromotions);
|
const product = toProductDTO(row, activePromotions, 'personalised');
|
||||||
|
|
||||||
const relatedRows = await prisma.product.findMany({
|
const relatedRows = await prisma.product.findMany({
|
||||||
where: { category: product.category, showOnPersonalised: true, NOT: { id: product.id } },
|
where: { category: product.category, showOnPersonalised: true, NOT: { id: product.id } },
|
||||||
take: 4,
|
take: 4,
|
||||||
});
|
});
|
||||||
const related = relatedRows.map((p) => toProductDTO(p, activePromotions));
|
const related = relatedRows.map((p) => toProductDTO(p, activePromotions, 'personalised'));
|
||||||
|
|
||||||
// Fetch custom photo from request if customRequestId is provided
|
// Fetch custom photo from request if customRequestId is provided
|
||||||
let customPhoto: string | undefined;
|
let customPhoto: string | undefined;
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export default async function ProductsPage({ searchParams }: { searchParams: Sea
|
|||||||
|
|
||||||
const activePromotions = await fetchActivePromotions();
|
const activePromotions = await fetchActivePromotions();
|
||||||
const rows = await prisma.product.findMany({ where, orderBy: { createdAt: 'asc' } });
|
const rows = await prisma.product.findMany({ where, orderBy: { createdAt: 'asc' } });
|
||||||
const products = rows.map((p) => toProductDTO(p, activePromotions));
|
const products = rows.map((p) => toProductDTO(p, activePromotions, 'personalised'));
|
||||||
|
|
||||||
const hasFilters =
|
const hasFilters =
|
||||||
selectedCategories.length > 0 ||
|
selectedCategories.length > 0 ||
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ export default async function StandardProductPage({ params }: { params: { slug:
|
|||||||
const row = await prisma.product.findUnique({ where: { slug: params.slug } });
|
const row = await prisma.product.findUnique({ where: { slug: params.slug } });
|
||||||
if (!row || !row.showOnProducts) notFound();
|
if (!row || !row.showOnProducts) notFound();
|
||||||
|
|
||||||
const product = toProductDTO(row, activePromotions);
|
const product = toProductDTO(row, activePromotions, 'plain');
|
||||||
|
|
||||||
const relatedRows = await prisma.product.findMany({
|
const relatedRows = await prisma.product.findMany({
|
||||||
where: { category: product.category, showOnProducts: true, NOT: { id: product.id } },
|
where: { category: product.category, showOnProducts: true, NOT: { id: product.id } },
|
||||||
take: 4,
|
take: 4,
|
||||||
});
|
});
|
||||||
const related = relatedRows.map((p) => toProductDTO(p, activePromotions));
|
const related = relatedRows.map((p) => toProductDTO(p, activePromotions, 'plain'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-6xl px-6 py-12">
|
<div className="mx-auto max-w-6xl px-6 py-12">
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default async function ReadyMadeProductsPage({ searchParams }: { searchPa
|
|||||||
|
|
||||||
const activePromotions = await fetchActivePromotions();
|
const activePromotions = await fetchActivePromotions();
|
||||||
const rows = await prisma.product.findMany({ where, orderBy: { createdAt: 'asc' } });
|
const rows = await prisma.product.findMany({ where, orderBy: { createdAt: 'asc' } });
|
||||||
const products = rows.map((p) => toProductDTO(p, activePromotions));
|
const products = rows.map((p) => toProductDTO(p, activePromotions, 'plain'));
|
||||||
|
|
||||||
const hasFilters =
|
const hasFilters =
|
||||||
selectedCategories.length > 0 ||
|
selectedCategories.length > 0 ||
|
||||||
|
|||||||
+8
-2
@@ -1,6 +1,7 @@
|
|||||||
type SalePricingFields = {
|
type SalePricingFields = {
|
||||||
id: string;
|
id: string;
|
||||||
basePrice: number;
|
basePrice: number;
|
||||||
|
plainPrice: number | null;
|
||||||
personalisedPrice: number | null;
|
personalisedPrice: number | null;
|
||||||
salePrice: number | null;
|
salePrice: number | null;
|
||||||
saleStartsAt: Date | null;
|
saleStartsAt: Date | null;
|
||||||
@@ -44,9 +45,14 @@ export function getEffectivePrice(
|
|||||||
product: SalePricingFields,
|
product: SalePricingFields,
|
||||||
activePromotions: ActivePromotion[] = [],
|
activePromotions: ActivePromotion[] = [],
|
||||||
now = new Date(),
|
now = new Date(),
|
||||||
usePersonalisedPrice = false,
|
priceType: 'base' | 'plain' | 'personalised' = 'base',
|
||||||
) {
|
) {
|
||||||
const basePrice = usePersonalisedPrice && product.personalisedPrice != null ? product.personalisedPrice : product.basePrice;
|
let basePrice = product.basePrice;
|
||||||
|
if (priceType === 'plain' && product.plainPrice != null) {
|
||||||
|
basePrice = product.plainPrice;
|
||||||
|
} else if (priceType === 'personalised' && product.personalisedPrice != null) {
|
||||||
|
basePrice = product.personalisedPrice;
|
||||||
|
}
|
||||||
const candidates = [basePrice];
|
const candidates = [basePrice];
|
||||||
|
|
||||||
const manualSaleActive =
|
const manualSaleActive =
|
||||||
|
|||||||
+3
-2
@@ -2,8 +2,8 @@ import type { Product as PrismaProduct, DesignProof as PrismaDesignProof } from
|
|||||||
import type { ProductDTO, ProofDTO } from './types';
|
import type { ProductDTO, ProofDTO } from './types';
|
||||||
import { getEffectivePrice, type ActivePromotion } from './pricing';
|
import { getEffectivePrice, type ActivePromotion } from './pricing';
|
||||||
|
|
||||||
export function toProductDTO(p: PrismaProduct, activePromotions: ActivePromotion[] = []): ProductDTO {
|
export function toProductDTO(p: PrismaProduct, activePromotions: ActivePromotion[] = [], priceType: 'base' | 'plain' | 'personalised' = 'base'): ProductDTO {
|
||||||
const { onSale, price, originalPrice } = getEffectivePrice(p, activePromotions);
|
const { onSale, price, originalPrice } = getEffectivePrice(p, activePromotions, new Date(), priceType);
|
||||||
return {
|
return {
|
||||||
id: p.id,
|
id: p.id,
|
||||||
slug: p.slug,
|
slug: p.slug,
|
||||||
@@ -11,6 +11,7 @@ export function toProductDTO(p: PrismaProduct, activePromotions: ActivePromotion
|
|||||||
category: p.category,
|
category: p.category,
|
||||||
description: p.description,
|
description: p.description,
|
||||||
basePrice: p.basePrice,
|
basePrice: p.basePrice,
|
||||||
|
plainPrice: p.plainPrice,
|
||||||
personalisedPrice: p.personalisedPrice,
|
personalisedPrice: p.personalisedPrice,
|
||||||
colors: JSON.parse(p.colors),
|
colors: JSON.parse(p.colors),
|
||||||
colorPhotos: p.colorPhotos ? JSON.parse(p.colorPhotos) : {},
|
colorPhotos: p.colorPhotos ? JSON.parse(p.colorPhotos) : {},
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export type ProductDTO = {
|
|||||||
category: string; // matches a Category.slug — categories are admin-managed, not a fixed set
|
category: string; // matches a Category.slug — categories are admin-managed, not a fixed set
|
||||||
description: string;
|
description: string;
|
||||||
basePrice: number;
|
basePrice: number;
|
||||||
|
plainPrice: number | null; // price for plain/blank items; null = use basePrice
|
||||||
personalisedPrice: number | null; // higher price for made-to-order items; null = use basePrice
|
personalisedPrice: number | null; // higher price for made-to-order items; null = use basePrice
|
||||||
colors: string[];
|
colors: string[];
|
||||||
colorPhotos: Record<string, string>;
|
colorPhotos: Record<string, string>;
|
||||||
|
|||||||
Reference in New Issue
Block a user