netgescon-day0/docs/ops/amazon_creators_pms_integration_pack.md

12 KiB

📦 PACCHETTO COMPLETO INTEGRAZIONE AMAZON API & VETRINA PRODOTTI PER PMS

Documento di Specifica e Codice Pronto all'Uso per Agent / Sviluppatori
Risoluzione Errore HTTP 403/404 + Configurazione .env + Service PHP + Pagina Pubblica Vetrina (Carrello & Referral)


📌 1. PERCHÉ RICEVEVI ERRORE HTTP 403 / 404 (DIAGNOSI TECNICA)

  1. Confusione tra PA-API v5 e Creators API v3:

    • Se chiami la Product Advertising API v5 (webservices.amazon.it) inviando un Authorization: Bearer <token> tramite OAuth, Amazon RIFIUTA la richiesta con HTTP 403! PA-API v5 richiede obbligatoriamente la firma AWS Signature Version 4 (AWS4-HMAC-SHA256) con la coppia Access Key + Secret Key.
    • Se usate invece la nuova Amazon Creators API (v3.2) via OAuth client_credentials, l'endpoint base è https://creators-api.amazon.com/v3/ ed il campo scope deve essere creatorsapi::default.
  2. Header x-marketplace Obbligatorio:

    • Tutte le chiamate ad Amazon Creators API devono includere l'header HTTP: x-marketplace: www.amazon.it

⚙️ 2. AMBIENTE .env PRONTO DA COPIARE

Inserisci questo blocco nel file .env del nuovo PMS:

# ==============================================================================
# AMAZON INTEGRATION (CREATORS API & PA-API v5)
# ==============================================================================
AMAZON_REFERRAL_TAG="tuotag-21"
AMAZON_MARKETPLACE="www.amazon.it"

# --- OPZIONE A: Amazon Creators API (OAuth 2.0 Client Credentials) ---
AMAZON_CREATORS_ENABLED=true
AMAZON_CREATORS_TOKEN_URL="https://api.amazon.com/auth/o2/token"
AMAZON_CREATORS_API_BASE_URL="https://creators-api.amazon.com"
AMAZON_CREATORS_CREDENTIAL_ID="amzn1.application-oa2-client.YOUR_CLIENT_ID"
AMAZON_CREATORS_CREDENTIAL_SECRET="YOUR_CLIENT_SECRET"
AMAZON_CREATORS_CREDENTIAL_VERSION="v3.2"
AMAZON_CREATORS_SEARCH_PATH="v3/searchItems"
AMAZON_CREATORS_GET_ITEMS_PATH="v3/getItems"
AMAZON_CREATORS_TIMEOUT=20

# --- OPZIONE B: Amazon PA-API 5.0 (AWS SigV4 Header Signing) ---
AMAZON_PAAPI5_HOST="webservices.amazon.it"
AMAZON_PAAPI5_REGION="eu-west-1"
AMAZON_PAAPI5_SEARCH_URL="https://webservices.amazon.it/paapi5/searchitems"
AMAZON_PAAPI5_GET_ITEMS_URL="https://webservices.amazon.it/paapi5/getitems"

💻 3. CODICE PHP SERVICE (AmazonCreatorsApiService.php)

Salva in app/Services/Catalog/AmazonCreatorsApiService.php:

<?php
namespace App\Services\Catalog;

use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use RuntimeException;

class AmazonCreatorsApiService
{
    public function isConfigured(): bool
    {
        return (bool) config('services.amazon.creators_enabled', false)
            && $this->credentialId() !== ''
            && $this->credentialSecret() !== ''
            && $this->tokenUrl() !== ''
            && $this->apiBaseUrl() !== '';
    }

    public function getMarketplace(): string
    {
        return trim((string) config('services.amazon.marketplace', 'www.amazon.it')) ?: 'www.amazon.it';
    }

    public function getAssociateTag(): ?string
    {
        $tag = trim((string) config('services.amazon.referral_tag', ''));
        return $tag !== '' ? $tag : null;
    }

