calculator.ts•4.05 kB
import { MRPInput, MRPOutput, CalculationStep } from './types';
export function calculateMRP(input: MRPInput): MRPOutput {
const steps: CalculationStep[] = [];
const projectedBalances = [];
// Step 1: Calculate Starting Balance
const startingBalance = input.current_balance + input.open_orders.reduce((sum, order) => sum + order.quantity, 0);
steps.push({
step: 'starting_balance',
description: 'Current balance + Open orders',
value: startingBalance
});
// Step 2: Calculate Total Demand
const totalDemand = input.forecast_periods.reduce((sum, period) => sum + period.quantity, 0);
steps.push({
step: 'total_demand',
description: 'Sum of all forecast periods',
value: totalDemand
});
// Step 3: Calculate Order Need
const orderNeed = (input.must_order_point + totalDemand) - startingBalance;
steps.push({
step: 'order_need',
description: '(Must-order point + Total demand) - Starting balance',
value: orderNeed
});
// Step 4: Calculate Daily Projected Balances
let currentBalance = startingBalance;
let currentDate = new Date(input.analysis_date);
const secondDelivery = new Date(input.delivery_schedule.second_delivery);
while (currentDate < secondDelivery) {
const dateStr = currentDate.toISOString().split('T')[0];
const adjustments = [];
let endingBalance = currentBalance;
// Check for open orders on this date
const todaysOrders = input.open_orders.filter(order =>
order.delivery_date === dateStr
);
if (todaysOrders.length > 0) {
const orderSum = todaysOrders.reduce((sum, order) => sum + order.quantity, 0);
adjustments.push({
type: 'RECEIPT',
quantity: orderSum,
reason: 'Open order delivery'
});
endingBalance += orderSum;
}
// Apply forecast demand
const forecastPeriod = input.forecast_periods.find(period =>
dateStr >= period.start_date && dateStr <= period.end_date
);
if (forecastPeriod) {
const daysInPeriod = (new Date(forecastPeriod.end_date).getTime() -
new Date(forecastPeriod.start_date).getTime()) /
(1000 * 60 * 60 * 24) + 1;
const dailyDemand = forecastPeriod.quantity / daysInPeriod;
adjustments.push({
type: 'DEMAND',
quantity: -dailyDemand,
reason: 'Daily forecast demand'
});
endingBalance -= dailyDemand;
}
projectedBalances.push({
date: dateStr,
starting_balance: currentBalance,
adjustments,
ending_balance: endingBalance
});
currentBalance = endingBalance;
currentDate.setDate(currentDate.getDate() + 1);
}
// Step 5: Apply Batch Size Optimization
let finalOrderNeed = orderNeed;
if (input.batch_sizes && input.batch_sizes.length > 0 && orderNeed > 0) {
const batchSize = input.batch_sizes.find(size => size >= orderNeed) ||
input.batch_sizes[input.batch_sizes.length - 1];
finalOrderNeed = batchSize;
steps.push({
step: 'batch_optimization',
description: 'Rounded to nearest valid batch size',
value: finalOrderNeed
});
}
// Prepare validation messages
const messages = [];
const lowestProjectedBalance = Math.min(...projectedBalances.map(pb => pb.ending_balance));
if (lowestProjectedBalance < input.must_order_point) {
messages.push(`Warning: Projected balance falls below must-order point (${input.must_order_point})`);
}
return {
order_need: finalOrderNeed,
calculation_steps: steps,
validation: {
status: messages.length > 0 ? 'WARNING' : 'SUCCESS',
messages
},
projected_balances: projectedBalances
};
}