Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
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,
  };
}