Hello from MCP server
import type { CostsMaterialRecord, CostsTimeRecord } from "@/pocketbase-types";
import type { VarUnit, VarMeta } from "@/lib/tnfrLegacyWithHours";
import { convertToPrefs, convertManyToPrefs, type ConvertedValue } from "@/framework/currencies";
// Debug control - disabled
const debug = {
log: (...args: any[]) => {
// Logging disabled
},
error: (...args: any[]) => {
// Logging disabled
},
};
// Helper to check if a variable is in the new VarMeta format
function isVarMeta(v: any): v is VarMeta {
return v !== null && typeof v === "object" && "value" in v && "unit" in v;
}
// Helper to get the raw value from a var (handles both old and new formats)
function getValue(v: any): number | number[][] {
if (isVarMeta(v)) {
return v.value;
}
return v;
}
// Helper to set the value on a var (handles both old and new formats)
function setValue(container: Record<string, any>, key: string, value: number): void {
if (isVarMeta(container[key])) {
container[key].value = value;
} else {
container[key] = value;
}
}
export interface FormulaStepValue {
name: string;
value: number | number[][];
unit?: VarUnit;
convertedCurrency?: ConvertedValue | null;
}
export interface FormulaStep {
op: string;
in1: FormulaStepValue;
in2: FormulaStepValue;
out: FormulaStepValue;
}
export interface CalculatePriceResult {
finalPrice: number;
finalPriceConverted: ConvertedValue | null;
formulaSteps: FormulaStep[];
derivedVars: Record<string, number>;
derivedVarsConverted: Record<string, ConvertedValue> | null;
}
/* A Basic Formula
*
* This is an example formula and a default in cases where a formula is not
* provided.
*
* All variables that the fomula uses should provide default values. In normal
* usage, we expect organization, sessions and menus to create values for these
* variables, which can be passed inot the calculatPrice function.
*
* This formula is simple:
*
* 1. Multiply the base hours by the hourly fee
* 2. Multiply the base material cost by the markup rate
* 3. Add together the time and material fees to get the final fee
* 4. Subtract a flat discount of "0.5 XAG"
*
* Everything is calculated using the base currency of ounces of silver (XAG)
* and all monetary values for variables must be in XAG. The user doesn't ever
* have to know about this because all interactions with currency in the UI,
* including in the currency builder and in setting org variables will
* automatically convert to/from their preferred currency. But in the database,
* these values all must be XAG.
*
* Subtraction looks like minus(10, 2) = 8
* i.e. var1 - var2
* i.e. var2 is subtracted from var1
*
* There are a few special variables
*
* `materialCostBase` will be calculated by the function by adding up all of the
* material costs associated with the offer in the database
*
* `timeCostBase` will be calculated by adding up all of the time costs
* associated with the offer
*
* `finalPrice` will always be returned by the function as the final price. If
* a formula doesn't set it's desired output to `finalPrice` then it won't work
* as expected.
*
* Outputs can only be assigned to derivedVars. Changing the value of the
* original vars will not work
*/
const aBasicPricingFormula = {
vars: {
materialCostBase: 0,
timeCostBase: 0,
hourlyFee: 5.262,
materialMarkup: 1.05,
standardDiscount: 0.5,
},
derivedVars: {
materialFee: 0,
timeFee: 0,
totalFee: 0,
finalPrice: 0,
},
operations: [
{
op: "mult",
in1: "timeCostBase",
in2: "hourlyFee",
out: "timeFee",
},
{
op: "mult",
in1: "materialCostBase",
in2: "materialMarkup",
out: "materialFee",
},
{
op: "add",
in1: "timeFee",
in2: "materialFee",
out: "totalFee",
},
{
op: "sub",
in1: "totalFee",
in2: "standardDiscount",
out: "finalPrice",
},
],
};
// TODO: this would be a good candidate for some unit tests, especially as
// all of the features (like scales) get added to the formulas
export default async function (
costsMaterial: CostsMaterialRecord[],
costsTime: CostsTimeRecord[],
f: any, // the formula with var values filled in
extraHours: number = 0, // optional extra hours to add to timeCostBase
baseTimeCost?: number, // optional base time cost to use instead of calculating from costsTime
): Promise<CalculatePriceResult> {
const formulaSteps: FormulaStep[] = [];
// Calculate materialCostBase from array only if array is non-empty
// This allows pre-setting the value in the formula for testing or direct use
if (costsMaterial.length > 0) {
const materialTotal = costsMaterial.reduce(
(total, item) => total + (Number(item?.quantity) || 0),
0,
);
setValue(f.vars, "materialCostBase", materialTotal);
}
// Calculate timeCostBase:
// 1. If baseTimeCost is provided, use it
// 2. Else if costsTime array is non-empty, calculate from it
// 3. Else preserve existing value in formula
// Always add extraHours if provided
if (baseTimeCost !== undefined) {
setValue(f.vars, "timeCostBase", baseTimeCost + extraHours);
} else if (costsTime.length > 0) {
const timeTotal = costsTime.reduce(
(total, item) => total + (item?.hours || 0),
0,
) + extraHours;
setValue(f.vars, "timeCostBase", timeTotal);
} else if (extraHours > 0) {
const currentTime = getValue(f.vars.timeCostBase) as number || 0;
setValue(f.vars, "timeCostBase", currentTime + extraHours);
}
debug.log("=== Starting Formula Calculation ===");
debug.log("Initial values:", {
materialCostBase: getValue(f.vars.materialCostBase),
timeCostBase: getValue(f.vars.timeCostBase),
hourlyFee: getValue(f.vars.hourlyFee),
multiplier: getValue(f.vars.multiplier),
});
// Helper to get unit from a var
function getUnit(v: string): VarUnit | undefined {
if (f.vars[v] != undefined && isVarMeta(f.vars[v])) {
return f.vars[v].unit;
} else if (f.derivedVars[v] != undefined && isVarMeta(f.derivedVars[v])) {
return f.derivedVars[v].unit;
}
return undefined;
}
function findVar(v: string): number | number[][] {
if (f.vars[v] != undefined) {
return getValue(f.vars[v]);
} else if (f.derivedVars[v] != undefined) {
return getValue(f.derivedVars[v]);
} else {
throw new Error(
`Variable ${v} not found in formula ${JSON.stringify(f)}`,
);
}
}
for (const step of f.operations) {
const in1 = findVar(step.in1);
const in2 = findVar(step.in2);
let result: number;
switch (step.op) {
case "add":
result = (in1 as number) + (in2 as number);
setValue(f.derivedVars, step.out, result);
debug.log(
`Step: ${step.out} = ${step.in1}(${in1}) + ${step.in2}(${in2}) = ${result}`,
);
break;
case "sub":
result = (in1 as number) - (in2 as number);
setValue(f.derivedVars, step.out, result);
debug.log(
`Step: ${step.out} = ${step.in1}(${in1}) - ${step.in2}(${in2}) = ${result}`,
);
break;
case "mult":
result = (in1 as number) * (in2 as number);
setValue(f.derivedVars, step.out, result);
debug.log(
`Step: ${step.out} = ${step.in1}(${in1}) * ${step.in2}(${in2}) = ${result}`,
);
break;
case "scale":
// in1 must be the scale and in2 the value to match
const match = (in1 as number[][]).find(
(pair) => pair[0] <= (in2 as number),
);
result = match ? match[1] : 1;
setValue(f.derivedVars, step.out, result);
debug.log(
`Step: ${step.out} = scale(${step.in2}(${in2})) = ${result} (matched: [${match ? match.join(",") : "none"}])`,
);
break;
case "div":
// Avoid division by zero
result = (in2 as number) !== 0 ? (in1 as number) / (in2 as number) : 0;
setValue(f.derivedVars, step.out, result);
debug.log(
`Step: ${step.out} = ${step.in1}(${in1}) / ${step.in2}(${in2}) = ${result}`,
);
break;
default:
debug.error("No operation defined for:", step.op);
result = 0;
}
// Record the step with unit information
const in1Unit = getUnit(step.in1);
const in2Unit = getUnit(step.in2);
const outUnit = getUnit(step.out);
formulaSteps.push({
op: step.op,
in1: {
name: step.in1,
value: in1,
unit: in1Unit,
convertedCurrency: in1Unit === 'currency' && typeof in1 === 'number'
? await convertToPrefs(in1)
: undefined,
},
in2: {
name: step.in2,
value: in2,
unit: in2Unit,
convertedCurrency: in2Unit === 'currency' && typeof in2 === 'number'
? await convertToPrefs(in2)
: undefined,
},
out: {
name: step.out,
value: result,
unit: outUnit,
convertedCurrency: outUnit === 'currency'
? await convertToPrefs(result)
: undefined,
},
});
}
debug.log("=== Formula Calculation Complete ===");
debug.log("Final result:", getValue(f.derivedVars.finalPrice));
// Extract all derived var values as plain numbers
const derivedVars: Record<string, number> = {};
for (const key of Object.keys(f.derivedVars)) {
derivedVars[key] = getValue(f.derivedVars[key]) as number;
}
const finalPrice = getValue(f.derivedVars.finalPrice) as number;
// Convert to user's preferred currency using saved prefs
const finalPriceConverted = await convertToPrefs(finalPrice);
const derivedVarsConverted = await convertManyToPrefs(derivedVars);
return {
finalPrice,
finalPriceConverted,
formulaSteps,
derivedVars,
derivedVarsConverted,
};
}