Aller au contenu
ShopentlyLearn

Modifier un prix

Modifier un prix. Paramètres: PRODUCT_ID, MARKET_ID. Exemples de code: cURL, PHP, Python.

Paramètres

  • SHOPENTLY_SHOP_URL — URL de la ressource.
  • SHOPENTLY_ACCESS_TOKEN — Jeton d'accès Bearer à courte durée de validité.
  • PRODUCT_ID — Identifiant du produit.
  • MARKET_ID — Identifiant de marché renvoyé par le point d'accès au contexte.
  • NEW_AMOUNT — Montant décimal sous forme de chaîne JSON ; null uniquement lorsque cela est autorisé.
  • IDEMPOTENCY_KEY — Identifiant unique d'une opération logique d'écriture.

PHP 8.3 (64-bit, ext-curl, JSON) · Python 3.12 + requests · cURL 8 + jq 1.7.

Exemple complet

cURL

#!/bin/sh
# POSIX shell, curl 8+, jq 1.7. Credentials stay in environment variables.
set -eu
: "${SHOPENTLY_SHOP_URL:?}"
base=${SHOPENTLY_SHOP_URL%/}
case "$base" in https://*) ;; *) echo 'Use an HTTPS origin' >&2; exit 1;; esac
authority=${base#https://}
case "$authority" in ''|*/*|*\?*|*\#*|*@*) echo 'Use an HTTPS origin' >&2; exit 1;; esac
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT HUP INT TERM
chmod 700 "$work"
encode() { printf '%s' "$1" | jq -sRr @uri; }
api() {
    method=$1; path=$2; body=${3-}; key=${4-}
    scheme=Bearer; credential=${SHOPENTLY_ACCESS_TOKEN-}
    if [ "$path" = /api/v1/auth/token ]; then scheme=ApiKey; credential=${SHOPENTLY_API_KEY-}; fi
    [ -n "$credential" ] || { echo 'Missing credential' >&2; return 1; }
    set -- --disable --globoff --silent --show-error --proto '=https' --connect-timeout 5 --max-time 30 \
        --request "$method" --header "Authorization: $scheme $credential" \
        --dump-header "$work/headers" --output "$work/body" --write-out '%{http_code}'
    if [ -n "$key" ]; then set -- "$@" --header "Idempotency-Key: $key"; fi
    if [ -n "$body" ]; then set -- "$@" --header 'Content-Type: application/json' --data-raw "$body"; fi
    status=$(curl "$@" --url "$base$path") || return 1
    case "$status" in
        2??) if [ -s "$work/body" ]; then jq -e . "$work/body"; else printf 'null\n'; fi ;;
        *) printf 'HTTP %s\n' "$status" >&2
           jq -c '.error | {code, fields}' "$work/body" >&2 2>/dev/null || true
           return 1 ;;
    esac
}

: "${MARKET_ID:?}" "${PRODUCT_ID:?}" "${NEW_AMOUNT:?}" "${IDEMPOTENCY_KEY:?}"
context=$(api GET /api/v1/context)
market=$(printf '%s' "$context" | jq -ec --arg id "$MARKET_ID" '.data.markets[] | select(.id == $id and .price_list != null)')
base_market=$(printf '%s' "$context" | jq -er '.data.base_market_id')
kind=market_fixed
[ "$MARKET_ID" != "$base_market" ] || kind=base
path="/api/v1/products/$(encode "$PRODUCT_ID")?market_id=$(encode "$MARKET_ID")"
product=$(api GET "$path")
body=$(printf '%s' "$product" | jq -c --argjson market "$market" --arg kind "$kind" --arg amount "$NEW_AMOUNT" '{
    expected_version: .data.version, pricing: {
        kind: $kind, amount: $amount, currency: $market.price_list.currency,
        price_list_id: $market.price_list.id, expected_price_list_version: $market.price_list.version,
        expected_version: .data.price_version, tax_included: $market.price_list.tax_included}}')
# Persist this key and body before sending. Retry an uncertain write unchanged.
api PATCH "$path" "$body" "$IDEMPOTENCY_KEY"

PHP

<?php
declare(strict_types=1);
// PHP 8.3+, 64-bit, ext-curl. Credentials stay in environment variables.
function env(string $name): string {
    $value = getenv($name);
    if ($value === false || $value === '' || strpbrk($value, "\r\n") !== false) {
        throw new RuntimeException('Invalid environment variable: ' . $name);
    }
    return $value;
}
$base = rtrim(env('SHOPENTLY_SHOP_URL'), '/');
$origin = parse_url($base);
if (!$origin || ($origin['scheme'] ?? '') !== 'https' || empty($origin['host']) ||
    isset($origin['user']) || isset($origin['pass']) || isset($origin['path']) ||
    isset($origin['query']) || isset($origin['fragment']) || PHP_INT_SIZE !== 8) {
    throw new RuntimeException('Use a 64-bit PHP runtime and an HTTPS origin');
}
class ApiError extends RuntimeException {
    public function __construct(public int $status, public array $headers, public array $error) {
        parent::__construct('HTTP ' . $status . '; code=' . ($error['code'] ?? '') .
            '; request_id=' . ($headers['x-request-id'] ?? ''));
    }
}
function api(string $method, string $path, array $query = [], ?array $body = null, ?string $key = null, bool $apiKey = false): ?array {
    global $base;
    $credential = env($apiKey ? 'SHOPENTLY_API_KEY' : 'SHOPENTLY_ACCESS_TOKEN');
    $headers = ['Authorization: ' . ($apiKey ? 'ApiKey ' : 'Bearer ') . $credential];
    if ($key !== null) $headers[] = 'Idempotency-Key: ' . $key;
    if ($body !== null) $headers[] = 'Content-Type: application/json';
    $url = $base . $path . ($query ? '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986) : '');
    $responseHeaders = [];
    $h = curl_init($url);
    curl_setopt_array($h, [
        CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false,
        CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 30,
        CURLOPT_HEADERFUNCTION => static function ($h, string $line) use (&$responseHeaders): int {
            if (str_contains($line, ':')) {
                [$name, $value] = explode(':', $line, 2);
                $responseHeaders[strtolower(trim($name))] = trim($value);
            }
            return strlen($line);
        },
    ]);
    if ($body !== null) {
        $json = $apiKey && $body === [] ? '{}' : json_encode($body, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
        curl_setopt($h, CURLOPT_POSTFIELDS, $json);
    }
    $raw = curl_exec($h);
    $status = curl_getinfo($h, CURLINFO_RESPONSE_CODE);
    $error = curl_error($h);
    curl_close($h);
    if ($raw === false) throw new RuntimeException('Transport error: ' . $error);
    if ($status < 200 || $status >= 300) {
        $data = json_decode($raw, true);
        throw new ApiError($status, $responseHeaders, is_array($data) ? ($data['error'] ?? []) : []);
    }
    return $raw === '' ? null : json_decode($raw, true, 512, JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING);
}
function output(mixed $value): void {
    echo json_encode($value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE), PHP_EOL;
}
$context = api('GET', '/api/v1/context')['data'];
$matches = array_values(array_filter($context['markets'], fn($m) => $m['id'] === env('MARKET_ID')));
if (count($matches) !== 1 || !$matches[0]['price_list']) throw new RuntimeException('Select a market with an active price list');
$market = $matches[0]; $priceList = $market['price_list'];
$path = '/api/v1/products/' . rawurlencode(env('PRODUCT_ID'));
$query = ['market_id' => $market['id']];
$product = api('GET', $path, $query)['data'];
$body = ['expected_version' => $product['version'], 'pricing' => [
    'kind' => $market['id'] === $context['base_market_id'] ? 'base' : 'market_fixed',
    'amount' => env('NEW_AMOUNT'), 'currency' => $priceList['currency'],
    'price_list_id' => $priceList['id'], 'expected_price_list_version' => $priceList['version'],
    'expected_version' => $product['price_version'], 'tax_included' => $priceList['tax_included'],
]];
// Persist this key and body before sending. Retry an uncertain write unchanged.
output(api('PATCH', $path, $query, $body, env('IDEMPOTENCY_KEY')));

Python

"""Run with Python 3.12 and requests. Credentials stay in environment variables."""
import json
import os
import random
import time
from urllib.parse import quote, urlencode, urlsplit
import requests

def env(name):
    value = os.environ[name]
    if not value or "\r" in value or "\n" in value:
        raise ValueError("Invalid environment variable: " + name)
    return value

base = env("SHOPENTLY_SHOP_URL").rstrip("/")
origin = urlsplit(base)
if origin.scheme != "https" or not origin.hostname or origin.username or origin.password or origin.path or origin.query or origin.fragment:
    raise ValueError("SHOPENTLY_SHOP_URL must be an HTTPS origin")

class ApiError(RuntimeError):
    def __init__(self, response):
        self.status = response.status_code
        self.request_id = response.headers.get("X-Request-ID", "")
        self.retry_after = response.headers.get("Retry-After", "")
        try:
            self.error = response.json().get("error", {})
        except (ValueError, AttributeError):
            self.error = {}
        super().__init__(f"HTTP {self.status}; code={self.error.get('code', '')}; request_id={self.request_id}")

def api(method, path, *, query=None, body=None, key=None, api_key=False):
    credential = env("SHOPENTLY_API_KEY" if api_key else "SHOPENTLY_ACCESS_TOKEN")
    headers = {"Authorization": ("ApiKey " if api_key else "Bearer ") + credential}
    if key:
        headers["Idempotency-Key"] = key
    if body is not None:
        headers["Content-Type"] = "application/json"
    response = requests.request(method, base + path, params=query, headers=headers,
        data=json.dumps(body, ensure_ascii=False, allow_nan=False).encode() if body is not None else None,
        timeout=(5, 30), allow_redirects=False)
    if not 200 <= response.status_code < 300:
        raise ApiError(response)
    return response.json() if response.content else None

def segment(value):
    return quote(value, safe="")

# The chosen market must expose its own active price list.
context = api("GET", "/api/v1/context")["data"]
market = next(m for m in context["markets"] if m["id"] == env("MARKET_ID"))
price_list = market["price_list"]
if not price_list:
    raise ValueError("Selected market has no active price list")
path = "/api/v1/products/" + segment(env("PRODUCT_ID"))
query = {"market_id": market["id"]}
product = api("GET", path, query=query)["data"]
body = {"expected_version": product["version"], "pricing": {
    "kind": "base" if market["id"] == context["base_market_id"] else "market_fixed",
    "amount": env("NEW_AMOUNT"), "currency": price_list["currency"],
    "price_list_id": price_list["id"], "expected_price_list_version": price_list["version"],
    "expected_version": product["price_version"], "tax_included": price_list["tax_included"]}}
# Persist this key and body before sending. Retry an uncertain write unchanged.
print(json.dumps(api("PATCH", path, query=query, body=body, key=env("IDEMPOTENCY_KEY"))))

Fonctionnement

Utilisez POST pour créer les ressources prises en charge, PATCH pour les modifications partielles et DELETE pour les suppressions disponibles. Chaque écriture de ressource nécessite Idempotency-Key. Générez une clé par opération logique et conservez clé, méthode, cible et contenu lors de sa répétition.

Lisez la ressource avant de la modifier et fournissez expected_version comme indiqué. Les prix peuvent aussi exiger les versions du produit, du prix, des prix de variantes ou de la liste de prix. Si une version change, relisez et réconciliez la modification plutôt que de forcer des données obsolètes.

La répétition d'une opération réussie renvoie son résultat enregistré sans appliquer à nouveau la modification. Réutiliser une clé pour une autre requête entraîne idempotency.key_reused. Les exemples sont synthétiques ; remplacez les identifiants par ceux de votre boutique avant de les exécuter.