import { defineTool, ToolError } from 'libllmorbiter'; const DEFAULT_TAX_RATE = 19; const DEFAULT_PRECISION = 6; /** * Calculates net-to-gross or gross-to-net for a single price. * * @param {object} params * @param {number} [params.net] * @param {number} [params.gross] * @param {number} [params.amount] * @param {'net'|'gross'} [params.from] * @param {number} [params.taxRate=19] * @param {number} [params.precision=6] * @returns {object} */ export function calculatePrice({ net, gross, amount, from, taxRate = DEFAULT_TAX_RATE, precision = DEFAULT_PRECISION }) { const rate = taxRate != null ? Number(taxRate) : DEFAULT_TAX_RATE; if (!Number.isFinite(rate) || rate < 0) { throw new ToolError(`Invalid taxRate: ${taxRate}. Must be a non-negative number.`); } const prec = precision != null ? Math.min(8, Math.max(2, Number(precision) || DEFAULT_PRECISION)) : DEFAULT_PRECISION; const factor = 1 + rate / 100; let inputType = null; let inputValue = null; if (net != null && gross == null) { inputType = 'net'; inputValue = Number(net); } else if (gross != null && net == null) { inputType = 'gross'; inputValue = Number(gross); } else if (amount != null) { inputValue = Number(amount); inputType = from === 'net' ? 'net' : 'gross'; } else if (net != null && gross != null) { // Both provided: validate consistency const n = Number(net); const g = Number(gross); const calculatedGross = +(n * factor).toFixed(2); const effectiveRate = n > 0 ? +(((g - n) / n) * 100).toFixed(2) : 0; return { direction: 'verify', net: +n.toFixed(2), gross: +g.toFixed(2), taxRate: rate, effectiveTaxRate: effectiveRate, isConsistent: Math.abs(calculatedGross - g) <= 0.02, taxAmount: +(g - n).toFixed(2), }; } else { throw new ToolError('net_gross_calc requires "net", "gross", or "amount".'); } if (!Number.isFinite(inputValue) || inputValue < 0) { throw new ToolError(`Invalid price amount: ${inputValue}. Must be a non-negative number.`); } if (inputType === 'gross') { const grossVal = +inputValue.toFixed(2); const exactNet = inputValue / factor; const netRounded = +exactNet.toFixed(2); const netPrecision = +exactNet.toFixed(prec); const taxAmount = +(grossVal - netRounded).toFixed(2); return { direction: 'gross_to_net', gross: grossVal, net: netRounded, netPrecision, netPrecise: exactNet, taxRate: rate, taxAmount, apiNetValue: netPrecision, formatted: { gross: `${grossVal.toFixed(2)} €`, net: `${netRounded.toFixed(2)} €`, tax: `${taxAmount.toFixed(2)} €`, }, }; } else { const netVal = +inputValue.toFixed(2); const exactGross = inputValue * factor; const grossRounded = +exactGross.toFixed(2); const taxAmount = +(grossRounded - netVal).toFixed(2); return { direction: 'net_to_gross', net: netVal, gross: grossRounded, grossPrecise: exactGross, taxRate: rate, taxAmount, formatted: { net: `${netVal.toFixed(2)} €`, gross: `${grossRounded.toFixed(2)} €`, tax: `${taxAmount.toFixed(2)} €`, }, }; } } /** * Creates the net_gross_calc tool. */ export function createNetGrossCalcTool() { return defineTool({ name: 'net_gross_calc', description: 'Convert between net and gross prices (net to gross, gross to net) with VAT/tax rate (default 19%). ' + 'Computes 2-decimal rounded display values and 6-decimal high-precision net values for API article creation.', strict: false, parameters: { type: 'object', additionalProperties: false, properties: { note: { type: 'string', description: 'A note explaining what this price calculation is doing. This note will be displayed to the user while executing.', }, gross: { type: ['number', 'null'], description: 'Gross price (incl. VAT) or null. When provided, computes net price and tax amount.', }, net: { type: ['number', 'null'], description: 'Net price (excl. VAT) or null. When provided, computes gross price and tax amount.', }, taxRate: { type: ['number', 'null'], description: `Tax/VAT rate percentage (e.g. 19 for 19%, 7 for 7%) or null. Default ${DEFAULT_TAX_RATE}.`, }, amount: { type: ['number', 'null'], description: 'Generic price amount or null when used with "from" ("gross" or "net").', }, from: { type: ['string', 'null'], enum: ['gross', 'net', null], description: 'Direction when using "amount": "gross" (default) or "net".', }, precision: { type: ['number', 'null'], description: `Decimal precision for net price API value or null (default ${DEFAULT_PRECISION}, between 2 and 8).`, }, items: { type: ['array', 'null'], description: 'Optional array of prices or objects for batch calculation: [19.99, 49.95] or [{ gross: 19.99, taxRate: 7 }].', items: { type: 'object', additionalProperties: false, properties: { gross: { type: ['number', 'null'] }, net: { type: ['number', 'null'] }, amount: { type: ['number', 'null'] }, from: { type: ['string', 'null'], enum: ['gross', 'net', null] }, taxRate: { type: ['number', 'null'] }, }, }, }, }, }, invoke: async ({ note, gross, net, amount, from, taxRate = DEFAULT_TAX_RATE, precision = DEFAULT_PRECISION, items } = {}) => { if (Array.isArray(items) && items.length > 0) { const results = items.map((item, idx) => { const itemRate = item.taxRate != null ? item.taxRate : taxRate; try { return calculatePrice({ net: item.net, gross: item.gross, amount: item.amount, from: item.from || from, taxRate: itemRate, precision, }); } catch (err) { return { itemIndex: idx, error: err.message }; } }); return JSON.stringify({ count: results.length, results }, null, 2); } const result = calculatePrice({ net, gross, amount, from, taxRate, precision }); return JSON.stringify(result, null, 2); }, }); }