    public function searchItems(string $keywords, array $parameters = []): array
    {
        $keywords = trim($keywords);
        if ($keywords === '') {
            throw new RuntimeException('Keywords Amazon vuote.');
        }

        return $this->requestOperation('searchItems', array_filter(array_replace([
            'keywords'   => $keywords,
            'partnerTag' => $this->getAssociateTag(),
        ], $parameters), static fn(mixed $value): bool => $value !== null && $value !== ''));
    }

    public function getItems(array $identifiers, array $parameters = []): array
    {
        $identifiers = array_values(array_filter(array_map(
            static fn(mixed $value): string => trim((string) $value),
            $identifiers
        ), static fn(string $value): bool => $value !== ''));

        if ($identifiers === []) {
            throw new RuntimeException('Specificare almeno un ASIN Amazon.');
        }

        return $this->requestOperation('getItems', array_filter(array_replace([
            'itemIds'    => $identifiers,
            'partnerTag' => $this->getAssociateTag(),
            'resources'  => [
                'images.primary.large',
                'itemInfo.title',
                'offersV2.listings.price'
            ]
        ], $parameters), static fn(mixed $value): bool => $value !== null && $value !== ''));
    }

    private function credentialId(): string
    {
        return trim((string) config('services.amazon.creators_credential_id', ''));
    }

    private function credentialSecret(): string
    {
        return trim((string) config('services.amazon.creators_credential_secret', ''));
    }

    private function tokenUrl(): string
    {
        return trim((string) config('services.amazon.creators_token_url', ''));
    }

    private function apiBaseUrl(): string
    {
        return rtrim(trim((string) config('services.amazon.creators_api_base_url', '')), '/');
    }

    private function requestOperation(string $operation, array $payload): array
    {
        if (! $this->isConfigured()) {
            throw new RuntimeException('Amazon Creators API non configurata in ambiente.');
        }

        $path = trim((string) Arr::get(config('services.amazon.creators_paths', []), $operation, ''), '/');

        $response = Http::timeout(20)
            ->acceptJson()
            ->asJson()
            ->withToken($this->getAccessToken())
            ->withHeaders([
                'x-marketplace' => $this->getMarketplace(),
            ])
            ->post($this->apiBaseUrl() . '/' . $path, $payload);

        if (! $response->successful()) {
            throw new RuntimeException('Creators API error HTTP ' . $response->status() . ': ' . $response->body());
        }

        return $response->json() ?: [];
    }

    private function getAccessToken(): string
    {
        $cacheKey = 'amazon-creators-token:' . sha1($this->credentialId());
        $cached   = Cache::get($cacheKey);
        if (is_string($cached) && trim($cached) !== '') {
            return $cached;
        }

        $response = Http::timeout(20)->asJson()->post($this->tokenUrl(), [
            'grant_type'    => 'client_credentials',
            'client_id'     => $this->credentialId(),
            'client_secret' => $this->credentialSecret(),
            'scope'         => 'creatorsapi::default',
        ]);

        if (! $response->successful()) {
            throw new RuntimeException('Token OAuth Amazon fallito: ' . $response->body());
        }

        $token = (string) ($response->json('access_token') ?? '');
        $expires = (int) ($response->json('expires_in') ?? 3600);
        Cache::put($cacheKey, $token, now()->addSeconds($expires - 60));

        return $token;
    }
}

🎨 4. PAGINA PUBBLICA VETRINA HTML/BLADE CON CARRELLO E REFERRAL

Salva in resources/views/public/amazon-referral.blade.php:

