256 lines
13 KiB
PHP
Executable File
256 lines
13 KiB
PHP
Executable File
<?php
|
|
namespace App\Services\Catalog;
|
|
|
|
use App\Models\Fornitore;
|
|
use App\Models\Product;
|
|
use App\Models\ProductIdentifier;
|
|
use App\Models\ProductOffer;
|
|
use App\Models\TicketIntervento;
|
|
use App\Modules\Contabilita\Models\FatturaFornitoreRiga;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
class ProductHubViewService
|
|
{
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function buildRows(Fornitore $fornitore, ?int $stabileId = null, int $limit = 20): array
|
|
{
|
|
if (! Schema::hasTable('products')) {
|
|
return [];
|
|
}
|
|
|
|
$supplierIds = $fornitore->catalogScopeSupplierIds();
|
|
|
|
$products = Product::query()
|
|
->withCount(['identifiers', 'serials', 'media'])
|
|
->with([
|
|
'identifiers' => function ($identifierQuery) use ($supplierIds): void {
|
|
$identifierQuery
|
|
->whereNull('fornitore_id')
|
|
->orWhereIn('fornitore_id', $supplierIds);
|
|
},
|
|
'offers' => function ($offerQuery) use ($supplierIds): void {
|
|
$offerQuery
|
|
->where('is_active', true)
|
|
->where(function ($builder) use ($supplierIds): void {
|
|
$builder->whereNull('fornitore_id')
|
|
->orWhereIn('fornitore_id', $supplierIds);
|
|
})
|
|
->orderByDesc('is_internal')
|
|
->orderBy('price_amount');
|
|
},
|
|
])
|
|
->where('is_active', true)
|
|
->where(function ($query) use ($supplierIds): void {
|
|
$query->whereIn('default_fornitore_id', $supplierIds)
|
|
->orWhereHas('offers', fn($offerQuery) => $offerQuery->whereIn('fornitore_id', $supplierIds));
|
|
})
|
|
->limit(max($limit * 3, 30))
|
|
->get(['id', 'default_fornitore_id', 'internal_code', 'name', 'brand', 'model', 'color_label', 'track_serials', 'meta']);
|
|
|
|
if ($products->isEmpty()) {
|
|
return [];
|
|
}
|
|
|
|
$productIds = $products->pluck('id')->map(fn($value): int => (int) $value)->all();
|
|
$usageByProduct = $this->buildUsageSummary($fornitore, $productIds, $stabileId);
|
|
$pricingByProduct = $this->buildPurchaseSummary($supplierIds, $productIds);
|
|
|
|
return $products
|
|
->map(function (Product $product) use ($fornitore, $supplierIds, $usageByProduct, $pricingByProduct): array {
|
|
$offers = $product->offers ?? collect();
|
|
$internalOffer = $offers->first(fn(ProductOffer $offer): bool => (bool) ($offer->is_internal ?? false));
|
|
$bestExternal = $offers
|
|
->filter(fn(ProductOffer $offer): bool => ! (bool) ($offer->is_internal ?? false) && filled($offer->price_amount))
|
|
->sortBy('price_amount')
|
|
->first();
|
|
$amazonOffer = $offers->first(fn(ProductOffer $offer): bool => in_array((string) ($offer->source_type ?? ''), ['amazon_referral', 'amazon_creators_api'], true));
|
|
$identifier = $product->identifiers
|
|
->first(fn(ProductIdentifier $item): bool => in_array((int) ($item->fornitore_id ?? 0), $supplierIds, true)) ?? $product->identifiers->first();
|
|
$meta = is_array($product->meta ?? null) ? $product->meta : [];
|
|
$catalogMeta = is_array($meta['catalog'] ?? null) ? $meta['catalog'] : [];
|
|
$usage = $usageByProduct[(int) $product->id] ?? $this->emptyUsageSummary();
|
|
$purchaseSummary = $pricingByProduct[(int) $product->id] ?? $this->emptyPurchaseSummary();
|
|
|
|
return [
|
|
'id' => (int) $product->id,
|
|
'internal_code' => (string) ($product->internal_code ?? ''),
|
|
'name' => (string) ($product->name ?? ''),
|
|
'brand' => (string) ($product->brand ?? ''),
|
|
'model' => (string) ($product->model ?? ''),
|
|
'color_label' => (string) ($product->color_label ?? ''),
|
|
'track_serials' => (bool) $product->track_serials,
|
|
'identifier' => (string) ($identifier->code_value ?? ''),
|
|
'identifiers_count' => (int) ($product->identifiers_count ?? 0),
|
|
'serials_count' => (int) ($product->serials_count ?? 0),
|
|
'media_count' => (int) ($product->media_count ?? 0),
|
|
'source' => (string) (($meta['source'] ?? 'manual') ?: 'manual'),
|
|
'publication_mode' => (string) (($catalogMeta['publication_mode'] ?? '') ?: ''),
|
|
'public_reference' => (string) (($catalogMeta['public_reference'] ?? '') ?: ($product->internal_code ?? '')),
|
|
'private_links' => count(array_filter(is_array($catalogMeta['private_source_links'] ?? null) ? $catalogMeta['private_source_links'] : [], fn($value) => filled($value))),
|
|
'categories' => (string) (($catalogMeta['categories'] ?? '') ?: ''),
|
|
'stock_quantity' => is_numeric($meta['stock']['quantity'] ?? null) ? (int) $meta['stock']['quantity'] : null,
|
|
'price_wholesale' => is_numeric($meta['pricing']['wholesale'] ?? null) ? (float) $meta['pricing']['wholesale'] : null,
|
|
'offers_count' => $offers->count(),
|
|
'own_offer_price' => $internalOffer && filled($internalOffer->price_amount) ? (float) $internalOffer->price_amount : null,
|
|
'best_competitor_price' => $bestExternal && filled($bestExternal->price_amount) ? (float) $bestExternal->price_amount : null,
|
|
'best_competitor_source' => $bestExternal ? (string) ($bestExternal->source_name ?? $bestExternal->source_type ?? '') : '',
|
|
'amazon_referral_url' => $amazonOffer ? (string) ($amazonOffer->referral_url ?? $amazonOffer->external_url ?? '') : '',
|
|
'usage_count' => (int) ($usage['usage_count'] ?? 0),
|
|
'usage_quantity' => (float) ($usage['usage_quantity'] ?? 0),
|
|
'last_used_at' => (string) ($usage['last_used_at'] ?? ''),
|
|
'stable_usage_count' => (int) ($usage['stable_usage_count'] ?? 0),
|
|
'stable_usage_quantity' => (float) ($usage['stable_usage_quantity'] ?? 0),
|
|
'stable_last_used_at' => (string) ($usage['stable_last_used_at'] ?? ''),
|
|
'purchase_lines_count' => (int) ($purchaseSummary['purchase_lines_count'] ?? 0),
|
|
'last_purchase_total' => $purchaseSummary['last_purchase_total'] ?? null,
|
|
'last_purchase_date' => (string) ($purchaseSummary['last_purchase_date'] ?? ''),
|
|
'purchase_history_labels' => (array) ($purchaseSummary['purchase_history_labels'] ?? []),
|
|
'sort_score' => ((int) ($usage['stable_usage_count'] ?? 0) * 1000)
|
|
+ ((int) round((float) ($usage['stable_usage_quantity'] ?? 0) * 100))
|
|
+ ((int) ($usage['usage_count'] ?? 0) * 10),
|
|
];
|
|
})
|
|
->sort(function (array $left, array $right): int {
|
|
if (($left['sort_score'] ?? 0) === ($right['sort_score'] ?? 0)) {
|
|
return strcmp((string) ($left['name'] ?? ''), (string) ($right['name'] ?? ''));
|
|
}
|
|
|
|
return (int) ($right['sort_score'] ?? 0) <=> (int) ($left['sort_score'] ?? 0);
|
|
})
|
|
->take($limit)
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param array<int, int> $productIds
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildUsageSummary(Fornitore $fornitore, array $productIds, ?int $stabileId): array
|
|
{
|
|
if ($productIds === [] || ! Schema::hasTable('ticket_interventi')) {
|
|
return [];
|
|
}
|
|
|
|
$interventi = TicketIntervento::query()
|
|
->with(['ticket:id,stabile_id'])
|
|
->where('fornitore_id', (int) $fornitore->id)
|
|
->whereNotNull('materiali_utilizzati')
|
|
->latest('updated_at')
|
|
->limit(250)
|
|
->get(['id', 'ticket_id', 'materiali_utilizzati', 'updated_at']);
|
|
|
|
$summary = [];
|
|
|
|
foreach ($interventi as $intervento) {
|
|
$currentStabileId = (int) ($intervento->ticket?->stabile_id ?? 0);
|
|
|
|
foreach ((array) ($intervento->materiali_utilizzati ?? []) as $item) {
|
|
if (! is_array($item)) {
|
|
continue;
|
|
}
|
|
|
|
$productId = isset($item['product_id']) && is_numeric($item['product_id']) ? (int) $item['product_id'] : 0;
|
|
if ($productId <= 0 || ! in_array($productId, $productIds, true)) {
|
|
continue;
|
|
}
|
|
|
|
$qty = isset($item['qty']) && is_numeric($item['qty']) ? (float) $item['qty'] : 1.0;
|
|
|
|
if (! isset($summary[$productId])) {
|
|
$summary[$productId] = $this->emptyUsageSummary();
|
|
}
|
|
|
|
$summary[$productId]['usage_count']++;
|
|
$summary[$productId]['usage_quantity'] += $qty;
|
|
$summary[$productId]['last_used_at'] = $summary[$productId]['last_used_at'] !== ''
|
|
? $summary[$productId]['last_used_at']
|
|
: (optional($intervento->updated_at)->format('d/m/Y H:i') ?: '');
|
|
|
|
if ($stabileId !== null && $stabileId > 0 && $currentStabileId === $stabileId) {
|
|
$summary[$productId]['stable_usage_count']++;
|
|
$summary[$productId]['stable_usage_quantity'] += $qty;
|
|
$summary[$productId]['stable_last_used_at'] = $summary[$productId]['stable_last_used_at'] !== ''
|
|
? $summary[$productId]['stable_last_used_at']
|
|
: (optional($intervento->updated_at)->format('d/m/Y H:i') ?: '');
|
|
}
|
|
}
|
|
}
|
|
|
|
return $summary;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, int> $productIds
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildPurchaseSummary(array $supplierIds, array $productIds): array
|
|
{
|
|
if ($productIds === [] || ! Schema::hasTable('contabilita_fatture_fornitori_righe') || ! Schema::hasTable('contabilita_fatture_fornitori')) {
|
|
return [];
|
|
}
|
|
|
|
$rows = FatturaFornitoreRiga::query()
|
|
->with(['fattura:id,fornitore_id,data_documento,numero_documento'])
|
|
->whereIn('product_id', $productIds)
|
|
->whereHas('fattura', fn($query) => $query->whereIn('fornitore_id', $supplierIds))
|
|
->orderByDesc('id')
|
|
->limit(300)
|
|
->get(['id', 'fattura_id', 'product_id', 'descrizione', 'imponibile_euro', 'totale_euro']);
|
|
|
|
$summary = [];
|
|
|
|
foreach ($rows->groupBy('product_id') as $productId => $group) {
|
|
$labels = $group
|
|
->take(3)
|
|
->map(function (FatturaFornitoreRiga $row): string {
|
|
$date = $row->fattura?->data_documento?->format('d/m/Y') ?: 'data n/d';
|
|
$number = trim((string) ($row->fattura?->numero_documento ?? 'riga #' . $row->id));
|
|
$total = is_numeric($row->totale_euro) ? number_format((float) $row->totale_euro, 2, ',', '.') . ' EUR' : 'totale n/d';
|
|
|
|
return $date . ' · ' . $number . ' · ' . $total;
|
|
})
|
|
->values()
|
|
->all();
|
|
|
|
/** @var FatturaFornitoreRiga|null $latest */
|
|
$latest = $group->first();
|
|
|
|
$summary[(int) $productId] = [
|
|
'purchase_lines_count' => $group->count(),
|
|
'last_purchase_total' => $latest && is_numeric($latest->totale_euro) ? (float) $latest->totale_euro : null,
|
|
'last_purchase_date' => $latest?->fattura?->data_documento?->format('d/m/Y') ?: '',
|
|
'purchase_history_labels' => $labels,
|
|
];
|
|
}
|
|
|
|
return $summary;
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
private function emptyUsageSummary(): array
|
|
{
|
|
return [
|
|
'usage_count' => 0,
|
|
'usage_quantity' => 0.0,
|
|
'last_used_at' => '',
|
|
'stable_usage_count' => 0,
|
|
'stable_usage_quantity' => 0.0,
|
|
'stable_last_used_at' => '',
|
|
];
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
private function emptyPurchaseSummary(): array
|
|
{
|
|
return [
|
|
'purchase_lines_count' => 0,
|
|
'last_purchase_total' => null,
|
|
'last_purchase_date' => '',
|
|
'purchase_history_labels' => [],
|
|
];
|
|
}
|
|
}
|