ultimatepos/Modules/IndustrialEngineering/Services/CpqService.php

128 lines
4.7 KiB
PHP

<?php
namespace Modules\IndustrialEngineering\Services;
use Modules\IndustrialEngineering\Models\Bom;
use Modules\IndustrialEngineering\Models\CpqConfiguration;
use Modules\IndustrialEngineering\Models\CpqOption;
use Modules\IndustrialEngineering\Models\CpqOptionGroup;
use Modules\IndustrialEngineering\Models\CpqRule;
class CpqService
{
public function __construct(
protected LiveBomCostingService $liveBomCosting,
protected BomTreeService $bomTree
) {
}
public function getConfigurator(int $businessId, int $productPlatformId): array
{
$groups = CpqOptionGroup::forBusiness($businessId)
->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);
}
}