where('product_platform_id', $productPlatformId) ->with('options') ->orderBy('sort_order') ->get(); return [ 'groups' => $groups, 'rules' => CpqRule::forBusiness($businessId)->where('is_active', true)->orderBy('priority')->get(), ]; } public function buildConfiguration(int $businessId, array $data): CpqConfiguration { $selectedOptions = $data['selected_options'] ?? []; $bomSnapshot = $this->buildBomSnapshot($businessId, $data['base_bom_id'] ?? null, $selectedOptions); $price = $this->calculatePrice($businessId, $bomSnapshot, $selectedOptions); return CpqConfiguration::create([ 'business_id' => $businessId, 'product_platform_id' => $data['product_platform_id'] ?? null, 'name' => $data['name'] ?? 'Configuration', 'ref_no' => $this->nextRef($businessId), 'status' => 'draft', 'contact_id' => $data['contact_id'] ?? null, 'selected_options' => $selectedOptions, 'bom_snapshot' => $bomSnapshot, 'quoted_price' => $price, 'created_by' => auth()->id(), ]); } public function applyRules(array $selectedOptionIds, int $businessId): array { $rules = CpqRule::forBusiness($businessId)->where('is_active', true)->orderBy('priority')->get(); $result = $selectedOptionIds; foreach ($rules as $rule) { $conditions = $rule->conditions ?? []; $ifSelected = $conditions['if_option_ids'] ?? []; if (empty($ifSelected) || count(array_intersect($ifSelected, $result)) === count($ifSelected)) { foreach ($rule->actions['add_option_ids'] ?? [] as $addId) { if (! in_array($addId, $result)) { $result[] = $addId; } } foreach ($rule->actions['remove_option_ids'] ?? [] as $removeId) { $result = array_values(array_diff($result, [$removeId])); } } } return $result; } protected function buildBomSnapshot(int $businessId, ?int $baseBomId, array $selectedOptionIds): array { $lines = []; if ($baseBomId) { $bom = Bom::forBusiness($businessId)->with('lines')->find($baseBomId); if ($bom) { $lines = $bom->lines->map(fn ($l) => [ 'item_master_id' => $l->item_master_id, 'quantity' => $l->quantity, ])->all(); } } $options = CpqOption::forBusiness($businessId)->whereIn('id', $selectedOptionIds)->get(); foreach ($options as $option) { foreach ($option->bom_additions ?? [] as $add) { $lines[] = $add; } foreach ($option->bom_removals ?? [] as $remove) { $lines = array_filter($lines, fn ($l) => ($l['item_master_id'] ?? null) != ($remove['item_master_id'] ?? null)); } } return ['lines' => array_values($lines), 'base_bom_id' => $baseBomId]; } protected function calculatePrice(int $businessId, array $bomSnapshot, array $selectedOptionIds): float { $price = 0.0; if (! empty($bomSnapshot['base_bom_id'])) { $bom = Bom::forBusiness($businessId)->find($bomSnapshot['base_bom_id']); if ($bom) { $costing = $this->liveBomCosting->analyze($bom, 1); $price += (float) ($costing['totals']['live_material'] ?? 0); } } $options = CpqOption::forBusiness($businessId)->whereIn('id', $selectedOptionIds)->get(); foreach ($options as $option) { $price += (float) $option->price_delta; } return round($price, 4); } protected function nextRef(int $businessId): string { $count = CpqConfiguration::forBusiness($businessId)->withTrashed()->count() + 1; return 'CPQ-'.str_pad((string) $count, 5, '0', STR_PAD_LEFT); } }