Ugrás a tartalomhoz
ShopentlyLearn

Rendelések exportálása

Rendelések exportálása. Paraméterek: CREATED_FROM, CREATED_TO. Kódpéldák: cURL, PHP, Python.

Paraméterek

  • SHOPENTLY_SHOP_URL — Az erőforrás URL-je.
  • SHOPENTLY_ACCESS_TOKEN — Rövid élettartamú Bearer hitelesítő adat.
  • CREATED_FROM — A létrehozási idő szerinti szűrés kezdete.
  • CREATED_TO — A létrehozási idő szerinti szűrés vége.

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

Teljes példa

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
    if [ "$method" = GET ]; then set -- "$@" --retry 2 --retry-max-time 60; 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
}

query='limit=100'
: "${CREATED_FROM:?}" "${CREATED_TO:?}"
query="$query&created_from=$(encode "$CREATED_FROM")&created_to=$(encode "$CREATED_TO")"
seen='[]'
while :; do
    page=$(api GET "/api/v1/orders?$query")
    printf '%s' "$page" | jq -r '.data[].id | @uri' > "$work/order-ids"
    while IFS= read -r order_id; do
        detail=$(api GET "/api/v1/orders/$order_id")
        printf '%s' "$detail" | jq -c '.data'
    done < "$work/order-ids"
    more=$(printf '%s' "$page" | jq -r '.meta.pagination.has_more')
    [ "$more" = true ] || break
    cursor=$(printf '%s' "$page" | jq -er '.meta.pagination.next_cursor')
    if printf '%s' "$seen" | jq -e --arg cursor "$cursor" 'index($cursor) != null' >/dev/null; then
        echo 'Repeated cursor' >&2; exit 1
    fi
    seen=$(printf '%s' "$seen" | jq -c --arg cursor "$cursor" '. + [$cursor]')
    query="limit=100&cursor=$(encode "$cursor")"
    query="$query&created_from=$(encode "$CREATED_FROM")&created_to=$(encode "$CREATED_TO")"
done

PHP

