Hoppa till innehållet
ShopentlyLearn

Justera lagersaldot

Justera lagersaldot. Parametrar: STOCK_ID, LOCATION_ID. Kodexempel: cURL, PHP, Python.

Parametrar

  • SHOPENTLY_SHOP_URL — Resursens URL.
  • SHOPENTLY_ACCESS_TOKEN — Kortlivad Bearer-token.
  • STOCK_ID — Lagerpostens identifierare.
  • LOCATION_ID — Lagerplatsens identifierare.
  • NEW_ON_HAND — Fysiskt antal i lager.
  • ADJUSTMENT_REASON — Registrerad orsak till ändringen.
  • IDEMPOTENCY_KEY — Unik identifierare för en logisk skrivoperation.

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

Fullständigt exempel

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
}

: "${STOCK_ID:?}" "${LOCATION_ID:?}" "${NEW_ON_HAND:?}" "${ADJUSTMENT_REASON:?}" "${IDEMPOTENCY_KEY:?}"
case "$NEW_ON_HAND" in ''|*[!0-9]*) echo 'Use a nonnegative integer' >&2; exit 1;; esac
path="/api/v1/inventory/stock/$(encode "$STOCK_ID")"
stock=$(api GET "$path")
printf '%s' "$stock" | jq -e --arg id "$LOCATION_ID" 'any(.data.allocations[]; .location_id == $id)' >/dev/null
body=$(printf '%s' "$stock" | jq -c --arg id "$LOCATION_ID" --argjson quantity "$NEW_ON_HAND" --arg reason "$ADJUSTMENT_REASON" '{
    expected_version: .data.version, reason: $reason, locations: [.data.allocations[] |
    {location_id, on_hand: (if .location_id == $id then $quantity else .on_hand end)}]}')
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;
}
$path = '/api/v1/inventory/stock/' . rawurlencode(env('STOCK_ID'));
$stock = api('GET', $path)['data'];
$locationId = env('LOCATION_ID');
if (!in_array($locationId, array_column($stock['allocations'], 'location_id'), true)) throw new RuntimeException('Choose an assigned location');
$quantity = filter_var(env('NEW_ON_HAND'), FILTER_VALIDATE_INT);
if ($quantity === false || $quantity < 0) throw new RuntimeException('Use a nonnegative integer');
$body = ['expected_version' => $stock['version'], 'reason' => env('ADJUSTMENT_REASON'),
    'locations' => array_map(fn($a) => ['location_id' => $a['location_id'],
        'on_hand' => $a['location_id'] === $locationId ? $quantity : $a['on_hand']], $stock['allocations'])];
output(api('PATCH', $path, body: $body, key: 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="")

path = "/api/v1/inventory/stock/" + segment(env("STOCK_ID"))
stock = api("GET", path)["data"]
location_id = env("LOCATION_ID")
if location_id not in {a["location_id"] for a in stock["allocations"]}:
    raise ValueError("Choose a location already assigned to this stock record")
quantity = int(env("NEW_ON_HAND"))
if quantity < 0:
    raise ValueError("NEW_ON_HAND must be nonnegative")
body = {"expected_version": stock["version"], "reason": env("ADJUSTMENT_REASON"),
        "locations": [{"location_id": a["location_id"],
                       "on_hand": quantity if a["location_id"] == location_id else a["on_hand"]}
                      for a in stock["allocations"]]}
print(json.dumps(api("PATCH", path, body=body, key=env("IDEMPOTENCY_KEY"))))

Så fungerar det

Lagerposter skiljer mellan fysisk kvantitet on_hand, orderreservationer, manuella reservationer och antal tillgängliga för försäljning. Använd returnerad lageridentifierare och version. Produktlager och variantlager är separata mål.

sellable_limit med värdet -1 betyder obegränsat, medan 0 betyder att inget får säljas. Obegränsad tillgänglighet visas som null i available. Behandla inte null som noll. Lagerjusteringar, försäljningsgränser och manuella reservationer har separata operationer och behörigheter.