$alternateSelections bom_line_id => alternate_item_id */ public function buildScenario( int $businessId, Bom $bom, string $scenarioCode, string $name, array $alternateSelections = [], string $scenarioType = 'what_if' ): CostScenario { return DB::transaction(function () use ($businessId, $bom, $scenarioCode, $name, $alternateSelections, $scenarioType) { $scenario = CostScenario::create([ 'business_id' => $businessId, 'line_revision_id' => $bom->line_revision_id, 'bom_id' => $bom->id, 'scenario_code' => $scenarioCode, 'name' => $name, 'scenario_type' => $scenarioType, 'status' => 'draft', 'created_by' => auth()->id(), ]); $flat = $this->bomService->explode($bom); $materialTotal = 0; foreach ($flat as $row) { $bomLineId = $row['bom_line_id']; $selectedAlternateId = $alternateSelections[$bomLineId] ?? null; $unitCost = $row['unit_cost']; $itemId = $row['item_master_id']; if ($selectedAlternateId) { $altItem = ItemMaster::find($selectedAlternateId); if ($altItem) { $unitCost = (float) $altItem->standard_cost; $itemId = $altItem->id; } } $extended = $row['quantity'] * $unitCost; $materialTotal += $extended; CostScenarioLine::create([ 'cost_scenario_id' => $scenario->id, 'bom_line_id' => $bomLineId, 'item_master_id' => $itemId, 'selected_alternate_item_id' => $selectedAlternateId, 'quantity' => $row['quantity'], 'unit_cost' => $unitCost, 'extended_cost' => $extended, 'lead_time_days' => $row['lead_time_days'], ]); } $routingCosts = $this->calculateRoutingCosts($bom->line_revision_id); $overhead = $materialTotal * 0.1; $scenario->update([ 'total_material_cost' => $materialTotal, 'total_labor_cost' => $routingCosts['labor'], 'total_machine_cost' => $routingCosts['machine'], 'total_overhead_cost' => $overhead, 'total_cost' => $materialTotal + $routingCosts['labor'] + $routingCosts['machine'] + $overhead, ]); return $scenario->fresh(['lines.itemMaster', 'lines.selectedAlternateItem']); }); } protected function calculateRoutingCosts(?int $lineRevisionId): array { if (! $lineRevisionId) { return ['labor' => 0, 'machine' => 0]; } $operations = RoutingOperation::with('workCenter') ->where('line_revision_id', $lineRevisionId) ->get(); $labor = 0; $machine = 0; foreach ($operations as $op) { $wc = $op->workCenter; if (! $wc) { continue; } $hours = (float) $op->setup_time_hours + (float) $op->run_time_hours; $labor += $hours * (float) $wc->labor_rate_per_hour; $machine += $hours * (float) $wc->machine_rate_per_hour; } return ['labor' => $labor, 'machine' => $machine]; } /** * Compare multiple scenarios side by side. */ public function compareScenarios(array $scenarioIds): array { $scenarios = CostScenario::with('lines.itemMaster') ->whereIn('id', $scenarioIds) ->get(); return $scenarios->map(fn (CostScenario $s) => [ 'id' => $s->id, 'scenario_code' => $s->scenario_code, 'name' => $s->name, 'total_material_cost' => (float) $s->total_material_cost, 'total_labor_cost' => (float) $s->total_labor_cost, 'total_machine_cost' => (float) $s->total_machine_cost, 'total_overhead_cost' => (float) $s->total_overhead_cost, 'total_cost' => (float) $s->total_cost, 'line_count' => $s->lines->count(), ])->values()->all(); } /** * What-if: replace one component and recalculate. */ public function whatIfReplaceComponent( CostScenario $baseScenario, int $bomLineId, int $alternateItemId, string $newScenarioCode ): CostScenario { $selections = []; foreach ($baseScenario->lines as $line) { if ($line->bom_line_id) { $selections[$line->bom_line_id] = $line->selected_alternate_item_id; } } $selections[$bomLineId] = $alternateItemId; $bom = $baseScenario->bom; if (! $bom) { throw new \RuntimeException('Base scenario has no linked BOM.'); } return $this->buildScenario( $baseScenario->business_id, $bom, $newScenarioCode, $baseScenario->name.' (What-if)', $selections, 'what_if' ); } }