v2.0.0

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

Terminal
cd bk-omnibus

2. Installazione Dipendenze

Terminal
# Backend
npm install

# Frontend
cd web && npm install

3. Configurazione Shopify Partner

  1. Crea una nuova App
    Vai su partners.shopify.com e crea una nuova app. Seleziona "Build app for partners".
  2. Ottieni le credenziali
    Copia API Key e API Secret dalla sezione "App setup".
  3. Configura gli URL
    Imposta l'App URL e i Redirect URLs (verranno aggiornati automaticamente in development).

4. Configurazione Supabase

  1. Crea un nuovo progetto
    Vai su supabase.com e crea un nuovo progetto.
  2. Esegui le migrazioni
    Vai nella sezione SQL Editor ed esegui i file in supabase/migrations/.
  3. Copia le chiavi API
    Ottieni URL, anon key e service_role key da Project Settings - API.

5. Variabili d'Ambiente

.env
# 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

Terminal
# 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

  1. Apri il dev store nel Shopify Admin
  2. Vai su Apps - la tua app verra installata automaticamente
  3. Configura i Markets nel Shopify Admin (Settings - Markets)
  4. Modifica il prezzo di un prodotto per testare il webhook
  5. 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.

Shopify Store
Webhooks + Markets
->
NestJS Backend
API + contextualPricing
->
Supabase
PostgreSQL


Theme Extension
Market-aware Liquid
<-
Metafields
Per-market prices
<-
Admin Dashboard
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.

bk-omnibus/
src/
modules/
auth/
shops/
products/
markets/
price-history/
webhooks/
metafields/
analytics/
common/
main.ts
app.module.ts
supabase/migrations/
extensions/theme-app-extension/
web/
shopify.app.toml
package.json

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:

  1. L'app viene caricata nel Shopify Admin (Managed Installation)
  2. App Bridge fornisce un Session Token (JWT, validita 1 minuto)
  3. Il backend scambia il Session Token per un Access Token
  4. L'Access Token viene salvato nel database per le chiamate API

Validazione Session Token

src/modules/auth/auth.service.ts
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

Token Exchange Payload
{
  "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:

Esempio utilizzo
@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:

  1. Verifica HMAC e identifica lo shop
  2. Recupera tutti i market abilitati per lo shop
  3. Per ogni market, esegue una query contextualPricing
  4. Salva i prezzi in price_history con il market_id
  5. Aggiorna la cache e i metafield

HMAC Verification

Ogni webhook ricevuto viene verificato tramite HMAC SHA-256 per garantire l'autenticita:

src/modules/webhooks/webhooks.service.ts
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

  1. Sync Markets - All'installazione, l'app sincronizza tutti i market configurati dallo shop
  2. Contextual Pricing - Per ogni prodotto, recupera il prezzo specifico di ogni market via GraphQL
  3. Storage Separato - I prezzi vengono salvati con market_id per tracking indipendente
  4. Display Market-Aware - Il tema mostra il prezzo minimo del market corrente del visitatore

Markets Service

src/modules/markets/markets.service.ts
// 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

GraphQL
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

JSON
{
  "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:

PATCH /markets/:id/settings
{
  "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

  1. Ricezione webhook - Il prodotto viene modificato su Shopify
  2. Fetch markets - Recupera tutti i market abilitati per lo shop con i relativi history_days
  3. Contextual pricing - Per ogni market, ottieni il prezzo contestuale
  4. Confronto prezzo - Verifica se il prezzo e cambiato rispetto all'ultimo registrato
  5. Salvataggio - Se diverso, salva in price_history con market_id
  6. Calcolo minimo - Ricalcola il prezzo minimo usando i history_days del market specifico
  7. Aggiornamento cache - Aggiorna price_minimum_cache
  8. 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:

src/modules/price-history/price-history.service.ts
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"):

JSON Value
{
  "primary": "19.99 EUR",
  "it": "19.99 EUR",
  "de": "21.99 EUR",
  "us": "24.99 USD"
}

GraphQL Mutation

GraphQL
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:

Liquid
{% 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.

SQL
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.

SQL
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.

SQL
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.

SQL
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

SQL
-- 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

  1. Apri il progetto Supabase
  2. Vai su SQL Editor
  3. Copia ed esegui i file in ordine:
    • 001_initial_schema.sql - Include tabella shop_markets
    • 002_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.

supabase/migrations/002_rls_policies.sql
-- 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:

web/src/components/MarketSelector.tsx
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

extensions/theme-app-extension/
blocks/
omnibus-price.liquid
snippets/
omnibus-badge.liquid
assets/
omnibus.css
locales/
en.default.json
it.json
shopify.extension.toml

App Block (Market-Aware)

Il block omnibus-price.liquid rileva automaticamente il market del visitatore:

blocks/omnibus-price.liquid
{% 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

locales/it.json
{
  "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

GraphQL
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

GraphQL
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)

JSON Variables
{
  "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

Terminal
# Build dell'applicazione
npm run build
cd web && npm run build

# Deploy su Shopify
shopify app deploy

Post-Deploy

  1. Verifica che l'app sia accessibile dall'Admin Shopify
  2. Vai su Markets nella dashboard e verifica che siano sincronizzati
  3. Testa la sincronizzazione prodotti per ogni market
  4. Modifica un prezzo e verifica il webhook
  5. 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 contextualPricing restituisca 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.handle corrisponda 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_URL e SUPABASE_SERVICE_ROLE_KEY
  • Controlla che la tabella shop_markets esista
  • Verifica che RLS sia configurato correttamente