ultimatepos/Modules/IndustrialEngineering/Services/TraceabilityService.php

211 lines
7.7 KiB
PHP

<?php
namespace Modules\IndustrialEngineering\Services;
use Illuminate\Support\Facades\DB;
use Modules\IndustrialEngineering\Models\BuildGenealogyEvent;
use Modules\IndustrialEngineering\Models\CustomerInstalledAsset;
use Modules\IndustrialEngineering\Models\SerializedUnit;
use Modules\IndustrialEngineering\Models\SerializedUnitComponent;
use Modules\IndustrialEngineering\Models\SerialRegistry;
class TraceabilityService
{
public function __construct(
protected BomService $bomService
) {
}
public function createSerializedUnit(
int $businessId,
string $unitCode,
string $serialNumber,
?int $releasePackageId = null,
?int $workOrderId = null,
?int $lineRevisionId = null
): SerializedUnit {
return DB::transaction(function () use ($businessId, $unitCode, $serialNumber, $releasePackageId, $workOrderId, $lineRevisionId) {
$registry = SerialRegistry::create([
'business_id' => $businessId,
'serial_number' => $serialNumber,
'serial_type' => 'unit',
'status' => 'allocated',
'manufactured_at' => now()->toDateString(),
]);
return SerializedUnit::create([
'business_id' => $businessId,
'release_package_id' => $releasePackageId,
'work_order_id' => $workOrderId,
'line_revision_id' => $lineRevisionId,
'serial_registry_id' => $registry->id,
'unit_code' => $unitCode,
'serial_number' => $serialNumber,
'build_status' => 'in_progress',
'build_started_at' => now()->toDateString(),
]);
});
}
/**
* Record a component installed on a serialized unit (as-built genealogy).
*/
public function recordComponentInstallation(
SerializedUnit $unit,
array $componentData,
?int $performedBy = null
): SerializedUnitComponent {
return DB::transaction(function () use ($unit, $componentData, $performedBy) {
$serialRegistryId = null;
if (! empty($componentData['serial_number'])) {
$registry = SerialRegistry::firstOrCreate(
[
'business_id' => $unit->business_id,
'serial_number' => $componentData['serial_number'],
],
[
'item_master_id' => $componentData['item_master_id'] ?? null,
'serial_type' => 'component',
'status' => 'installed',
'manufactured_at' => $componentData['manufactured_at'] ?? now()->toDateString(),
'manufacturer_id' => $componentData['manufacturer_id'] ?? null,
]
);
$serialRegistryId = $registry->id;
}
$component = SerializedUnitComponent::create(array_merge([
'business_id' => $unit->business_id,
'serialized_unit_id' => $unit->id,
'installed_at' => now()->toDateString(),
], $componentData, ['serial_registry_id' => $serialRegistryId]));
BuildGenealogyEvent::create([
'business_id' => $unit->business_id,
'serialized_unit_id' => $unit->id,
'event_type' => 'component_installed',
'serialized_unit_component_id' => $component->id,
'performed_by' => $performedBy ?? auth()->id(),
'performed_at' => now(),
'event_data' => $componentData,
]);
return $component;
});
}
public function completeBuild(SerializedUnit $unit): SerializedUnit
{
$unit->update([
'build_status' => 'completed',
'build_completed_at' => now()->toDateString(),
'as_built_snapshot' => $this->buildAsBuiltSnapshot($unit),
]);
if ($unit->serialRegistry) {
$unit->serialRegistry->update(['status' => 'available']);
}
return $unit->fresh(['components.itemMaster', 'components.children']);
}
public function buildAsBuiltSnapshot(SerializedUnit $unit): array
{
$unit->load(['components.itemMaster', 'lineRevision', 'releasePackage']);
return [
'unit_id' => $unit->id,
'serial_number' => $unit->serial_number,
'line_revision_id' => $unit->line_revision_id,
'components' => $unit->components->map(fn ($c) => [
'id' => $c->id,
'position_code' => $c->position_code,
'label' => $c->component_label,
'serial_number' => $c->serial_number,
'brand' => $c->brand,
'manufacturer' => $c->manufacturer,
'model' => $c->model,
'warranty_expires_at' => $c->warranty_expires_at?->toDateString(),
])->values()->all(),
'snapshot_at' => now()->toIso8601String(),
];
}
/**
* Register customer installed asset from sold serialized unit.
*/
public function registerInstalledAsset(
SerializedUnit $unit,
int $customerId,
string $assetCode,
string $name,
?int $sellTransactionId = null,
?int $sellLineId = null,
?int $projectId = null
): CustomerInstalledAsset {
return DB::transaction(function () use ($unit, $customerId, $assetCode, $name, $sellTransactionId, $sellLineId, $projectId) {
$asBuilt = $this->buildAsBuiltSnapshot($unit);
$asset = CustomerInstalledAsset::create([
'business_id' => $unit->business_id,
'serialized_unit_id' => $unit->id,
'line_revision_id' => $unit->line_revision_id,
'customer_id' => $customerId,
'project_id' => $projectId,
'sell_transaction_id' => $sellTransactionId,
'sell_line_id' => $sellLineId,
'asset_code' => $assetCode,
'serial_number' => $unit->serial_number,
'name' => $name,
'installation_status' => 'pending',
'sold_at' => now()->toDateString(),
'as_sold_snapshot' => $asBuilt,
]);
$unit->update(['build_status' => 'shipped']);
return $asset;
});
}
/**
* Lookup by serial, asset code, or component serial.
*/
public function lookup(int $businessId, string $query): array
{
$asset = CustomerInstalledAsset::forBusiness($businessId)
->with(['customer', 'serializedUnit.components.itemMaster', 'lineRevision.lineTemplate', 'warranties'])
->where(function ($q) use ($query) {
$q->where('serial_number', $query)
->orWhere('asset_code', $query);
})
->first();
if ($asset) {
return ['type' => 'installed_asset', 'data' => $asset];
}
$component = SerializedUnitComponent::forBusiness($businessId)
->with(['serializedUnit', 'itemMaster', 'supplier'])
->where('serial_number', $query)
->orWhere('position_code', $query)
->first();
if ($component) {
return ['type' => 'component', 'data' => $component];
}
$unit = SerializedUnit::forBusiness($businessId)
->with(['components', 'lineRevision', 'installedAsset.customer'])
->where('serial_number', $query)
->orWhere('unit_code', $query)
->first();
if ($unit) {
return ['type' => 'serialized_unit', 'data' => $unit];
}
return ['type' => 'not_found', 'data' => null];
}
}