ultimatepos/Modules/SupplyChain/Services/SupplyChainIntegrationService.php

163 lines
5.2 KiB
PHP

<?php
namespace Modules\SupplyChain\Services;
use App\Utils\ModuleUtil;
use App\Variation;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Modules\IndustrialEngineering\Models\MrpRun;
use Modules\SupplyChain\Models\DemandForecast;
class SupplyChainIntegrationService
{
public function __construct(
protected DemandForecastService $demandForecastService,
protected RfqService $rfqService,
protected ModuleUtil $moduleUtil
) {
}
/**
* Hook after IE MRP run — import shortages as demand forecasts and optionally create RFQ.
*
* @param array<string, mixed>|MrpRun $runOrData
* @return array<string, mixed>
*/
public function afterMrpRun($runOrData): array
{
if (! $this->moduleUtil->isModuleInstalled('SupplyChain')) {
return ['forecasts' => 0, 'rfq_id' => null];
}
$run = $runOrData instanceof MrpRun ? $runOrData : null;
$businessId = $run?->business_id ?? (int) ($runOrData['business_id'] ?? 0);
$analysis = $run?->shortage_analysis ?? ($runOrData['shortage_analysis'] ?? []);
$shortages = $analysis['shortages'] ?? [];
if ($businessId <= 0 || empty($shortages)) {
return ['forecasts' => 0, 'rfq_id' => null];
}
$forecastMonth = now()->addMonth()->startOfMonth();
$count = 0;
foreach ($shortages as $shortage) {
$productId = $this->resolveProductId($shortage);
if (! $productId) {
continue;
}
DemandForecast::updateOrCreate(
[
'business_id' => $businessId,
'product_id' => $productId,
'variation_id' => ! empty($shortage['variation_id']) ? (int) $shortage['variation_id'] : null,
'forecast_month' => $forecastMonth->toDateString(),
'method' => 'mrp_shortage',
],
[
'forecast_qty' => (float) ($shortage['shortage_qty'] ?? 0),
'confidence' => 85.0,
'source' => 'mrp',
'meta' => [
'mrp_run_id' => $run?->id,
'mrp_run_code' => $run?->run_code,
],
]
);
$count++;
}
return ['forecasts' => $count, 'rfq_id' => null];
}
/**
* Create RFQ from latest MRP run shortages.
*/
public function createRfqFromMrpRun(int $businessId, int $mrpRunId, ?array $supplierIds = null): ?int
{
if (! $this->moduleUtil->isModuleInstalled('IndustrialEngineering')
|| ! Schema::hasTable('ie_mrp_runs')) {
return null;
}
$run = MrpRun::forBusiness($businessId)->find($mrpRunId);
if (! $run || empty($run->shortage_analysis['shortages'])) {
return null;
}
$rfq = $this->rfqService->createFromMrpShortages(
$businessId,
$run->shortage_analysis['shortages'],
__('supplychain::lang.rfq_from_mrp_run', ['code' => $run->run_code]),
$supplierIds
);
return $rfq->id;
}
/**
* Demand inputs for MRP — merges forecast qty into planned demand.
*
* @return array<int, float> variation_id => additional_qty
*/
public function getMrpDemandInputs(int $businessId, ?Carbon $month = null): array
{
try {
return $this->demandForecastService->getForecastByVariation($businessId, $month);
} catch (\Throwable $e) {
Log::warning('SupplyChain MRP demand inputs failed: '.$e->getMessage());
return [];
}
}
/**
* Apply demand forecast adjustments to MRP shortage analysis.
*
* @param array<string, mixed> $analysis
* @return array<string, mixed>
*/
public function enrichMrpAnalysis(int $businessId, array $analysis): array
{
$forecasts = $this->getMrpDemandInputs($businessId);
if (empty($forecasts) || empty($analysis['shortages'])) {
return $analysis;
}
foreach ($analysis['shortages'] as $idx => $shortage) {
$variationId = (int) ($shortage['variation_id'] ?? 0);
if ($variationId && isset($forecasts[$variationId])) {
$additional = $forecasts[$variationId];
$currentShortage = (float) ($shortage['shortage_qty'] ?? 0);
$analysis['shortages'][$idx]['forecast_demand_qty'] = $additional;
$analysis['shortages'][$idx]['shortage_qty'] = $currentShortage + $additional;
}
}
$analysis['supply_chain_forecast_applied'] = true;
return $analysis;
}
/**
* @param array<string, mixed> $shortage
*/
protected function resolveProductId(array $shortage): ?int
{
if (! empty($shortage['product_id'])) {
return (int) $shortage['product_id'];
}
if (! empty($shortage['variation_id'])) {
$productId = Variation::where('id', (int) $shortage['variation_id'])->value('product_id');
return $productId ? (int) $productId : null;
}
return null;
}
}