docs(amazon): add unified Amazon Creators API & PA-API integration pack for PMS agent
This commit is contained in:
parent
a0b54cee64
commit
0e9475be9c
314
docs/ops/amazon_creators_pms_integration_pack.md
Normal file
314
docs/ops/amazon_creators_pms_integration_pack.md
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
# 📦 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:
|
||||
|
||||
```env
|
||||
# ==============================================================================
|
||||
# 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
|
||||
<?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`:
|
||||
|
||||
```html
|
||||
<!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!
|
||||
Loading…
Reference in New Issue
Block a user