<?php
declare(strict_types=1);
set_exception_handler(static function (Throwable $error): void {
    fwrite(STDERR, $error->getMessage() . PHP_EOL);
    exit(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;
}
function readOrderApi(string $path, array $query = []): array {
    $deadline = hrtime(true) / 1e9 + 60;
    for ($attempt = 0; $attempt < 3; $attempt++) {
        try {
            return api('GET', $path, $query);
        } catch (ApiError $error) {
            if ($attempt === 2 || !in_array($error->status, [408, 429, 500, 502, 503, 504], true)) throw $error;
            $delay = 2 ** $attempt;
            $retry = $error->headers['retry-after'] ?? '';
            if ($retry !== '') {
                if (preg_match('/^[0-9]+$/D', $retry) === 1) $delay = (float)$retry;
                elseif (($date = strtotime($retry)) !== false) $delay = max(0, $date - time());
            }
            if ($delay > $deadline - hrtime(true) / 1e9) throw $error;
            usleep((int)($delay * 1000000));
        }
    }
    throw new RuntimeException('GET retry limit reached');
}
$query = ['limit' => 100, 'created_from' => env('CREATED_FROM'), 'created_to' => env('CREATED_TO')];
$seen = [];
do {
    $page = readOrderApi('/api/v1/orders', $query);
    foreach ($page['data'] as $record) {
        $detail = readOrderApi('/api/v1/orders/' . rawurlencode($record['id']));
        output($detail['data']);
    }
    $pagination = $page['meta']['pagination'];
    if (!$pagination['has_more']) break;
    $cursor = $pagination['next_cursor'];
    if (!$cursor || isset($seen[$cursor])) throw new RuntimeException('Missing or repeated cursor');
    $seen[$cursor] = true;
    $query['cursor'] = $cursor;
} while (true);

Python

"""Run with Python 3.12 and requests. Credentials stay in environment variables."""
import json
import os
import time
from urllib.parse import quote, urlsplit
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
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="")

def read(path, query=None):
    deadline = time.monotonic() + 60
    for attempt in range(3):
        try:
            return api("GET", path, query=query)
        except ApiError as error:
            if attempt == 2 or error.status not in {408, 429, 500, 502, 503, 504}:
                raise
            delay = 2 ** attempt
            if error.retry_after:
                try:
                    delay = int(error.retry_after)
                except ValueError:
                    try:
                        delay = (parsedate_to_datetime(error.retry_after) - datetime.now(timezone.utc)).total_seconds()
                    except (ValueError, TypeError):
                        pass
            delay = max(0, delay)
            if delay > deadline - time.monotonic():
                raise
            time.sleep(delay)

query = {"limit": 100}
query.update(created_from=env("CREATED_FROM"), created_to=env("CREATED_TO"))
seen = set()
while True:
    page = read("/api/v1/orders", query)
    for record in page["data"]:
        detail = read("/api/v1/orders/" + segment(record["id"]))
        print(json.dumps(detail["data"], ensure_ascii=False, allow_nan=False))
    pagination = page["meta"]["pagination"]
    if not pagination["has_more"]:
        break
    cursor = pagination["next_cursor"]
    if not cursor or cursor in seen:
        raise RuntimeError("Missing or repeated cursor")
    seen.add(cursor)
    query["cursor"] = cursor

Működés

A rendeléslista és a részletek tartalmazzák a delivery.dates objektumot. A YYYY-MM-DD dátumok tervezettek, nem igazolják a feladást. Az aktuális dátumok elsőbbséget élveznek; a régebbi rendelések a mentett becslést használják. A törölt vagy ismeretlen érték null. A részletek minden tételt és termékképet is tartalmaznak.

Az érvényes állapotazonosítókat az /api/v1/order-statuses végpontból kérdezze le. A rendelési és fizetési állapot külön PATCH végpontot és jogosultságot használ. A rögzített fizetési állapot módosítása nem terhel bankkártyát, és nem indít visszatérítést a fizetési szolgáltatónál.

delivery.dates: planned_dispatch_on, dispatch_timezone, estimated_delivery_from, estimated_delivery_to. Feladási és kézbesítési dátumok: YYYY-MM-DD; null. A feladás időzónája: IANA.

Rendelés · Összesítések

Az összegek és adókulcsok a mentett rendelésből származnak, a tételjavításokkal együtt. A hiányzó előzmény null, és nem helyettesíthető a katalógus aktuális adókulcsaival. Az összegek decimális karakterláncok maradnak.

  • tax_country_code · Adózási ország · Mentett rendelési adatok · ISO 3166-1 alpha-2; null.
  • lines[].unit_* · Egy egység · Kedvezmények előtt; lines[].original_total_* · Kedvezmények előtt; lines[].total_* · Kedvezmények után.
  • lines[].discount_* · Kedvezmények; manual_discount · Kézi kedvezmény; percentage "10" = 10%.
  • delivery.net / tax / gross · Kedvezmények után; delivery.original_* · Kedvezmények előtt; delivery.discount_* · Kedvezmények.
  • tax_rate: "0.23" = 23%; "0" ≠ null. Ismeretlen: null; ismert nulla: 0.
  • totals · Összesítések; tax_breakdown.groups · Adócsoportok; Fel nem osztott adó = order.tax − Σ groups.tax; ±.

A lista összefoglalókat ad vissza. Minden találathoz kérje le a GET /api/v1/orders/{order_id} végpontot a teljes pénzügyi exporthoz. A GET-ismétlések korlátozottak és figyelembe veszik a Retry-After értékét. A létrehozási dátum szerinti export nem szinkronizál későbbi javításokat.

A payment.amount a rendelés végösszege. A fizetési állapot nem határozza meg a fizetett, visszatérített vagy fennmaradó összegeket.