import { z } from 'zod';
import { InputValidationError, RangeError } from './errors.js';
import { logger } from './logger.js';
/**
* Financial value constraints
*/
const FINANCIAL_CONSTRAINTS = {
MIN_AMOUNT: -1e12, // -1 trillion (for costs)
MAX_AMOUNT: 1e12, // 1 trillion
MIN_RATE: -1, // -100% (for negative growth)
MAX_RATE: 10, // 1000%
MIN_PERCENTAGE: 0,
MAX_PERCENTAGE: 1,
MIN_MONTHS: 0,
MAX_MONTHS: 600, // 50 years
MIN_VOLUME: 0,
MAX_VOLUME: 1e9, // 1 billion
MIN_TIME_SECONDS: 0,
MAX_TIME_SECONDS: 86400, // 24 hours
MIN_ITERATIONS: 100,
MAX_ITERATIONS: 1e6
};
/**
* Validates financial amounts (can be negative for costs)
*/
export function validateFinancialAmount(
value: number,
fieldName: string,
allowNegative: boolean = true
): number {
if (typeof value !== 'number' || isNaN(value)) {
throw new InputValidationError(fieldName, value, 'must be a valid number');
}
if (!isFinite(value)) {
throw new InputValidationError(fieldName, value, 'must be a finite number');
}
const min = allowNegative ? FINANCIAL_CONSTRAINTS.MIN_AMOUNT : 0;
const max = FINANCIAL_CONSTRAINTS.MAX_AMOUNT;
if (value < min || value > max) {
throw new RangeError(fieldName, value, min, max);
}
return value;
}
/**
* Validates percentage values (0-1)
*/
export function validatePercentage(
value: number,
fieldName: string
): number {
if (typeof value !== 'number' || isNaN(value)) {
throw new InputValidationError(fieldName, value, 'must be a valid number');
}
if (value < FINANCIAL_CONSTRAINTS.MIN_PERCENTAGE ||
value > FINANCIAL_CONSTRAINTS.MAX_PERCENTAGE) {
throw new RangeError(
fieldName,
value,
FINANCIAL_CONSTRAINTS.MIN_PERCENTAGE,
FINANCIAL_CONSTRAINTS.MAX_PERCENTAGE
);
}
return value;
}
/**
* Validates rate values (can be negative for decline)
*/
export function validateRate(
value: number,
fieldName: string
): number {
if (typeof value !== 'number' || isNaN(value)) {
throw new InputValidationError(fieldName, value, 'must be a valid number');
}
if (value < FINANCIAL_CONSTRAINTS.MIN_RATE ||
value > FINANCIAL_CONSTRAINTS.MAX_RATE) {
throw new RangeError(
fieldName,
value,
FINANCIAL_CONSTRAINTS.MIN_RATE,
FINANCIAL_CONSTRAINTS.MAX_RATE
);
}
return value;
}
/**
* Validates volume/count values
*/
export function validateVolume(
value: number,
fieldName: string
): number {
if (typeof value !== 'number' || isNaN(value)) {
throw new InputValidationError(fieldName, value, 'must be a valid number');
}
if (!Number.isInteger(value)) {
logger.warn(`Non-integer volume value for ${fieldName}, rounding`, { value });
value = Math.round(value);
}
if (value < FINANCIAL_CONSTRAINTS.MIN_VOLUME ||
value > FINANCIAL_CONSTRAINTS.MAX_VOLUME) {
throw new RangeError(
fieldName,
value,
FINANCIAL_CONSTRAINTS.MIN_VOLUME,
FINANCIAL_CONSTRAINTS.MAX_VOLUME
);
}
return value;
}
/**
* Validates time duration in months
*/
export function validateMonths(
value: number,
fieldName: string
): number {
if (typeof value !== 'number' || isNaN(value)) {
throw new InputValidationError(fieldName, value, 'must be a valid number');
}
if (!Number.isInteger(value)) {
logger.warn(`Non-integer month value for ${fieldName}, rounding`, { value });
value = Math.round(value);
}
if (value < FINANCIAL_CONSTRAINTS.MIN_MONTHS ||
value > FINANCIAL_CONSTRAINTS.MAX_MONTHS) {
throw new RangeError(
fieldName,
value,
FINANCIAL_CONSTRAINTS.MIN_MONTHS,
FINANCIAL_CONSTRAINTS.MAX_MONTHS
);
}
return value;
}
/**
* Validates Monte Carlo iterations
*/
export function validateIterations(
value: number,
fieldName: string = 'iterations'
): number {
if (typeof value !== 'number' || isNaN(value)) {
throw new InputValidationError(fieldName, value, 'must be a valid number');
}
if (!Number.isInteger(value)) {
value = Math.round(value);
}
if (value < FINANCIAL_CONSTRAINTS.MIN_ITERATIONS ||
value > FINANCIAL_CONSTRAINTS.MAX_ITERATIONS) {
throw new RangeError(
fieldName,
value,
FINANCIAL_CONSTRAINTS.MIN_ITERATIONS,
FINANCIAL_CONSTRAINTS.MAX_ITERATIONS
);
}
return value;
}
/**
* Zod schemas with custom refinements
*/
export const FinancialAmountSchema = z.number()
.refine(val => !isNaN(val), { message: 'Must be a valid number' })
.refine(val => isFinite(val), { message: 'Must be a finite number' })
.refine(
val => val >= FINANCIAL_CONSTRAINTS.MIN_AMOUNT && val <= FINANCIAL_CONSTRAINTS.MAX_AMOUNT,
{ message: `Must be between ${FINANCIAL_CONSTRAINTS.MIN_AMOUNT} and ${FINANCIAL_CONSTRAINTS.MAX_AMOUNT}` }
);
export const PositiveFinancialAmountSchema = z.number()
.refine(val => !isNaN(val), { message: 'Must be a valid number' })
.refine(val => isFinite(val), { message: 'Must be a finite number' })
.refine(val => val >= 0, { message: 'Must be non-negative' })
.refine(
val => val <= FINANCIAL_CONSTRAINTS.MAX_AMOUNT,
{ message: `Must be less than ${FINANCIAL_CONSTRAINTS.MAX_AMOUNT}` }
);
export const PercentageSchema = z.number()
.refine(val => !isNaN(val), { message: 'Must be a valid number' })
.refine(
val => val >= 0 && val <= 1,
{ message: 'Must be between 0 and 1' }
);
export const RateSchema = z.number()
.refine(val => !isNaN(val), { message: 'Must be a valid number' })
.refine(
val => val >= FINANCIAL_CONSTRAINTS.MIN_RATE && val <= FINANCIAL_CONSTRAINTS.MAX_RATE,
{ message: `Must be between ${FINANCIAL_CONSTRAINTS.MIN_RATE} and ${FINANCIAL_CONSTRAINTS.MAX_RATE}` }
);
export const VolumeSchema = z.number()
.refine(val => !isNaN(val), { message: 'Must be a valid number' })
.refine(val => val >= 0, { message: 'Must be non-negative' })
.refine(
val => val <= FINANCIAL_CONSTRAINTS.MAX_VOLUME,
{ message: `Must be less than ${FINANCIAL_CONSTRAINTS.MAX_VOLUME}` }
)
.transform(val => Math.round(val)); // Auto-round to integer
export const MonthsSchema = z.number()
.refine(val => !isNaN(val), { message: 'Must be a valid number' })
.refine(val => val >= 0, { message: 'Must be non-negative' })
.refine(
val => val <= FINANCIAL_CONSTRAINTS.MAX_MONTHS,
{ message: `Must be less than ${FINANCIAL_CONSTRAINTS.MAX_MONTHS} months` }
)
.transform(val => Math.round(val)); // Auto-round to integer
export const IterationsSchema = z.number()
.refine(val => !isNaN(val), { message: 'Must be a valid number' })
.refine(
val => val >= FINANCIAL_CONSTRAINTS.MIN_ITERATIONS && val <= FINANCIAL_CONSTRAINTS.MAX_ITERATIONS,
{ message: `Must be between ${FINANCIAL_CONSTRAINTS.MIN_ITERATIONS} and ${FINANCIAL_CONSTRAINTS.MAX_ITERATIONS}` }
)
.transform(val => Math.round(val)); // Auto-round to integer
/**
* Validates an entire use case object
*/
export function validateUseCase(useCase: any): void {
const { current_state, future_state } = useCase;
// Validate current state
if (current_state) {
if (current_state.volume_per_month !== undefined) {
validateVolume(current_state.volume_per_month, 'current_state.volume_per_month');
}
if (current_state.cost_per_unit !== undefined) {
validateFinancialAmount(current_state.cost_per_unit, 'current_state.cost_per_unit');
}
if (current_state.time_per_unit_hours !== undefined) {
if (current_state.time_per_unit_hours < 0 || current_state.time_per_unit_hours > 168) {
throw new RangeError('current_state.time_per_unit_hours', current_state.time_per_unit_hours, 0, 168);
}
}
if (current_state.error_rate !== undefined) {
validatePercentage(current_state.error_rate, 'current_state.error_rate');
}
if (current_state.revenue_per_unit !== undefined) {
validateFinancialAmount(current_state.revenue_per_unit, 'current_state.revenue_per_unit', false);
}
}
// Validate future state
if (future_state) {
if (future_state.automation_rate !== undefined) {
validatePercentage(future_state.automation_rate, 'future_state.automation_rate');
}
if (future_state.cost_reduction_rate !== undefined) {
validatePercentage(future_state.cost_reduction_rate, 'future_state.cost_reduction_rate');
}
if (future_state.error_reduction_rate !== undefined) {
validatePercentage(future_state.error_reduction_rate, 'future_state.error_reduction_rate');
}
if (future_state.time_reduction_rate !== undefined) {
validatePercentage(future_state.time_reduction_rate, 'future_state.time_reduction_rate');
}
if (future_state.volume_growth_rate !== undefined) {
validateRate(future_state.volume_growth_rate, 'future_state.volume_growth_rate');
}
if (future_state.quality_improvement_rate !== undefined) {
validatePercentage(future_state.quality_improvement_rate, 'future_state.quality_improvement_rate');
}
}
}
/**
* Sanitizes string inputs to prevent injection attacks
*/
export function sanitizeString(input: string, maxLength: number = 1000): string {
if (typeof input !== 'string') {
throw new InputValidationError('input', input, 'must be a string');
}
// Trim and limit length
input = input.trim().substring(0, maxLength);
// Remove control characters
input = input.replace(/[\x00-\x1F\x7F]/g, '');
return input;
}
/**
* Validates array inputs
*/
export function validateArray<T>(
arr: T[],
fieldName: string,
minLength: number = 0,
maxLength: number = 1000
): T[] {
if (!Array.isArray(arr)) {
throw new InputValidationError(fieldName, arr, 'must be an array');
}
if (arr.length < minLength || arr.length > maxLength) {
throw new RangeError(fieldName, arr.length, minLength, maxLength);
}
return arr;
}