feat: Centralize pricing calculations into a new calculateTotals utility function and Totals interface.
This commit is contained in:
@@ -4,7 +4,7 @@ import * as readline from 'node:readline/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createElement } from 'react';
|
||||
import { renderToFile } from '@react-pdf/renderer';
|
||||
import { calculatePositions } from '../src/logic/pricing/calculator.js';
|
||||
import { calculatePositions, calculateTotals } from '../src/logic/pricing/calculator.js';
|
||||
import { CombinedQuotePDF } from '../src/components/CombinedQuotePDF.js';
|
||||
import { initialState, PRICING } from '../src/logic/pricing/constants.js';
|
||||
import { getTechDetails, getPrinciples } from '../src/logic/content-provider.js';
|
||||
@@ -35,9 +35,8 @@ async function main() {
|
||||
console.warn('⚠️ Missing recipient name or email. Document might look incomplete.');
|
||||
}
|
||||
|
||||
const totalPrice = calculateTotal(state);
|
||||
const monthlyPrice = calculateMonthly(state);
|
||||
const totalPagesCount = (state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0);
|
||||
const totals = calculateTotals(state, PRICING);
|
||||
const { totalPrice, monthlyPrice, totalPagesCount } = totals;
|
||||
|
||||
const finalOutputPath = generateDefaultPath(state);
|
||||
const outputDir = path.dirname(finalOutputPath);
|
||||
@@ -115,32 +114,6 @@ async function runWizard(state: any) {
|
||||
return state;
|
||||
}
|
||||
|
||||
function calculateTotal(state: any) {
|
||||
// Basic duplication of logic from ContactForm for consistency
|
||||
if (state.projectType !== 'website') return 0;
|
||||
const totalPagesCount = (state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0);
|
||||
let total = PRICING.BASE_WEBSITE;
|
||||
total += totalPagesCount * PRICING.PAGE;
|
||||
total += ((state.features?.length || 0) + (state.otherFeatures?.length || 0) + (state.otherFeaturesCount || 0)) * PRICING.FEATURE;
|
||||
total += ((state.functions?.length || 0) + (state.otherFunctions?.length || 0) + (state.otherFunctionsCount || 0)) * PRICING.FUNCTION;
|
||||
total += ((state.apiSystems?.length || 0) + (state.otherTech?.length || 0) + (state.otherTechCount || 0)) * PRICING.API_INTEGRATION;
|
||||
|
||||
if (state.cmsSetup) {
|
||||
total += PRICING.CMS_SETUP;
|
||||
total += ((state.features?.length || 0) + (state.otherFeatures?.length || 0) + (state.otherFeaturesCount || 0)) * PRICING.CMS_CONNECTION_PER_FEATURE;
|
||||
}
|
||||
|
||||
const languagesCount = state.languagesList?.length || 1;
|
||||
if (languagesCount > 1) {
|
||||
total *= (1 + (languagesCount - 1) * 0.2);
|
||||
}
|
||||
return Math.round(total);
|
||||
}
|
||||
|
||||
function calculateMonthly(state: any) {
|
||||
if (state.projectType !== 'website') return 0;
|
||||
return PRICING.HOSTING_MONTHLY + ((state.storageExpansion || 0) * PRICING.STORAGE_EXPANSION_MONTHLY);
|
||||
}
|
||||
|
||||
function generateDefaultPath(state: any) {
|
||||
const now = new Date();
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as confetti from 'canvas-confetti';
|
||||
|
||||
import { FormState, Step } from './ContactForm/types';
|
||||
import { PRICING, initialState } from './ContactForm/constants';
|
||||
import { calculateTotals } from '../logic/pricing/calculator';
|
||||
import { PriceCalculation } from './ContactForm/components/PriceCalculation';
|
||||
import { ShareModal } from './ShareModal';
|
||||
|
||||
@@ -182,39 +183,12 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
|
||||
}
|
||||
}, [currentUrl, router, isRemotion]);
|
||||
|
||||
const totalPagesCount = useMemo(() => {
|
||||
return state.selectedPages.length + state.otherPages.length + (state.otherPagesCount || 0);
|
||||
}, [state.selectedPages, state.otherPages, state.otherPagesCount]);
|
||||
|
||||
const totalPrice = useMemo(() => {
|
||||
if (state.projectType !== 'website') return 0;
|
||||
|
||||
let total = PRICING.BASE_WEBSITE;
|
||||
total += totalPagesCount * PRICING.PAGE;
|
||||
total += (state.features.length + state.otherFeatures.length + (state.otherFeaturesCount || 0)) * PRICING.FEATURE;
|
||||
total += (state.functions.length + state.otherFunctions.length + (state.otherFunctionsCount || 0)) * PRICING.FUNCTION;
|
||||
total += (state.apiSystems.length + state.otherTech.length + (state.otherTechCount || 0)) * PRICING.API_INTEGRATION;
|
||||
total += (state.newDatasets || 0) * PRICING.NEW_DATASET;
|
||||
|
||||
if (state.cmsSetup) {
|
||||
total += PRICING.CMS_SETUP;
|
||||
total += (state.features.length + state.otherFeatures.length + (state.otherFeaturesCount || 0)) * PRICING.CMS_CONNECTION_PER_FEATURE;
|
||||
}
|
||||
|
||||
// Multi-language factor (e.g. +20% per additional language)
|
||||
const languagesCount = state.languagesList.length || 1;
|
||||
if (languagesCount > 1) {
|
||||
total *= (1 + (languagesCount - 1) * 0.2);
|
||||
}
|
||||
|
||||
return Math.round(total);
|
||||
}, [state, totalPagesCount]);
|
||||
|
||||
const monthlyPrice = useMemo(() => {
|
||||
if (state.projectType !== 'website') return 0;
|
||||
return PRICING.HOSTING_MONTHLY + (state.storageExpansion * PRICING.STORAGE_EXPANSION_MONTHLY);
|
||||
const totals = useMemo(() => {
|
||||
return calculateTotals(state, PRICING);
|
||||
}, [state]);
|
||||
|
||||
const { totalPrice, monthlyPrice, totalPagesCount } = totals;
|
||||
|
||||
const updateState = (updates: Partial<FormState>) => {
|
||||
setState((s: FormState) => ({ ...s, ...updates }));
|
||||
};
|
||||
@@ -598,9 +572,7 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
|
||||
</div>
|
||||
<PriceCalculation
|
||||
state={state}
|
||||
totalPrice={totalPrice}
|
||||
monthlyPrice={monthlyPrice}
|
||||
totalPagesCount={totalPagesCount}
|
||||
totals={totals}
|
||||
isClient={isClient}
|
||||
qrCodeData={qrCodeData}
|
||||
onShare={handleShare}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { FormState } from '../types';
|
||||
import { FormState, Totals } from '../types';
|
||||
import { PRICING } from '../constants';
|
||||
import { AnimatedNumber } from './AnimatedNumber';
|
||||
import { ConceptPrice, ConceptAutomation } from '../../Landing/ConceptIllustrations';
|
||||
@@ -20,9 +20,7 @@ const PDFDownloadLink = dynamic(
|
||||
|
||||
interface PriceCalculationProps {
|
||||
state: FormState;
|
||||
totalPrice: number;
|
||||
monthlyPrice: number;
|
||||
totalPagesCount: number;
|
||||
totals: Totals;
|
||||
isClient: boolean;
|
||||
qrCodeData: string;
|
||||
onShare?: () => void;
|
||||
@@ -30,20 +28,15 @@ interface PriceCalculationProps {
|
||||
|
||||
export function PriceCalculation({
|
||||
state,
|
||||
totalPrice,
|
||||
monthlyPrice,
|
||||
totalPagesCount,
|
||||
totals,
|
||||
isClient,
|
||||
qrCodeData,
|
||||
onShare
|
||||
}: PriceCalculationProps) {
|
||||
const totalPages = totalPagesCount + (state.otherPagesCount || 0);
|
||||
const totalFeatures = state.features.length + state.otherFeatures.length + (state.otherFeaturesCount || 0);
|
||||
const totalFunctions = state.functions.length + state.otherFunctions.length + (state.otherFunctionsCount || 0);
|
||||
const totalApis = state.apiSystems.length + state.otherTech.length + (state.otherTechCount || 0);
|
||||
const { totalPrice, monthlyPrice, totalPagesCount, totalFeatures, totalFunctions, totalApis, languagesCount } = totals;
|
||||
const totalPages = totalPagesCount;
|
||||
|
||||
const [pdfLoading, setPdfLoading] = React.useState(false);
|
||||
const languagesCount = state.languagesList.length || 1;
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (pdfLoading) return;
|
||||
@@ -59,7 +52,6 @@ export function PriceCalculation({
|
||||
monthlyPrice={monthlyPrice}
|
||||
totalPagesCount={totalPagesCount}
|
||||
pricing={PRICING}
|
||||
qrCodeData={qrCodeData}
|
||||
headerIcon={typeof IconWhite === 'string' ? IconWhite : (IconWhite as any).src}
|
||||
footerLogo={typeof LogoBlack === 'string' ? LogoBlack : (LogoBlack as any).src}
|
||||
/>;
|
||||
|
||||
@@ -60,6 +60,16 @@ export interface FormState {
|
||||
complexInteractions: string;
|
||||
}
|
||||
|
||||
export interface Totals {
|
||||
totalPrice: number;
|
||||
monthlyPrice: number;
|
||||
totalPagesCount: number;
|
||||
totalFeatures: number;
|
||||
totalFunctions: number;
|
||||
totalApis: number;
|
||||
languagesCount: number;
|
||||
}
|
||||
|
||||
export interface Step {
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
@@ -1,6 +1,67 @@
|
||||
import { FormState, Position } from './types';
|
||||
import { FormState, Position, Totals } from './types';
|
||||
import { FEATURE_LABELS, FUNCTION_LABELS, API_LABELS, PAGE_LABELS } from './constants';
|
||||
|
||||
export function calculateTotals(state: FormState, pricing: any): Totals {
|
||||
if (state.projectType !== 'website') {
|
||||
return {
|
||||
totalPrice: 0,
|
||||
monthlyPrice: 0,
|
||||
totalPagesCount: 0,
|
||||
totalFeatures: 0,
|
||||
totalFunctions: 0,
|
||||
totalApis: 0,
|
||||
languagesCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
const sitemapPagesCount = state.sitemap?.reduce((acc: number, cat: any) => acc + (cat.pages?.length || 0), 0) || 0;
|
||||
const totalPagesCount = Math.max(
|
||||
(state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0),
|
||||
sitemapPagesCount
|
||||
);
|
||||
|
||||
const totalFeatures = (state.features?.length || 0) + (state.otherFeatures?.length || 0) + (state.otherFeaturesCount || 0);
|
||||
const totalFunctions = (state.functions?.length || 0) + (state.otherFunctions?.length || 0) + (state.otherFunctionsCount || 0);
|
||||
const totalApis = (state.apiSystems?.length || 0) + (state.otherTech?.length || 0) + (state.otherTechCount || 0);
|
||||
|
||||
let total = pricing.BASE_WEBSITE;
|
||||
total += totalPagesCount * pricing.PAGE;
|
||||
total += totalFeatures * pricing.FEATURE;
|
||||
total += totalFunctions * pricing.FUNCTION;
|
||||
total += totalApis * pricing.API_INTEGRATION;
|
||||
total += (state.newDatasets || 0) * pricing.NEW_DATASET;
|
||||
|
||||
if (state.cmsSetup) {
|
||||
total += pricing.CMS_SETUP;
|
||||
total += totalFeatures * pricing.CMS_CONNECTION_PER_FEATURE;
|
||||
}
|
||||
|
||||
// Optional visual/complexity boosters (from calculator.ts logic)
|
||||
if (state.visualStaging && !isNaN(Number(state.visualStaging)) && Number(state.visualStaging) > 0) {
|
||||
total += Number(state.visualStaging) * pricing.VISUAL_STAGING;
|
||||
}
|
||||
if (state.complexInteractions && !isNaN(Number(state.complexInteractions)) && Number(state.complexInteractions) > 0) {
|
||||
total += Number(state.complexInteractions) * pricing.COMPLEX_INTERACTION;
|
||||
}
|
||||
|
||||
const languagesCount = state.languagesList?.length || 1;
|
||||
if (languagesCount > 1) {
|
||||
total *= (1 + (languagesCount - 1) * 0.2);
|
||||
}
|
||||
|
||||
const monthlyPrice = pricing.HOSTING_MONTHLY + ((state.storageExpansion || 0) * pricing.STORAGE_EXPANSION_MONTHLY);
|
||||
|
||||
return {
|
||||
totalPrice: Math.round(total),
|
||||
monthlyPrice: Math.round(monthlyPrice),
|
||||
totalPagesCount,
|
||||
totalFeatures,
|
||||
totalFunctions,
|
||||
totalApis,
|
||||
languagesCount
|
||||
};
|
||||
}
|
||||
|
||||
export function calculatePositions(state: FormState, pricing: any): Position[] {
|
||||
const positions: Position[] = [];
|
||||
let pos = 1;
|
||||
|
||||
@@ -78,3 +78,12 @@ export interface Position {
|
||||
price: number;
|
||||
isRecurring?: boolean;
|
||||
}
|
||||
export interface Totals {
|
||||
totalPrice: number;
|
||||
monthlyPrice: number;
|
||||
totalPagesCount: number;
|
||||
totalFeatures: number;
|
||||
totalFunctions: number;
|
||||
totalApis: number;
|
||||
languagesCount: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user