519 lines
24 KiB
PHP
Executable File
519 lines
24 KiB
PHP
Executable File
<?php
|
|
namespace App\Http\Controllers\PublicAccess;
|
|
|
|
use App\Models\Product;
|
|
use App\Models\ProductMedia;
|
|
use App\Models\ProductOffer;
|
|
use App\Services\Catalog\AmazonCreatorsApiService;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\View\View;
|
|
|
|
class AmazonReferralController
|
|
{
|
|
public function __construct(
|
|
private readonly AmazonCreatorsApiService $amazonCreatorsApiService,
|
|
) {
|
|
}
|
|
|
|
public function index(Request $request): View
|
|
{
|
|
$selectedBrand = Str::upper(trim((string) $request->query('brand', 'EPSON')));
|
|
|
|
$amazonOffers = $this->resolvePublicShowcaseOffers();
|
|
|
|
$exampleOffers = $amazonOffers
|
|
->filter(fn(ProductOffer $offer): bool => $this->matchesSelectedBrand($offer, $selectedBrand))
|
|
->take(8)
|
|
->map(function (ProductOffer $offer): array {
|
|
$payload = is_array($offer->meta['payload'] ?? null) ? $offer->meta['payload'] : [];
|
|
$galleryUrls = $this->resolveGalleryUrls($offer, $payload);
|
|
$amazonPrice = $offer->price_amount !== null ? (float) $offer->price_amount : $this->normalizeMoney(data_get($payload, 'price_amount'));
|
|
$reference = $this->buildReferenceLabel($payload);
|
|
|
|
return [
|
|
'title' => trim((string) ($offer->title ?: $offer->product?->name ?: 'Prodotto Amazon')),
|
|
'internal_code' => trim((string) ($offer->product?->internal_code ?? '')),
|
|
'brand' => trim((string) ($offer->product?->brand ?? data_get($payload, 'brand', ''))),
|
|
'asin' => trim((string) data_get($payload, 'asin', $offer->external_product_id)),
|
|
'reference_label' => $reference['label'],
|
|
'reference_value' => $reference['value'],
|
|
'price_amount' => $amazonPrice,
|
|
'price_compare_at' => $offer->price_compare_at !== null ? (float) $offer->price_compare_at : $this->normalizeMoney(data_get($payload, 'price_compare_at')),
|
|
'currency' => trim((string) ($offer->currency ?: data_get($payload, 'currency', 'EUR'))),
|
|
'availability' => trim((string) ($offer->availability ?: data_get($payload, 'availability', ''))),
|
|
'referral_url' => trim((string) ($offer->referral_url ?: data_get($payload, 'referral_url', ''))),
|
|
'detail_url' => trim((string) ($offer->external_url ?: data_get($payload, 'detail_url', ''))),
|
|
'image_url' => $galleryUrls[0] ?? '',
|
|
'image_urls' => $galleryUrls,
|
|
'search_query' => trim((string) data_get($payload, 'search_query', '')),
|
|
'sync_source' => trim((string) ($offer->meta['sync_source'] ?? 'amazon_creators_api')),
|
|
'synced_at' => trim((string) data_get($payload, 'synced_at', optional($offer->updated_at)->toIso8601String())),
|
|
'feature_points' => array_values(array_filter(array_map(static fn(mixed $value): string => trim((string) $value), (array) data_get($payload, 'feature_points', [])))),
|
|
];
|
|
})->values();
|
|
|
|
$brandCatalog = $this->buildBrandCatalog($amazonOffers, $selectedBrand);
|
|
$featuredComparisons = $this->buildFeaturedComparisons($selectedBrand);
|
|
|
|
$catalogStats = [
|
|
'offers_count' => $amazonOffers->count(),
|
|
'products_count' => $amazonOffers->pluck('product_id')->filter()->unique()->count(),
|
|
'brand_products_count' => count($brandCatalog),
|
|
];
|
|
|
|
return view('public.amazon-referral', [
|
|
'associateTag' => $this->associateTag(),
|
|
'defaultQuery' => trim((string) $request->query('q', '')),
|
|
'defaultAsin' => trim((string) $request->query('asin', '')),
|
|
'defaultUrl' => trim((string) $request->query('url', '')),
|
|
'selectedBrand' => $selectedBrand,
|
|
'exampleOffers' => $exampleOffers->all(),
|
|
'brandCatalog' => $brandCatalog,
|
|
'catalogStats' => $catalogStats,
|
|
'featuredComparisons' => $featuredComparisons,
|
|
'capabilityCards' => $this->capabilityCards(),
|
|
'telegramSetup' => $this->telegramSetup(),
|
|
'evaluationNotes' => $this->evaluationNotes(),
|
|
'kbChecklist' => $this->kbChecklist(),
|
|
]);
|
|
}
|
|
|
|
/** @return Collection<int, ProductOffer> */
|
|
private function resolvePublicShowcaseOffers(): Collection
|
|
{
|
|
return ProductOffer::query()
|
|
->with(['product', 'product.media'])
|
|
->where('source_type', 'amazon_creators_api')
|
|
->where('is_active', true)
|
|
->latest('last_seen_at')
|
|
->latest('id')
|
|
->limit(24)
|
|
->get()
|
|
->filter(fn(ProductOffer $offer): bool => $this->isPublicShowcaseOffer($offer))
|
|
->unique(fn(ProductOffer $offer): string => (string) ($offer->product_id ?: $offer->external_product_id ?: $offer->id))
|
|
->values();
|
|
}
|
|
|
|
private function matchesSelectedBrand(ProductOffer $offer, string $selectedBrand): bool
|
|
{
|
|
$brand = trim((string) ($offer->product?->brand ?: data_get($offer->meta, 'payload.brand', '')));
|
|
|
|
return $brand !== '' && Str::contains(Str::upper($brand), $selectedBrand);
|
|
}
|
|
|
|
private function isPublicShowcaseOffer(ProductOffer $offer): bool
|
|
{
|
|
$product = $offer->product;
|
|
if (! $product instanceof Product) {
|
|
return false;
|
|
}
|
|
|
|
$catalogMeta = is_array(data_get($product->meta, 'catalog')) ? data_get($product->meta, 'catalog') : [];
|
|
if (($catalogMeta['public_showcase'] ?? false) === true) {
|
|
return true;
|
|
}
|
|
|
|
return in_array(strtoupper((string) $offer->external_product_id), $this->featuredShowcaseAsins(), true);
|
|
}
|
|
|
|
/** @return array<int, string> */
|
|
private function featuredShowcaseAsins(): array
|
|
{
|
|
return [
|
|
'B0CPPKNRLJ',
|
|
'B0B9C3ZVHR',
|
|
'B087DFLF9S',
|
|
];
|
|
}
|
|
|
|
public function go(Request $request): RedirectResponse
|
|
{
|
|
$asin = $this->normalizeAsin((string) $request->query('asin', ''));
|
|
if ($asin !== null) {
|
|
return redirect()->away($this->appendAssociateTag('https://www.amazon.it/dp/' . rawurlencode($asin)));
|
|
}
|
|
|
|
$url = trim((string) $request->query('url', ''));
|
|
if ($url !== '' && filter_var($url, FILTER_VALIDATE_URL)) {
|
|
return redirect()->away($this->appendAssociateTag($this->normalizeAmazonUrl($url)));
|
|
}
|
|
|
|
$query = trim((string) $request->query('q', ''));
|
|
if ($query !== '') {
|
|
return redirect()->away($this->appendAssociateTag('https://www.amazon.it/s?k=' . rawurlencode($query)));
|
|
}
|
|
|
|
return redirect()->route('public.amazon.index');
|
|
}
|
|
|
|
private function associateTag(): string
|
|
{
|
|
$tag = trim((string) config('services.amazon.referral_tag', ''));
|
|
|
|
if ($tag === '') {
|
|
$tag = trim((string) config('catalog.amazon.associate_tag', 'netgescon-21'));
|
|
}
|
|
|
|
return $tag !== '' ? $tag : 'netgescon-21';
|
|
}
|
|
|
|
private function appendAssociateTag(string $url): string
|
|
{
|
|
$parts = parse_url($url);
|
|
if (! is_array($parts)) {
|
|
return $url;
|
|
}
|
|
|
|
$query = [];
|
|
parse_str((string) ($parts['query'] ?? ''), $query);
|
|
$query['tag'] = $this->associateTag();
|
|
|
|
$base = ($parts['scheme'] ?? 'https') . '://' . ($parts['host'] ?? 'www.amazon.it') . ($parts['path'] ?? '');
|
|
|
|
return $base . '?' . http_build_query($query);
|
|
}
|
|
|
|
private function normalizeAmazonUrl(string $url): string
|
|
{
|
|
$asin = $this->extractAsinFromUrl($url);
|
|
|
|
if ($asin !== null) {
|
|
return 'https://www.amazon.it/dp/' . rawurlencode($asin);
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
private function extractAsinFromUrl(string $url): ?string
|
|
{
|
|
if (preg_match('~/(?:dp|gp/product)/([A-Z0-9]{10})(?:[/?]|$)~i', $url, $matches)) {
|
|
return strtoupper($matches[1]);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function normalizeAsin(string $value): ?string
|
|
{
|
|
$value = strtoupper(trim($value));
|
|
$value = preg_replace('/[^A-Z0-9]/', '', $value) ?? '';
|
|
|
|
return preg_match('/^[A-Z0-9]{10}$/', $value) ? $value : null;
|
|
}
|
|
|
|
/** @return array<int, array<string, string>> */
|
|
private function capabilityCards(): array
|
|
{
|
|
return [
|
|
[
|
|
'title' => 'Catalogo interno Amazon',
|
|
'status' => 'Attivo',
|
|
'tone' => 'live',
|
|
'body' => 'La ricerca Amazon e il salvataggio candidati funzionano. Ora salviamo anche snapshot strutturati di prezzo, disponibilita, immagini e metadati di sync.',
|
|
],
|
|
[
|
|
'title' => 'Bulk import risultati',
|
|
'status' => 'Attivo',
|
|
'tone' => 'live',
|
|
'body' => 'Dalla UI fornitore possiamo importare in massa i risultati della query corrente per creare un mini catalogo pronto per prove e riuso interno.',
|
|
],
|
|
[
|
|
'title' => 'Canale Telegram offerte',
|
|
'status' => 'Pronto da collegare',
|
|
'tone' => 'next',
|
|
'body' => 'Abbiamo gia la configurazione base Telegram. Serve aggiungere il bot come admin al canale e usare token e chat id del canale per pubblicare le offerte.',
|
|
],
|
|
[
|
|
'title' => 'Carrello interno -> Amazon',
|
|
'status' => 'Da validare',
|
|
'tone' => 'review',
|
|
'body' => 'Possiamo costruire un carrello interno NetGescon, ma il passaggio affidabile di piu prodotti nel carrello Amazon non e garantito dalle API attuali. La prima strada solida resta aprire link referral dei prodotti selezionati.',
|
|
],
|
|
[
|
|
'title' => 'Fattura e provenienza venditore',
|
|
'status' => 'Da testare',
|
|
'tone' => 'review',
|
|
'body' => 'Dobbiamo verificare se Amazon Creators espone davvero venditore, paese e segnali utili per fattura e garanzia. Se i dati non arrivano via API serve una validazione supplementare.',
|
|
],
|
|
[
|
|
'title' => 'Mini sito brand selezionati',
|
|
'status' => 'Attivo',
|
|
'tone' => 'live',
|
|
'body' => 'La pagina pubblica puo gia funzionare come mini sito vetrina per brand scelti, con immagini, codici prodotto ufficiali e uscita finale verso Amazon tramite referral.',
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @return array<int, array<string, string>> */
|
|
private function telegramSetup(): array
|
|
{
|
|
return [
|
|
['label' => 'Bot Telegram', 'value' => 'Crea o usa un bot con BotFather e recupera il token API.'],
|
|
['label' => 'Canale', 'value' => 'Aggiungi il bot come amministratore del canale @nethomestorelettronica con permesso di pubblicare messaggi.'],
|
|
['label' => 'Chat ID', 'value' => 'Recupera il chat_id del canale, di solito un id numerico negativo o lo username del canale se accettato dal bot.'],
|
|
['label' => 'Config', 'value' => 'Valorizza TELEGRAM_OFFERS_BOT_TOKEN, TELEGRAM_OFFERS_CHAT_ID e TELEGRAM_OFFERS_CHANNEL_NAME in ambiente.'],
|
|
['label' => 'Pubblicazione', 'value' => 'Il primo invio puo partire con il publisher offerte; esiste gia un endpoint test Telegram autenticato per invii singoli.'],
|
|
];
|
|
}
|
|
|
|
/** @return array<int, array<string, string>> */
|
|
private function evaluationNotes(): array
|
|
{
|
|
return [
|
|
[
|
|
'title' => 'Messaggio privato a un iscritto del canale',
|
|
'body' => 'Non e possibile scrivere in privato a un iscritto solo perche segue il canale. Un bot Telegram puo scrivere a un utente solo se quell utente ha avviato il bot o ha gia aperto una chat diretta con lui.',
|
|
],
|
|
[
|
|
'title' => 'Ordinare prima venditori italiani, poi UE, poi Cina',
|
|
'body' => 'E fattibile solo se il dato del merchant o del paese arriva davvero dall API o da una fonte affidabile. Per ora non lo stiamo ancora persistendo, quindi va trattato come test aperto.',
|
|
],
|
|
[
|
|
'title' => 'Fattura e garanzia',
|
|
'body' => 'La semplice presenza del prodotto su Amazon non basta. Serve verificare se il listing espone indicatori affidabili su fattura e venditore, altrimenti il dato va raccolto con un controllo operativo separato.',
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @return array<int, array<string, mixed>> */
|
|
private function buildBrandCatalog(Collection $amazonOffers, string $selectedBrand): array
|
|
{
|
|
return $amazonOffers
|
|
->filter(fn(ProductOffer $offer): bool => $this->matchesSelectedBrand($offer, $selectedBrand))
|
|
->take(12)
|
|
->map(function (ProductOffer $offer): array {
|
|
$payload = is_array($offer->meta['payload'] ?? null) ? $offer->meta['payload'] : [];
|
|
$galleryUrls = $this->resolveGalleryUrls($offer, $payload);
|
|
$amazonPrice = $offer->price_amount !== null ? (float) $offer->price_amount : $this->normalizeMoney(data_get($payload, 'price_amount'));
|
|
$reference = $this->buildReferenceLabel($payload);
|
|
|
|
return [
|
|
'title' => trim((string) ($offer->title ?: $offer->product?->name ?: 'Prodotto Amazon')),
|
|
'internal_code' => trim((string) ($offer->product?->internal_code ?? '')),
|
|
'brand' => trim((string) ($offer->product?->brand ?: data_get($payload, 'brand', ''))),
|
|
'asin' => trim((string) data_get($payload, 'asin', $offer->external_product_id)),
|
|
'reference' => $reference,
|
|
'amazon_price' => $amazonPrice,
|
|
'currency' => trim((string) ($offer->currency ?: data_get($payload, 'currency', 'EUR'))),
|
|
'availability' => trim((string) ($offer->availability ?: data_get($payload, 'availability', ''))),
|
|
'referral_url' => trim((string) ($offer->referral_url ?: data_get($payload, 'referral_url', ''))),
|
|
'image_url' => $galleryUrls[0] ?? '',
|
|
'image_urls' => $galleryUrls,
|
|
'feature_points' => array_values(array_filter(array_map(static fn(mixed $value): string => trim((string) $value), (array) data_get($payload, 'feature_points', [])))),
|
|
];
|
|
})
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/** @return array<int, array<string, mixed>> */
|
|
private function buildFeaturedComparisons(string $selectedBrand): array
|
|
{
|
|
$examples = [
|
|
[
|
|
'asin' => 'B0CPPKNRLJ',
|
|
'brand' => 'EPSON',
|
|
'note' => 'Scheda vetrina ufficiale EPSON con codice produttore, immagini e link referral verso Amazon.',
|
|
],
|
|
[
|
|
'asin' => 'B0B9C3ZVHR',
|
|
'brand' => 'SAMSUNG',
|
|
'note' => 'Prodotto Samsung da negozio riallineato con metadata Amazon completi: titolo, immagini, codice produttore ed EAN.',
|
|
],
|
|
];
|
|
|
|
return collect($examples)
|
|
->filter(static fn(array $item): bool => Str::upper((string) ($item['brand'] ?? '')) === $selectedBrand)
|
|
->map(function (array $item): array {
|
|
$amazon = $this->fetchAmazonShowcaseItem((string) ($item['asin'] ?? ''));
|
|
$amazonPrice = $amazon['price_amount'] ?? null;
|
|
|
|
return [
|
|
'asin' => (string) ($item['asin'] ?? ''),
|
|
'brand' => (string) ($item['brand'] ?? ''),
|
|
'title' => (string) ($amazon['title'] ?? 'Prodotto selezionato'),
|
|
'reference' => $this->buildReferenceLabel($amazon),
|
|
'detail_url' => (string) ($amazon['detail_url'] ?? ''),
|
|
'referral_url' => (string) ($amazon['referral_url'] ?? ''),
|
|
'image_url' => (string) ($amazon['image_url'] ?? ''),
|
|
'image_urls' => (array) ($amazon['image_urls'] ?? []),
|
|
'currency' => (string) ($amazon['currency'] ?? 'EUR'),
|
|
'amazon_public_price' => $amazonPrice,
|
|
'availability' => (string) ($amazon['availability'] ?? ''),
|
|
'note' => (string) ($item['note'] ?? ''),
|
|
];
|
|
})
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
private function fetchAmazonShowcaseItem(string $asin): array
|
|
{
|
|
$asin = strtoupper(trim($asin));
|
|
if ($asin === '' || ! $this->amazonCreatorsApiService->isConfigured()) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
$payload = $this->amazonCreatorsApiService->getItems([$asin]);
|
|
} catch (\Throwable) {
|
|
return [];
|
|
}
|
|
|
|
$item = data_get($payload, 'itemsResult.items.0') ?? data_get($payload, 'getItemsResult.items.0') ?? data_get($payload, 'ItemsResult.Items.0');
|
|
|
|
if (! is_array($item)) {
|
|
return [];
|
|
}
|
|
|
|
$detailUrl = trim((string) data_get($item, 'detailPageURL', ''));
|
|
$imageUrls = array_values(array_filter(array_map(
|
|
static fn(mixed $value): string => trim((string) (is_array($value) ? data_get($value, 'large.url', data_get($value, 'url', '')) : $value)),
|
|
array_merge(
|
|
array_filter([(string) data_get($item, 'images.primary.large.url', '')]),
|
|
(array) data_get($item, 'images.variants', []),
|
|
(array) data_get($item, 'image_urls', [])
|
|
)
|
|
)));
|
|
|
|
return [
|
|
'title' => trim((string) data_get($item, 'itemInfo.title.displayValue', data_get($item, 'title', 'Prodotto Amazon'))),
|
|
'detail_url' => $detailUrl,
|
|
'referral_url' => $detailUrl !== '' ? $this->appendAssociateTag($detailUrl) : '',
|
|
'image_url' => $imageUrls[0] ?? '',
|
|
'image_urls' => $imageUrls,
|
|
'manufacturer_code' => trim((string) data_get($item, 'itemInfo.manufactureInfo.itemPartNumber.displayValue', '')),
|
|
'ean' => trim((string) data_get($item, 'itemInfo.externalIds.eans.displayValues.0', '')),
|
|
'upc' => trim((string) data_get($item, 'itemInfo.externalIds.upcs.displayValues.0', '')),
|
|
'price_amount' => $this->normalizeMoney(data_get($item, 'offersV2.listings.0.price.money.amount', data_get($item, 'offers.listings.0.price.amount', data_get($item, 'price.amount')))),
|
|
'currency' => trim((string) data_get($item, 'offersV2.listings.0.price.money.currency', data_get($item, 'offers.listings.0.price.currency', data_get($item, 'price.currency', 'EUR')))),
|
|
'availability' => trim((string) data_get($item, 'offers.listings.0.availability.message', '')),
|
|
];
|
|
}
|
|
|
|
/** @param array<string, mixed> $payload
|
|
* @return array{label:string,value:string}
|
|
*/
|
|
private function buildReferenceLabel(array $payload): array
|
|
{
|
|
$manufacturerCode = trim((string) data_get($payload, 'manufacturer_code', ''));
|
|
if ($manufacturerCode !== '') {
|
|
return ['label' => 'Codice produttore', 'value' => $manufacturerCode];
|
|
}
|
|
|
|
$ean = trim((string) data_get($payload, 'ean', ''));
|
|
if ($ean !== '') {
|
|
return ['label' => 'EAN', 'value' => $ean];
|
|
}
|
|
|
|
$upc = trim((string) data_get($payload, 'upc', ''));
|
|
if ($upc !== '') {
|
|
return ['label' => 'UPC', 'value' => $upc];
|
|
}
|
|
|
|
return ['label' => '', 'value' => ''];
|
|
}
|
|
|
|
private function resolvePreferredSupplierOffer(?Product $product): ?ProductOffer
|
|
{
|
|
if (! $product instanceof Product) {
|
|
return null;
|
|
}
|
|
|
|
return ProductOffer::query()
|
|
->where('product_id', (int) $product->id)
|
|
->where('source_type', 'internal_supplier')
|
|
->whereNotNull('price_amount')
|
|
->orderByDesc('last_seen_at')
|
|
->orderByDesc('id')
|
|
->first();
|
|
}
|
|
|
|
/** @param array<string, mixed> $payload
|
|
* @return array<int, string>
|
|
*/
|
|
private function resolveGalleryUrls(ProductOffer $offer, array $payload): array
|
|
{
|
|
$urls = array_values(array_filter(array_map(
|
|
static fn(mixed $value): string => trim((string) $value),
|
|
(array) data_get($payload, 'image_urls', [])
|
|
), static fn(string $value): bool => $value !== ''));
|
|
|
|
if ($urls !== []) {
|
|
return array_values(array_unique($urls));
|
|
}
|
|
|
|
$product = $offer->product;
|
|
if (! $product instanceof Product) {
|
|
return [];
|
|
}
|
|
|
|
$mediaUrls = $product->media
|
|
->sortBy('sort_order')
|
|
->map(function (ProductMedia $media): ?string {
|
|
$sourceUrl = trim((string) data_get($media->meta, 'source_url', ''));
|
|
if ($sourceUrl !== '') {
|
|
return $sourceUrl;
|
|
}
|
|
|
|
if (($media->disk ?? '') === 'public' && trim((string) ($media->path ?? '')) !== '') {
|
|
return asset('storage/' . ltrim((string) $media->path, '/'));
|
|
}
|
|
|
|
return null;
|
|
})
|
|
->filter(static fn(?string $value): bool => is_string($value) && $value !== '')
|
|
->values()
|
|
->all();
|
|
|
|
return array_values(array_unique($mediaUrls));
|
|
}
|
|
|
|
/** @return array<int, array<string, string>> */
|
|
private function kbChecklist(): array
|
|
{
|
|
return [
|
|
['title' => 'Import Amazon', 'body' => 'Cercare da catalogo fornitore, salvare il singolo candidato o tutti i risultati, poi verificare che prezzo, gallery e referral siano finiti nel catalogo interno.'],
|
|
['title' => 'Scheda pubblica prodotto', 'body' => 'Esporre immagini, prezzo Amazon, codice produttore o EAN/UPC e pulsante referral senza mostrare il prezzo interno del gestionale.'],
|
|
['title' => 'Mini sito brand', 'body' => 'Usare public/amazon come vetrina per brand selezionati come EPSON, mostrando prima il catalogo interno e poi il salto finale su Amazon.'],
|
|
['title' => 'Telegram', 'body' => 'Configurare token e chat del canale, provare il dry-run e poi pubblicare una offerta reale con il comando offers:publish-telegram.'],
|
|
];
|
|
}
|
|
|
|
private function normalizeMoney(mixed $value): ?float
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
if (is_string($value)) {
|
|
$value = str_replace('.', '', trim($value));
|
|
$value = str_replace(',', '.', $value);
|
|
}
|
|
|
|
return is_numeric($value) ? round((float) $value, 2) : null;
|
|
}
|
|
|
|
private function calculateDelta(?float $supplierPrice, ?float $amazonPrice): ?float
|
|
{
|
|
if ($supplierPrice === null || $amazonPrice === null) {
|
|
return null;
|
|
}
|
|
|
|
return round($supplierPrice - $amazonPrice, 2);
|
|
}
|
|
|
|
private function calculateDeltaPercent(?float $supplierPrice, ?float $amazonPrice): ?float
|
|
{
|
|
if ($supplierPrice === null || $amazonPrice === null || $supplierPrice == 0.0) {
|
|
return null;
|
|
}
|
|
|
|
return round((($supplierPrice - $amazonPrice) / $supplierPrice) * 100, 2);
|
|
}
|
|
}
|