Omnibus Price Tracker
Applicazione Shopify per la conformita alla Direttiva EU 2019/2161 (Omnibus). Traccia automaticamente lo storico dei prezzi e mostra il prezzo piu basso degli ultimi 30 giorni quando un prodotto e in sconto, con supporto completo per Shopify Markets.
Direttiva Omnibus: Dal 28 maggio 2022, i venditori online nell'UE devono mostrare il prezzo piu basso degli ultimi 30 giorni ogni volta che un prodotto viene pubblicizzato con una riduzione di prezzo.
Funzionalita Principali
Tracking Automatico
Tracciamento automatico di ogni variazione di prezzo tramite webhook Shopify in tempo reale.
Shopify Markets
Supporto completo per Shopify Markets con prezzi contestuali per ogni market/regione configurato.
Theme Extension
App Block personalizzabile per visualizzare il badge Omnibus nel tema del negozio, market-aware.
Dashboard Analytics
Pannello di controllo completo con grafici, trend e statistiche sugli sconti per market.
Novita v2.0: Supporto completo per Shopify Markets. Ogni market puo avere prezzi base diversi, e l'app traccia il prezzo minimo per ciascun market separatamente.
Installation
Segui questi passaggi per installare e configurare l'applicazione nel tuo ambiente di sviluppo.
Prerequisiti
- Node.js versione 18.x o superiore
- npm o yarn come package manager
- Shopify CLI versione 3.x
- Account Shopify Partner
- Account Supabase (gratuito)
1. Accedi alla Directory del Progetto
cd bk-omnibus
2. Installazione Dipendenze
# Backend
npm install
# Frontend
cd web && npm install
3. Configurazione Shopify Partner
-
Crea una nuova App
Vai su partners.shopify.com e crea una nuova app. Seleziona "Build app for partners". -
Ottieni le credenziali
CopiaAPI KeyeAPI Secretdalla sezione "App setup". -
Configura gli URL
Imposta l'App URL e i Redirect URLs (verranno aggiornati automaticamente in development).
4. Configurazione Supabase
-
Crea un nuovo progetto
Vai su supabase.com e crea un nuovo progetto. -
Esegui le migrazioni
Vai nella sezione SQL Editor ed esegui i file insupabase/migrations/. -
Copia le chiavi API
OttieniURL,anon keyeservice_role keyda Project Settings - API.
5. Variabili d'Ambiente
# Shopify
SHOPIFY_API_KEY=your-api-key
SHOPIFY_API_SECRET=your-api-secret
SHOPIFY_SCOPES=read_products,write_products,read_orders
SHOPIFY_API_VERSION=2026-01
# App
APP_URL=https://your-app-domain.com
PORT=3000
# Supabase
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
Quick Start
Una volta completata l'installazione, puoi avviare l'applicazione in modalita sviluppo.
Avvio Development Server
# Avvia il backend NestJS
npm run start:dev
# In un altro terminale, avvia il frontend
cd web && npm run dev
# Oppure usa Shopify CLI per tutto
shopify app dev
L'app sara disponibile su https://localhost:3000. Shopify CLI creera automaticamente un tunnel HTTPS per il testing.
Testing su Dev Store
- Apri il dev store nel Shopify Admin
- Vai su Apps - la tua app verra installata automaticamente
- Configura i Markets nel Shopify Admin (Settings - Markets)
- Modifica il prezzo di un prodotto per testare il webhook
- Verifica che il prezzo sia stato salvato nel database per ogni market
Architecture
L'applicazione segue un'architettura modulare con separazione tra backend, database e frontend, con supporto nativo per Shopify Markets.
Webhooks + Markets
API + contextualPricing
PostgreSQL
Market-aware Liquid
Per-market prices
React + Polaris
Stack Tecnologico
| Componente | Tecnologia | Versione |
|---|---|---|
| Backend | NestJS (TypeScript) | 10.x |
| Database | Supabase (PostgreSQL) | 15.x |
| Frontend | React + Polaris | 18.x / 13.x |
| Theme | Liquid (Theme App Extension) | - |
| Shopify API | GraphQL Admin API | 2026-01 |
Project Structure
Struttura delle directory del progetto con descrizione dei file principali.
Descrizione Moduli
| Modulo | Descrizione |
|---|---|
auth |
Gestione autenticazione con Token Exchange (API 2026-01) |
shops |
CRUD negozi, gestione impostazioni |
products |
Sincronizzazione prodotti da Shopify |
markets |
Gestione Shopify Markets, sync markets, contextual pricing |
price-history |
Storico prezzi per market, calcolo minimo 30 giorni, cache |
webhooks |
Handler per webhook Shopify con pricing multi-market |
metafields |
Sincronizzazione metafield con chiavi per market handle |
analytics |
Aggregazioni, statistiche, trend per market |
Authentication
L'applicazione utilizza il nuovo flusso Token Exchange introdotto con l'API 2026-01, che sostituisce il tradizionale OAuth redirect flow.
Session Token Flow
Con il Token Exchange, l'autenticazione avviene interamente lato client tramite App Bridge:
- L'app viene caricata nel Shopify Admin (Managed Installation)
- App Bridge fornisce un Session Token (JWT, validita 1 minuto)
- Il backend scambia il Session Token per un Access Token
- L'Access Token viene salvato nel database per le chiamate API
Validazione Session Token
validateSessionToken(sessionToken: string): SessionTokenPayload {
const decoded = jwt.verify(sessionToken, this.apiSecret, {
algorithms: ['HS256'],
audience: this.apiKey,
});
// Verifica che issuer e destination corrispondano
const issuerHost = new URL(decoded.iss).host;
const destHost = new URL(decoded.dest).host;
if (issuerHost !== destHost) {
throw new UnauthorizedException('Invalid token');
}
return decoded;
}
Token Exchange Request
{
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": "session-token-jwt",
"subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
"requested_token_type": "urn:shopify:params:oauth:token-type:offline-access-token"
}
ShopifySessionGuard
Il guard ShopifySessionGuard protegge automaticamente gli endpoint che richiedono autenticazione:
@Controller('products')
@UseGuards(ShopifySessionGuard)
export class ProductsController {
@Get()
async listProducts(@CurrentShop() shop: ShopSession) {
// shop.shopDomain, shop.accessToken, shop.shopId disponibili
}
}
Webhooks
I webhook Shopify sono il meccanismo principale per il tracking in tempo reale delle variazioni di prezzo. Con il supporto Markets, ogni webhook processa i prezzi per tutti i market abilitati.
Webhook Configurati
| Topic | Descrizione | Azione |
|---|---|---|
products/create |
Nuovo prodotto creato | Registra prezzo iniziale per ogni market |
products/update |
Prodotto modificato | Fetch contextualPricing per ogni market |
products/delete |
Prodotto eliminato | Rimuove dalla cache tutti i market |
app/uninstalled |
App disinstallata | Disattiva shop e pulisce dati |
Flusso Webhook con Markets
Quando un prodotto viene aggiornato, il webhook handler:
- Verifica HMAC e identifica lo shop
- Recupera tutti i market abilitati per lo shop
- Per ogni market, esegue una query
contextualPricing - Salva i prezzi in
price_historycon ilmarket_id - Aggiorna la cache e i metafield
HMAC Verification
Ogni webhook ricevuto viene verificato tramite HMAC SHA-256 per garantire l'autenticita:
verifyHmac(rawBody: Buffer, hmacHeader: string): boolean {
const calculatedHmac = crypto
.createHmac('sha256', this.apiSecret)
.update(rawBody)
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(calculatedHmac),
Buffer.from(hmacHeader)
);
}
Shopify Markets
L'app supporta nativamente Shopify Markets, permettendo di tracciare prezzi diversi per ogni market/regione configurato nel negozio.
Shopify Markets vs Multi-Currency: Markets permettono di avere prezzi BASE diversi per regione (es. Italia 50EUR, Germania 55EUR), non solo conversioni di valuta. Questo e fondamentale per la conformita Omnibus.
Come Funziona
- Sync Markets - All'installazione, l'app sincronizza tutti i market configurati dallo shop
- Contextual Pricing - Per ogni prodotto, recupera il prezzo specifico di ogni market via GraphQL
- Storage Separato - I prezzi vengono salvati con
market_idper tracking indipendente - Display Market-Aware - Il tema mostra il prezzo minimo del market corrente del visitatore
Markets Service
// Sync markets from Shopify
async syncMarketsFromShopify(
shopId: string,
shopDomain: string,
accessToken: string
): Promise<ShopMarketRow[]>
// Get contextual pricing for a product in a market
async getContextualPricing(
shopDomain: string,
accessToken: string,
productId: string,
marketId: string
): Promise<VariantPricing[]>
GraphQL Query - Contextual Pricing
query getContextualPricing($productId: ID!, $context: ContextualPricingContext!) {
product(id: $productId) {
id
variants(first: 100) {
nodes {
id
contextualPricing(context: $context) {
price {
amount
currencyCode
}
compareAtPrice {
amount
currencyCode
}
}
}
}
}
}
Variables
{
"productId": "gid://shopify/Product/123",
"context": {
"market": {
"id": "gid://shopify/Market/456"
}
}
}
API Endpoints Markets
| Method | Endpoint | Descrizione |
|---|---|---|
| GET | /markets |
Lista tutti i market dello shop |
| POST | /markets/sync |
Sincronizza markets da Shopify |
| GET | /markets/:id |
Dettaglio singolo market |
| PATCH | /markets/:id/settings |
Aggiorna impostazioni market (history_days, is_enabled) |
Configurazione Giorni Storico
Ogni market puo avere un numero di giorni diverso per il calcolo del prezzo minimo:
{
"history_days": 60, // 7-365 giorni
"is_enabled": true
}
Esempi di configurazione:
- Germania: 30 giorni (default UE)
- Italia: 60 giorni (requisito AGCM)
- UK: 30 giorni
Price Tracking
Il cuore dell'applicazione: tracciamento dello storico prezzi per ogni market e calcolo del prezzo minimo nel periodo configurato (default 30 giorni, configurabile per market).
Flusso di Tracciamento
- Ricezione webhook - Il prodotto viene modificato su Shopify
- Fetch markets - Recupera tutti i market abilitati per lo shop con i relativi
history_days - Contextual pricing - Per ogni market, ottieni il prezzo contestuale
- Confronto prezzo - Verifica se il prezzo e cambiato rispetto all'ultimo registrato
- Salvataggio - Se diverso, salva in
price_historyconmarket_id - Calcolo minimo - Ricalcola il prezzo minimo usando i
history_daysdel market specifico - Aggiornamento cache - Aggiorna
price_minimum_cache - Sync metafield - Aggiorna il metafield con prezzi per market handle
Calcolo Prezzo Minimo per Market
Il metodo calculateMinPrice accetta un parametro days per supportare periodi configurabili per market:
async calculateMinPrice(
shopId: string,
variantId: number,
marketId: string,
days: number = 30 // Configurable per market
): Promise<number | null> {
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const { data } = await this.supabase.client
.from('price_history')
.select('price')
.eq('shop_id', shopId)
.eq('variant_id', variantId)
.eq('market_id', marketId)
.gte('recorded_at', startDate.toISOString())
.order('price', { ascending: true })
.limit(1)
.single();
return data?.price ?? null;
}
Cron Jobs
| Job | Frequenza | Descrizione |
|---|---|---|
recalculateRecentChanges |
Ogni ora | Ricalcola cache per varianti modificate per ogni market |
cleanOldHistory |
2:00 AM | Elimina storico > 35 giorni |
syncAllMetafields |
3:00 AM | Sincronizza metafield multi-market su Shopify |
calculateDailyAnalytics |
1:00 AM | Calcola statistiche giornaliere per market |
Metafields
I metafield Shopify vengono utilizzati per rendere disponibile il prezzo minimo nel tema Liquid. I valori sono organizzati per market handle.
Struttura Metafield
| Proprieta | Valore |
|---|---|
| Owner | ProductVariant |
| Namespace | $app:omnibus |
| Key | min_price_30d |
| Type | json |
Formato Valore (Multi-Market)
Le chiavi sono i market handle (es. "it", "de", "primary"):
{
"primary": "19.99 EUR",
"it": "19.99 EUR",
"de": "21.99 EUR",
"us": "24.99 USD"
}
GraphQL Mutation
mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
metafields {
id
}
userErrors {
field
message
}
}
}
Accesso in Liquid (Market-Aware)
Nel tema, il metafield viene letto usando il market handle corrente del visitatore:
{% assign min_price_json = product.selected_or_first_available_variant.metafields.app--omnibus.min_price_30d.value %}
{% if min_price_json %}
{%- comment -%} Usa il market handle corrente {%- endcomment -%}
{% assign market_handle = localization.market.handle | default: 'primary' %}
{% assign min_price = min_price_json[market_handle] %}
{% if min_price %}
<div class="omnibus-badge">
Prezzo piu basso 30gg: <strong>{{ min_price }}</strong>
</div>
{% endif %}
{% endif %}
Analytics
Il modulo analytics fornisce statistiche aggregate per la dashboard amministrativa, filtrabili per market.
Metriche Disponibili
- Total Products - Numero totale di varianti tracciate nel market
- Products on Sale - Varianti attualmente in sconto nel market
- Average Discount - Percentuale media di sconto
- Price Changes - Numero di variazioni prezzo (7/30 giorni)
- Discount Distribution - Distribuzione sconti per range
- Market Comparison - Confronto statistiche tra markets
API Endpoints
| Endpoint | Descrizione |
|---|---|
GET /analytics/dashboard?marketId=xxx |
Statistiche principali per market |
GET /analytics/trend?marketId=xxx |
Trend ultimi N giorni per market |
GET /analytics/top-discounted?marketId=xxx |
Top 10 prodotti piu scontati nel market |
GET /analytics/discount-distribution?marketId=xxx |
Distribuzione per range nel market |
GET /analytics/market-comparison |
Confronto tra tutti i market |
Database Schema
Schema PostgreSQL su Supabase con 5 tabelle principali, tutte con supporto market_id.
shops
Informazioni sui negozi installati.
CREATE TABLE shops (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shop_domain VARCHAR(255) UNIQUE NOT NULL,
access_token TEXT NOT NULL,
refresh_token TEXT,
token_expires_at TIMESTAMPTZ,
scope TEXT,
is_active BOOLEAN DEFAULT true,
settings JSONB DEFAULT '{...}',
shop_currency VARCHAR(3) DEFAULT 'EUR',
installed_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
shop_markets
Markets configurati per ogni shop con impostazioni personalizzabili per il periodo di storico prezzi.
CREATE TABLE shop_markets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shop_id UUID REFERENCES shops(id) ON DELETE CASCADE,
market_id VARCHAR(255) NOT NULL,
market_handle VARCHAR(100) NOT NULL,
market_name VARCHAR(255) NOT NULL,
currency_code VARCHAR(3) NOT NULL,
is_primary BOOLEAN DEFAULT false,
is_enabled BOOLEAN DEFAULT true,
history_days INT DEFAULT 30, -- Configurable per market (7-365)
countries TEXT[],
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(shop_id, market_id)
);
history_days configurabile: Ogni market puo avere un periodo di storico prezzi diverso. Ad esempio, Germania 30 giorni, Italia 60 giorni. Questo permette di rispettare requisiti normativi diversi per paese.
price_history
Storico completo delle variazioni di prezzo per market.
CREATE TABLE price_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shop_id UUID REFERENCES shops(id) ON DELETE CASCADE,
variant_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
market_id VARCHAR(255) NOT NULL,
price DECIMAL(12,2) NOT NULL,
compare_at_price DECIMAL(12,2),
currency VARCHAR(3) NOT NULL DEFAULT 'EUR',
recorded_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(shop_id, variant_id, market_id, recorded_at)
);
price_minimum_cache
Cache dei prezzi minimi calcolati per market.
CREATE TABLE price_minimum_cache (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shop_id UUID REFERENCES shops(id) ON DELETE CASCADE,
variant_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
market_id VARCHAR(255) NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'EUR',
min_price_30d DECIMAL(12,2) NOT NULL,
current_price DECIMAL(12,2) NOT NULL,
compare_at_price DECIMAL(12,2),
is_on_sale BOOLEAN DEFAULT false,
discount_percentage DECIMAL(5,2),
discount_from_min DECIMAL(5,2),
last_calculated TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(shop_id, variant_id, market_id)
);
Indici
-- Query veloci sui 30 giorni per market
CREATE INDEX idx_price_history_lookup
ON price_history(shop_id, variant_id, market_id, recorded_at DESC);
-- Query analytics per market
CREATE INDEX idx_price_history_analytics
ON price_history(shop_id, market_id, recorded_at, product_id);
-- Prodotti in sconto per market
CREATE INDEX idx_cache_on_sale
ON price_minimum_cache(shop_id, market_id, is_on_sale)
WHERE is_on_sale = true;
-- Markets per shop
CREATE INDEX idx_shop_markets_enabled
ON shop_markets(shop_id, is_enabled);
Migrations
File di migrazione SQL da eseguire su Supabase.
Esecuzione Migrazioni
- Apri il progetto Supabase
- Vai su SQL Editor
- Copia ed esegui i file in ordine:
001_initial_schema.sql- Include tabellashop_markets002_rls_policies.sql
In alternativa, puoi usare la Supabase CLI: supabase db push
RLS Policies
Row Level Security policies per l'isolamento dei dati tra shop.
-- Abilita RLS
ALTER TABLE shops ENABLE ROW LEVEL SECURITY;
ALTER TABLE shop_markets ENABLE ROW LEVEL SECURITY;
ALTER TABLE price_history ENABLE ROW LEVEL SECURITY;
ALTER TABLE price_minimum_cache ENABLE ROW LEVEL SECURITY;
ALTER TABLE price_analytics ENABLE ROW LEVEL SECURITY;
-- Policy per service_role (backend)
CREATE POLICY "Service role has full access"
ON shops
FOR ALL
USING (auth.role() = 'service_role')
WITH CHECK (auth.role() = 'service_role');
Admin Dashboard
Interfaccia amministrativa costruita con React e Shopify Polaris, con selettore market.
Pagine
| Pagina | Path | Descrizione |
|---|---|---|
| Dashboard | / |
Overview con statistiche e grafici per market selezionato |
| Products | /products |
Lista prodotti con filtri e selector market |
| Product Detail | /products/:id |
Storico prezzo singolo prodotto per market |
| Markets | /markets |
Gestione markets abilitati |
| Settings | /settings |
Configurazione app |
Market Selector
Ogni pagina include un selettore market per filtrare i dati visualizzati:
const MarketSelector = ({ markets, selected, onChange }) => {
return (
<Select
label="Market"
options={markets.map(m => ({
label: `${m.market_name} (${m.currency_code})`,
value: m.market_id
}))}
value={selected}
onChange={onChange}
/>
);
};
Theme Extension
Theme App Extension per visualizzare il badge Omnibus nel tema del negozio, con supporto automatico per il market corrente del visitatore.
Struttura
App Block (Market-Aware)
Il block omnibus-price.liquid rileva automaticamente il market del visitatore:
{% if block.settings.enable %}
{% assign current_variant = product.selected_or_first_available_variant %}
{% assign compare_at_price = current_variant.compare_at_price %}
{% if compare_at_price and compare_at_price > current_variant.price %}
{% assign min_price_data = current_variant.metafields.app--omnibus.min_price_30d.value %}
{% if min_price_data %}
{%- comment -%} Rileva il market handle corrente {%- endcomment -%}
{% assign market_handle = localization.market.handle | default: 'primary' %}
{% assign min_price_string = min_price_data[market_handle] %}
{% if min_price_string %}
<div class="omnibus-price-notice"
style="color: {{ block.settings.text_color }};
background: {{ block.settings.background_color }};">
{% if block.settings.show_icon %}
<svg ...>...</svg>
{% endif %}
<span>{{ 'omnibus.lowest_price_label' | t }}:
<strong>{{ min_price_string }}</strong>
</span>
</div>
{% endif %}
{% endif %}
{% endif %}
{% endif %}
Automatic Market Detection: Il template usa localization.market.handle per rilevare automaticamente il market del visitatore e mostrare il prezzo minimo corretto.
Localization
L'extension supporta multiple lingue tramite i file di traduzione.
Lingue Supportate
English
en.default.json
Italiano
it.json
Deutsch
de.json
Francais
fr.json
Chiavi di Traduzione
{
"omnibus": {
"lowest_price_label": "Prezzo piu basso degli ultimi 30 giorni",
"lowest_price_short": "Min. 30gg",
"info_tooltip": "In conformita alla Direttiva UE 2019/2161..."
}
}
API Endpoints
Riferimento completo degli endpoint REST API.
Authentication
| Method | Endpoint | Descrizione |
|---|---|---|
| POST | /auth/token-exchange |
Scambia session token per access token |
| POST | /auth/refresh |
Rinnova access token |
| GET | /auth/verify |
Verifica sessione attiva |
Markets
| Method | Endpoint | Descrizione |
|---|---|---|
| GET | /markets |
Lista tutti i market dello shop |
| POST | /markets/sync |
Sincronizza markets da Shopify |
| GET | /markets/:id |
Dettaglio singolo market |
| PUT | /markets/:id/toggle |
Abilita/disabilita market |
Products
| Method | Endpoint | Descrizione |
|---|---|---|
| GET | /products |
Lista prodotti paginata |
| GET | /products/:id |
Dettaglio singolo prodotto |
| GET | /products/:id/pricing?marketId=xxx |
Pricing contestuale per market |
| POST | /products/sync |
Sincronizza tutti i prodotti per tutti i market |
Price History
| Method | Endpoint | Descrizione |
|---|---|---|
| GET | /price-history/variant/:id?marketId=xxx |
Storico prezzi variante per market |
| GET | /price-history/on-sale?marketId=xxx |
Prodotti in sconto nel market |
| GET | /price-history/top-discounted?marketId=xxx |
Top prodotti scontati nel market |
Analytics
| Method | Endpoint | Descrizione |
|---|---|---|
| GET | /analytics/dashboard?marketId=xxx |
Statistiche dashboard per market |
| GET | /analytics/trend?marketId=xxx |
Trend temporale per market |
| GET | /analytics/market-comparison |
Confronto tra tutti i market |
GraphQL Queries
Query GraphQL utilizzate per comunicare con Shopify Admin API.
Fetch Markets
query getMarkets($cursor: String) {
markets(first: 50, after: $cursor) {
nodes {
id
handle
name
primary
currencySettings {
baseCurrency {
currencyCode
}
}
regions(first: 50) {
nodes {
name
... on Country {
code
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Contextual Pricing
query getContextualPricing($productId: ID!, $context: ContextualPricingContext!) {
product(id: $productId) {
id
variants(first: 100) {
nodes {
id
contextualPricing(context: $context) {
price {
amount
currencyCode
}
compareAtPrice {
amount
currencyCode
}
}
}
}
}
}
Update Metafields (Multi-Market)
{
"metafields": [
{
"ownerId": "gid://shopify/ProductVariant/123",
"namespace": "$app:omnibus",
"key": "min_price_30d",
"type": "json",
"value": "{\"primary\": \"19.99 EUR\", \"it\": \"19.99 EUR\", \"de\": \"21.99 EUR\"}"
}
]
}
Error Handling
Gestione degli errori e codici di risposta.
Codici HTTP
| Codice | Significato | Azione |
|---|---|---|
200 |
Success | Richiesta completata |
400 |
Bad Request | Verifica parametri (es. marketId mancante) |
401 |
Unauthorized | Token non valido o scaduto |
404 |
Not Found | Risorsa o market non trovato |
429 |
Too Many Requests | Rate limit raggiunto |
500 |
Internal Error | Errore server |
Environment Variables
Variabili d'ambiente richieste per la configurazione.
Backend (.env)
| Variabile | Descrizione | Required |
|---|---|---|
SHOPIFY_API_KEY |
API Key dell'app Shopify | Yes |
SHOPIFY_API_SECRET |
API Secret dell'app Shopify | Yes |
SHOPIFY_SCOPES |
Scope richiesti | Yes |
SHOPIFY_API_VERSION |
Versione API (2026-01) | Yes |
APP_URL |
URL pubblico dell'app | Yes |
PORT |
Porta server (default: 3000) | |
SUPABASE_URL |
URL progetto Supabase | Yes |
SUPABASE_ANON_KEY |
Chiave anonima Supabase | Yes |
SUPABASE_SERVICE_ROLE_KEY |
Chiave service role | Yes |
Frontend (web/.env)
| Variabile | Descrizione | Required |
|---|---|---|
VITE_SHOPIFY_API_KEY |
API Key per App Bridge | Yes |
VITE_API_URL |
URL backend API | Yes |
Deployment Guide
Guida al deployment in produzione.
Checklist Pre-Deploy
- Configurare tutte le variabili d'ambiente in produzione
- Eseguire migrazioni database su Supabase (inclusa tabella
shop_markets) - Aggiornare URL in shopify.app.toml
- Verificare webhook endpoints raggiungibili
- Testare flusso completo su dev store con multiple markets
Deploy con Shopify CLI
# Build dell'applicazione
npm run build
cd web && npm run build
# Deploy su Shopify
shopify app deploy
Post-Deploy
- Verifica che l'app sia accessibile dall'Admin Shopify
- Vai su Markets nella dashboard e verifica che siano sincronizzati
- Testa la sincronizzazione prodotti per ogni market
- Modifica un prezzo e verifica il webhook
- Controlla che il badge mostri il prezzo corretto per ogni market
Troubleshooting
Problemi comuni e relative soluzioni.
Markets non sincronizzati
- Verifica che lo shop abbia almeno un market configurato (Settings - Markets)
- Esegui manualmente
POST /markets/sync - Controlla i log per errori GraphQL
Prezzo minimo errato per market
- Verifica che il market sia abilitato nella dashboard
- Controlla che
contextualPricingrestituisca prezzi per quel market - Verifica che il market handle nel metafield corrisponda a
localization.market.handle
Badge non visibile nel tema
- Verifica che il prodotto sia effettivamente in sconto (
compare_at_price > price) - Controlla che il metafield esista con la chiave del market corrente
- Verifica che
localization.market.handlecorrisponda a una chiave nel JSON
Webhook non ricevuti
- Verifica che l'URL del webhook sia raggiungibile pubblicamente
- Controlla i log di Shopify Partner Dashboard - Webhooks
- Assicurati che HTTPS sia configurato correttamente
Database connection error
- Verifica
SUPABASE_URLeSUPABASE_SERVICE_ROLE_KEY - Controlla che la tabella
shop_marketsesista - Verifica che RLS sia configurato correttamente