<!DOCTYPE html>
<html lang="it">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Catalogo Prodotti Amazon & Referral PMS</title>
    <style>
        :root {
            --bg: #f8fafc;
            --card: #ffffff;
            --ink: #0f172a;
            --muted: #64748b;
            --primary: #d97706;
            --primary-dark: #b45309;
            --border: #e2e8f0;
            --radius: 16px;
        }
        body { margin: 0; font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--ink); padding: 24px; }
        .container { max-width: 1100px; margin: 0 auto; }
        .header { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 32px; margin-bottom: 24px; }
        .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; }
        .card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; display: flex; flex-direction: column; justify-content: space-between; }
        .card img { width: 100%; height: 200px; object-fit: contain; border-radius: 8px; margin-bottom: 12px; }
        .card h3 { font-size: 1.1rem; margin: 0 0 8px; color: var(--ink); }
        .card .price { font-size: 1.4rem; font-weight: bold; color: var(--primary); margin: 8px 0; }
        .btn { display: inline-flex; align-items: center; justify-content: center; padding: 12px 20px; border-radius: 10px; font-weight: bold; text-decoration: none; cursor: pointer; border: none; }
        .btn-primary { background: var(--primary); color: white; }
        .btn-primary:hover { background: var(--primary-dark); }
        .btn-secondary { background: #f1f5f9; color: #334155; border: 1px solid var(--border); margin-left: 8px; }
        .cart-box { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; margin-top: 32px; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>🛍️ Catalogo Prodotti & Vetrina Amazon</h1>
            <p>Seleziona i prodotti per la tua struttura ricettiva ed acquista direttamente su Amazon con referral dedicato.</p>
        </div>

        <div class="grid">
            <!-- Esempio Prodotto 1 -->
            <div class="card">
                <div>
                    <img src="https://m.media-amazon.com/images/I/71TPdaE6y1L._AC_SL1500_.jpg" alt="Epson Stampante">
                    <h3>EPSON EcoTank ET-2810</h3>
                    <p>Stampante multifunzione a serbatoi ricaricabili ideale per reception.</p>
                    <div class="price">189.99 €</div>
                </div>
                <div>
                    <a href="https://www.amazon.it/dp/B099KDBV34?tag={{ $associateTag ?? 'tuotag-21' }}" target="_blank" class="btn btn-primary">Vedi su Amazon</a>
                    <button class="btn btn-secondary cart-add" data-title="EPSON EcoTank ET-2810" data-url="https://www.amazon.it/dp/B099KDBV34?tag={{ $associateTag ?? 'tuotag-21' }}">+ Carrello</button>
                </div>
            </div>
        </div>

        <!-- Carrello Interno Integrato -->
        <div class="cart-box">
            <h2>🛒 Carrello Prodotti Selezionati (<span id="cart-count">0</span>)</h2>
            <div id="cart-items" style="margin: 16px 0;">Nessun prodotto selezionato.</div>
            <button id="cart-open" class="btn btn-primary">Apri tutti su Amazon</button>
            <button id="cart-clear" class="btn btn-secondary">Svuota Carrello</button>
        </div>
    </div>

    <script>
        (() => {
            const KEY = 'pms_amazon_cart';
            const getCart = () => JSON.parse(localStorage.getItem(KEY) || '[]');
            const saveCart = (items) => localStorage.setItem(KEY, JSON.stringify(items));
            const render = () => {
                const items = getCart();
                document.getElementById('cart-count').textContent = items.length;
                const container = document.getElementById('cart-items');
                if (items.length === 0) {
                    container.innerHTML = '<p>Nessun prodotto selezionato.</p>';
                    return;
                }
                container.innerHTML = items.map(i => `<div style="padding:8px 0; border-bottom:1px solid #eee;"><strong>${i.title}</strong></div>`).join('');
            };

            document.querySelectorAll('.cart-add').forEach(btn => {
                btn.addEventListener('click', () => {
                    const items = getCart();
                    const url = btn.dataset.url;
                    if (!items.some(i => i.url === url)) {
                        items.push({ title: btn.dataset.title, url });
                        saveCart(items);
                        render();
                    }
                });
            });

            document.getElementById('cart-open')?.addEventListener('click', () => {
                getCart().forEach(i => window.open(i.url, '_blank'));
            });

            document.getElementById('cart-clear')?.addEventListener('click', () => {
                saveCart([]);
                render();
            });

            render();
        })();
    </script>
</body>
</html>

🚀 5. TEST & VERIFICA IMMEDIATA

  1. Salva il file .env con le variabili riportate al punto 2.
  2. Inserisci il Service PHP ed il Controller.
  3. Apri la pagina /public/amazon nel browser: vedrai la vetrina ed il carrello funzionante con i link referral automatici!