Skip to content

Diagnostic Orders & Fulfillment

Fulfillment fails when variant stock equals one

An order comes in for a variant that has exactly one unit sitting in a warehouse. The checkout succeeds, the order is placed, everything looks normal. Then staff try to fulfill it and Saleor throws back INSUFFICIENT_STOCK, on a unit that is sitting right there on the shelf, reserved for this exact order. Here is why Saleor's own allocation math causes that, and a script that flags the boundary case before anyone calls orderFulfill.

Python and Node.js Saleor GraphQL API Safe by default (dry run, guarded repair)
Empty warehouse shelves
Photo by lim woojung on Unsplash
The short answer

Saleor allocates stock at order placement time, not at fulfillment time. The moment a customer checks out, the requested quantity is written straight into Stock.quantityAllocated, and available stock is computed as quantity - quantityAllocated. For a variant whose warehouse row has quantity equal to one, that single unit becomes fully allocated the instant the order is created. So when staff run orderFulfill, the mutation's own re-check sees available = 1 - 1 = 0 and rejects it with INSUFFICIENT_STOCK, even though that one unit was reserved for this exact order. This is a documented boundary condition, tracked as far back as Saleor issue #6136, and it is exactly why orderFulfill exposes an allowStockToBeExceeded escape hatch. A Python or Node.js script below detects the boundary case, confirms it is a false positive rather than a genuine shortage, and drives a dry-run-first guarded repair.

The problem in plain words

Most people assume Saleor checks stock twice: once when the order is placed, and again when it is fulfilled, comparing the same numbers each time. It does not quite work that way. Saleor commits the reservation once, at order placement, by writing the ordered quantity into Stock.quantityAllocated. From that moment on, "available" for that stock row is just quantity - quantityAllocated, and the order's own hold is baked into that number.

That is fine for a variant with ten units in stock and one allocated. Nine still read as available and nobody notices. But when a variant's warehouse stock is exactly one, and that one unit gets allocated to the order that just bought it, the available figure for that stock row drops to zero the instant the order exists. When staff later run orderFulfill, the mutation re-checks the same stock row, sees zero available, and refuses, quoting INSUFFICIENT_STOCK on an order that is not actually oversold. The unit was never double-booked. It is just fully spoken for by the one order trying to claim it.

Stock.quantity = 1 one unit in warehouse Order placed quantityAllocated set to 1 available reads 1 - 1 = 0 Stock looks empty to Saleor's own check orderFulfill rejected INSUFFICIENT_STOCK on the reserved unit Meanwhile, on the shelf the one unit is sitting right there, reserved for this order. Nothing else claimed it.
The one unit was never double sold. It is Saleor's own allocation, from this order, that makes the fulfillment re-check read zero.

Why it happens

This is a boundary condition in how allocation and availability are computed, not a data corruption bug. A few concrete ways it shows up:

This confusion is easy to have, because the error code, INSUFFICIENT_STOCK, sounds like it means "there really is not enough stock." In the general case it does. But the fulfillment mutation cannot tell the difference between "this order's own allocation makes the row read zero" and "somebody else also holds an allocation against the same row." Both look identical from inside the check. Related reports, such as saleor/apps issue #1175 on inaccurate insufficient stock errors during checkout, and saleor/saleor issue #543 on concurrent checkouts allocating more stock than available, both trace back to the same allocation model: a write that happens early and is trusted everywhere after, including in places where it should really be read as "already spoken for by us," not "gone."

The key insight

You cannot fix this by adding more stock or by editing Stock.quantity by hand, because the physical count was never wrong. The fix is to recompute availability the way orderFulfill should have for this specific order: subtract out the allocation that this order itself is holding before deciding whether stock is really insufficient. If, after that adjustment, the requested quantity still fits, this is the quantity equals one boundary case, and it is safe to proceed. If it does not fit even after the adjustment, there is a genuine shortage from other orders, and that goes to a human, not to a blind retry.

The fix, as a flow

We do not touch Stock.quantity and we do not force a fulfillment blind. We add a script that reads the order's lines and their variant stocks, runs a pure decision function that excludes this order's own allocation from the availability math, and only for the confirmed boundary case retries orderFulfill with allowStockToBeExceeded: true, behind a dry run flag. Anything that is not the confirmed boundary case is reported to a human instead.

Query order lines variant, stocks per warehouse Dry run orderFulfill capture the error code decideFulfillmentAllowed pure decision function Boundary case? yes no, real shortage Report to human warehouse reconciliation retry orderFulfill allowStockToBeExceeded true, DRY_RUN gated
Only the confirmed boundary case gets the guarded retry. A genuine shortage from other orders is reported for a human to reconcile, never forced.

Build it step by step

1

Get a Saleor auth token

Create a Saleor app token, or sign in with tokenCreate to get a staff JWT, with at least MANAGE_ORDERS so it can read orders and call orderFulfill. Keep the API URL and token in environment variables, never in the file.

setup (shell)
pip install requests

export SALEOR_API_URL="https://your-store.saleor.cloud/graphql/"
export SALEOR_AUTH_TOKEN="your app or staff token"
export DRY_RUN="true"   # start safe, only reports and dry-run-fulfills
setup (shell)
// Node 18+ has fetch built in, no dependencies needed

export SALEOR_API_URL="https://your-store.saleor.cloud/graphql/"
export SALEOR_AUTH_TOKEN="your app or staff token"
export DRY_RUN="true"   // start safe, only reports and dry-run-fulfills
2

Talk to the Saleor GraphQL API

Everything is one endpoint, POST with your token in the Authorization: Bearer header. A small helper sends a query and returns the data, and raises if Saleor reports an error. We reuse this helper for every query and mutation below.

step2.py
import os, requests

API_URL = os.environ["SALEOR_API_URL"]
TOKEN = os.environ["SALEOR_AUTH_TOKEN"]

def gql(query, variables=None):
    r = requests.post(
        API_URL,
        json={"query": query, "variables": variables or {}},
        headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
        timeout=30,
    )
    r.raise_for_status()
    body = r.json()
    if body.get("errors"):
        raise RuntimeError(body["errors"])
    return body["data"]
step2.js
const API_URL = process.env.SALEOR_API_URL;
const TOKEN = process.env.SALEOR_AUTH_TOKEN;

async function gql(query, variables = {}) {
  const res = await fetch(API_URL, {
    method: "POST",
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables }),
  });
  if (!res.ok) throw new Error(`Saleor ${res.status}`);
  const body = await res.json();
  if (body.errors) throw new Error(JSON.stringify(body.errors));
  return body.data;
}
3

Read the order's lines and their variant stocks

Pull the order's unfulfilled lines along with each variant's stock per warehouse, reading quantity and quantityAllocated. This is the raw material the decision needs, since the boundary case only shows up when we can see exactly how much of a one-unit stock row this order itself is holding.

step3.py
ORDER_QUERY = """
query($orderId: ID!) {
  order(id: $orderId) {
    id
    status
    lines {
      id
      quantity
      quantityFulfilled
      variant {
        id
        name
        stocks {
          warehouse { id name }
          quantity
          quantityAllocated
        }
      }
    }
  }
}"""

def get_order(order_id):
    return gql(ORDER_QUERY, {"orderId": order_id})["order"]
step3.js
const ORDER_QUERY = `
query($orderId: ID!) {
  order(id: $orderId) {
    id
    status
    lines {
      id
      quantity
      quantityFulfilled
      variant {
        id
        name
        stocks {
          warehouse { id name }
          quantity
          quantityAllocated
        }
      }
    }
  }
}`;

async function getOrder(orderId) {
  return (await gql(ORDER_QUERY, { orderId })).order;
}
4

Decide, with one pure function

Keep the decision in its own function that takes plain numbers, the stock row, the quantity this order is requesting, and how much this order already holds on that stock, and returns whether the fulfillment should proceed. It excludes this order's own allocation from the availability math, which is exactly the adjustment orderFulfill's own re-check does not make. It never touches the network, which is what makes it easy to test.

decide.py
def decide_fulfillment_allowed(stock, requested_qty, already_allocated_for_this_order):
    """
    Pure decision logic, no I/O.
    stock: {"quantity": int, "quantityAllocated": int}
    requested_qty: int, the amount this order line still needs fulfilled
    already_allocated_for_this_order: int, this order's own hold on this stock row
    Returns {"allowed": bool, "reason": str}.
    """
    true_available = stock["quantity"] - (stock["quantityAllocated"] - already_allocated_for_this_order)

    if requested_qty <= true_available:
        if stock["quantity"] == 1 and already_allocated_for_this_order >= stock["quantityAllocated"]:
            return {
                "allowed": True,
                "reason": "BOUNDARY_CASE: quantity=1 fully allocated to this order, fulfillment should proceed",
            }
        return {"allowed": True, "reason": "OK"}

    return {
        "allowed": False,
        "reason": f"INSUFFICIENT_STOCK: only {true_available} of {requested_qty} requested available",
    }
decide.js
export function decideFulfillmentAllowed(stock, requestedQty, alreadyAllocatedForThisOrder) {
  const trueAvailable = stock.quantity - (stock.quantityAllocated - alreadyAllocatedForThisOrder);

  if (requestedQty <= trueAvailable) {
    if (stock.quantity === 1 && alreadyAllocatedForThisOrder >= stock.quantityAllocated) {
      return {
        allowed: true,
        reason: "BOUNDARY_CASE: quantity=1 fully allocated to this order, fulfillment should proceed",
      };
    }
    return { allowed: true, reason: "OK" };
  }

  return {
    allowed: false,
    reason: `INSUFFICIENT_STOCK: only ${trueAvailable} of ${requestedQty} requested available`,
  };
}
5

Dry-run orderFulfill first to confirm the signal

Before trusting the pure function alone, call orderFulfill with allowStockToBeExceeded: false and read errors[].code. Seeing INSUFFICIENT_STOCK come back on a line whose stock quantity is exactly one is the definitive signal, matching what the pure function already computed. Only after that double confirmation do we consider the guarded retry.

step5.py
FULFILL_MUTATION = """
mutation($orderId: ID!, $lines: [OrderFulfillLineInput!]!, $allowExceed: Boolean!) {
  orderFulfill(
    order: $orderId,
    input: { linesInput: $lines, allowStockToBeExceeded: $allowExceed }
  ) {
    fulfillments { id status }
    errors { field code message }
  }
}"""

def attempt_fulfill(order_id, lines_input, allow_exceed):
    result = gql(
        FULFILL_MUTATION,
        {"orderId": order_id, "lines": lines_input, "allowExceed": allow_exceed},
    )["orderFulfill"]
    return result
step5.js
const FULFILL_MUTATION = `
mutation($orderId: ID!, $lines: [OrderFulfillLineInput!]!, $allowExceed: Boolean!) {
  orderFulfill(
    order: $orderId,
    input: { linesInput: $lines, allowStockToBeExceeded: $allowExceed }
  ) {
    fulfillments { id status }
    errors { field code message }
  }
}`;

async function attemptFulfill(orderId, linesInput, allowExceed) {
  return (
    await gql(FULFILL_MUTATION, { orderId, lines: linesInput, allowExceed })
  ).orderFulfill;
}
6

Wire it together with a dry run guard

The loop reads the order, checks each line's stock against the pure decision function, and dry-run-fulfills with allowStockToBeExceeded: false to see whether Saleor's own re-check agrees this is INSUFFICIENT_STOCK. When both signals confirm the quantity equals one boundary case, and DRY_RUN is turned off, the script retries with allowStockToBeExceeded: true so the reserved unit actually ships. If the pure function does not confirm the boundary case, meaning true available stock is genuinely short even after excluding this order's own hold, the script stops and reports the SKU instead of forcing anything.

Run it safe

Never set allowStockToBeExceeded: true as a blanket default. Only retry with it when decide_fulfillment_allowed confirms the boundary case for that specific stock row, and always run with DRY_RUN=true first so you can read the intended mutation and variables before anything writes. A false decrement can double-sell later, and a false override can mask a real oversell from another order.

The full code

Here is the complete script in one file for each language. It reads settings from the environment, logs what it finds, respects the dry run flag, and never forces a fulfillment unless the boundary case is confirmed twice, once by the pure function and once by Saleor's own dry-run error response.

Get this script on GitHub Follow @allanninal Python and Node.js, with tests. Dry run by default. One of 51 Saleor fixes, free and open source.
fix_stock_one_fulfillment.py
"""Detect and repair Saleor orders blocked from fulfillment by the
quantity-equals-one stock allocation boundary case.

Saleor allocates stock at order placement, not at fulfillment time. A variant
whose warehouse Stock.quantity is exactly one becomes fully allocated to its
own order the moment that order is placed. When orderFulfill later re-checks
availability as quantity - quantityAllocated, it reads zero and rejects the
fulfillment with INSUFFICIENT_STOCK, even though the unit is reserved for
this exact order.

This script only retries orderFulfill with allowStockToBeExceeded=true when
the pure decision function AND a dry-run orderFulfill call both confirm the
boundary case. Otherwise it reports the SKU for a human to reconcile. Safe
to run again and again.
"""
import os
import logging
import requests

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("fix_stock_one_fulfillment")

API_URL = os.environ["SALEOR_API_URL"]
TOKEN = os.environ["SALEOR_AUTH_TOKEN"]
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"

ORDER_QUERY = """
query($orderId: ID!) {
  order(id: $orderId) {
    id
    status
    lines {
      id
      quantity
      quantityFulfilled
      variant {
        id
        name
        stocks {
          warehouse { id name }
          quantity
          quantityAllocated
        }
      }
    }
  }
}"""

FULFILL_MUTATION = """
mutation($orderId: ID!, $lines: [OrderFulfillLineInput!]!, $allowExceed: Boolean!) {
  orderFulfill(
    order: $orderId,
    input: { linesInput: $lines, allowStockToBeExceeded: $allowExceed }
  ) {
    fulfillments { id status }
    errors { field code message }
  }
}"""


def gql(query, variables=None):
    r = requests.post(
        API_URL,
        json={"query": query, "variables": variables or {}},
        headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
        timeout=30,
    )
    r.raise_for_status()
    body = r.json()
    if body.get("errors"):
        raise RuntimeError(body["errors"])
    return body["data"]


def decide_fulfillment_allowed(stock, requested_qty, already_allocated_for_this_order):
    """
    Pure decision logic, no I/O.
    stock: {"quantity": int, "quantityAllocated": int}
    requested_qty: int, the amount this order line still needs fulfilled
    already_allocated_for_this_order: int, this order's own hold on this stock row
    Returns {"allowed": bool, "reason": str}.
    """
    true_available = stock["quantity"] - (stock["quantityAllocated"] - already_allocated_for_this_order)

    if requested_qty <= true_available:
        if stock["quantity"] == 1 and already_allocated_for_this_order >= stock["quantityAllocated"]:
            return {
                "allowed": True,
                "reason": "BOUNDARY_CASE: quantity=1 fully allocated to this order, fulfillment should proceed",
            }
        return {"allowed": True, "reason": "OK"}

    return {
        "allowed": False,
        "reason": f"INSUFFICIENT_STOCK: only {true_available} of {requested_qty} requested available",
    }


def get_order(order_id):
    return gql(ORDER_QUERY, {"orderId": order_id})["order"]


def attempt_fulfill(order_id, lines_input, allow_exceed):
    result = gql(
        FULFILL_MUTATION,
        {"orderId": order_id, "lines": lines_input, "allowExceed": allow_exceed},
    )["orderFulfill"]
    return result


def _line_input(line_id, warehouse_id, qty):
    return {"orderLineId": line_id, "stocks": [{"warehouse": warehouse_id, "quantity": qty}]}


def check_and_repair(order_id):
    order = get_order(order_id)
    flagged = []
    repaired = []

    for line in order["lines"]:
        requested = line["quantity"] - line["quantityFulfilled"]
        if requested <= 0:
            continue

        variant = line["variant"]
        for stock in variant["stocks"]:
            if stock["quantity"] != 1:
                continue

            # This order's own hold on a quantity=1 row is, at most, the
            # requested amount, since nothing else can share a single unit.
            already_allocated = min(requested, stock["quantityAllocated"])
            decision = decide_fulfillment_allowed(
                {"quantity": stock["quantity"], "quantityAllocated": stock["quantityAllocated"]},
                requested,
                already_allocated,
            )

            if not decision["allowed"]:
                log.warning(
                    "Order %s line %s: %s. Reporting for manual reconciliation.",
                    order_id, line["id"], decision["reason"],
                )
                flagged.append({"line_id": line["id"], "reason": decision["reason"]})
                continue

            if "BOUNDARY_CASE" not in decision["reason"]:
                continue

            warehouse_id = stock["warehouse"]["id"]
            lines_input = [_line_input(line["id"], warehouse_id, requested)]

            dry_result = attempt_fulfill(order_id, lines_input, allow_exceed=False)
            codes = [e["code"] for e in dry_result.get("errors", [])]

            if "INSUFFICIENT_STOCK" not in codes:
                log.info("Order %s line %s: no error on dry check, nothing to repair.", order_id, line["id"])
                continue

            log.warning(
                "Order %s line %s confirmed boundary case (quantity=1, fully self-allocated). %s",
                order_id, line["id"], "Would retry with allowStockToBeExceeded=true" if DRY_RUN else "Retrying now",
            )

            if not DRY_RUN:
                real_result = attempt_fulfill(order_id, lines_input, allow_exceed=True)
                if real_result.get("errors"):
                    raise RuntimeError(real_result["errors"])
                repaired.append({"line_id": line["id"], "fulfillments": real_result["fulfillments"]})

    log.info(
        "Done. %d line(s) flagged for manual review, %d line(s) %s.",
        len(flagged), len(repaired), "would be repaired" if DRY_RUN else "repaired",
    )
    return {"flagged": flagged, "repaired": repaired}


def run():
    order_id = os.environ["SALEOR_ORDER_ID"]
    check_and_repair(order_id)


if __name__ == "__main__":
    run()
fix-stock-one-fulfillment.js
/**
 * Detect and repair Saleor orders blocked from fulfillment by the
 * quantity-equals-one stock allocation boundary case.
 *
 * Saleor allocates stock at order placement, not at fulfillment time. A
 * variant whose warehouse Stock.quantity is exactly one becomes fully
 * allocated to its own order the moment that order is placed. When
 * orderFulfill later re-checks availability as quantity - quantityAllocated,
 * it reads zero and rejects the fulfillment with INSUFFICIENT_STOCK, even
 * though the unit is reserved for this exact order.
 *
 * This script only retries orderFulfill with allowStockToBeExceeded=true
 * when the pure decision function AND a dry-run orderFulfill call both
 * confirm the boundary case. Otherwise it reports the SKU for a human to
 * reconcile. Safe to run again and again.
 *
 * Guide: https://www.allanninal.dev/saleor/fulfillment-blocked-stock-equals-one/
 */
import { pathToFileURL } from "node:url";

const API_URL = process.env.SALEOR_API_URL || "https://demo.saleor.io/graphql/";
const TOKEN = process.env.SALEOR_AUTH_TOKEN || "token_dummy";
const DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";

export function decideFulfillmentAllowed(stock, requestedQty, alreadyAllocatedForThisOrder) {
  const trueAvailable = stock.quantity - (stock.quantityAllocated - alreadyAllocatedForThisOrder);

  if (requestedQty <= trueAvailable) {
    if (stock.quantity === 1 && alreadyAllocatedForThisOrder >= stock.quantityAllocated) {
      return {
        allowed: true,
        reason: "BOUNDARY_CASE: quantity=1 fully allocated to this order, fulfillment should proceed",
      };
    }
    return { allowed: true, reason: "OK" };
  }

  return {
    allowed: false,
    reason: `INSUFFICIENT_STOCK: only ${trueAvailable} of ${requestedQty} requested available`,
  };
}

async function gql(query, variables = {}) {
  const res = await fetch(API_URL, {
    method: "POST",
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables }),
  });
  if (!res.ok) throw new Error(`Saleor ${res.status}`);
  const body = await res.json();
  if (body.errors) throw new Error(JSON.stringify(body.errors));
  return body.data;
}

const ORDER_QUERY = `
query($orderId: ID!) {
  order(id: $orderId) {
    id
    status
    lines {
      id
      quantity
      quantityFulfilled
      variant {
        id
        name
        stocks {
          warehouse { id name }
          quantity
          quantityAllocated
        }
      }
    }
  }
}`;

const FULFILL_MUTATION = `
mutation($orderId: ID!, $lines: [OrderFulfillLineInput!]!, $allowExceed: Boolean!) {
  orderFulfill(
    order: $orderId,
    input: { linesInput: $lines, allowStockToBeExceeded: $allowExceed }
  ) {
    fulfillments { id status }
    errors { field code message }
  }
}`;

async function getOrder(orderId) {
  return (await gql(ORDER_QUERY, { orderId })).order;
}

async function attemptFulfill(orderId, linesInput, allowExceed) {
  return (
    await gql(FULFILL_MUTATION, { orderId, lines: linesInput, allowExceed })
  ).orderFulfill;
}

function lineInput(lineId, warehouseId, qty) {
  return { orderLineId: lineId, stocks: [{ warehouse: warehouseId, quantity: qty }] };
}

export async function checkAndRepair(orderId) {
  const order = await getOrder(orderId);
  const flagged = [];
  const repaired = [];

  for (const line of order.lines) {
    const requested = line.quantity - line.quantityFulfilled;
    if (requested <= 0) continue;

    for (const stock of line.variant.stocks) {
      if (stock.quantity !== 1) continue;

      // This order's own hold on a quantity=1 row is, at most, the
      // requested amount, since nothing else can share a single unit.
      const alreadyAllocated = Math.min(requested, stock.quantityAllocated);
      const decision = decideFulfillmentAllowed(
        { quantity: stock.quantity, quantityAllocated: stock.quantityAllocated },
        requested,
        alreadyAllocated
      );

      if (!decision.allowed) {
        console.warn(`Order ${orderId} line ${line.id}: ${decision.reason}. Reporting for manual reconciliation.`);
        flagged.push({ lineId: line.id, reason: decision.reason });
        continue;
      }

      if (!decision.reason.startsWith("BOUNDARY_CASE")) continue;

      const warehouseId = stock.warehouse.id;
      const linesInput = [lineInput(line.id, warehouseId, requested)];

      const dryResult = await attemptFulfill(orderId, linesInput, false);
      const codes = (dryResult.errors || []).map((e) => e.code);

      if (!codes.includes("INSUFFICIENT_STOCK")) {
        console.log(`Order ${orderId} line ${line.id}: no error on dry check, nothing to repair.`);
        continue;
      }

      console.warn(
        `Order ${orderId} line ${line.id} confirmed boundary case (quantity=1, fully self-allocated). ` +
        `${DRY_RUN ? "Would retry with allowStockToBeExceeded=true" : "Retrying now"}`
      );

      if (!DRY_RUN) {
        const realResult = await attemptFulfill(orderId, linesInput, true);
        if (realResult.errors && realResult.errors.length) {
          throw new Error(JSON.stringify(realResult.errors));
        }
        repaired.push({ lineId: line.id, fulfillments: realResult.fulfillments });
      }
    }
  }

  console.log(
    `Done. ${flagged.length} line(s) flagged for manual review, ${repaired.length} line(s) ` +
    `${DRY_RUN ? "would be repaired" : "repaired"}.`
  );
  return { flagged, repaired };
}

export async function run() {
  const orderId = process.env.SALEOR_ORDER_ID;
  await checkAndRepair(orderId);
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
  run().catch((err) => { console.error(err); process.exit(1); });
}

Add a test

The decision rule is the part most worth testing, because it decides whether a script forces a fulfillment. Because we kept decide_fulfillment_allowed pure, the test needs no network and no Saleor store. It just feeds in plain numbers and checks the answer, with table-driven cases for quantity equals one, quantityAllocated at zero and one, and requestedQty equal to one.

test_fulfillment_decision.py
import pytest
from fix_stock_one_fulfillment import decide_fulfillment_allowed


@pytest.mark.parametrize(
    "quantity, quantity_allocated, requested_qty, already_allocated, expect_allowed, reason_prefix",
    [
        # The classic boundary case: 1 unit, fully allocated to this order, requesting 1.
        (1, 1, 1, 1, True, "BOUNDARY_CASE"),
        # Plenty of stock, nothing allocated: a plain OK, not the boundary case.
        (10, 0, 1, 0, True, "OK"),
        # 1 unit, allocated to this order, but another order also holds an allocation
        # (quantityAllocated exceeds what this order holds): genuinely short.
        (1, 2, 1, 1, False, "INSUFFICIENT_STOCK"),
        # 1 unit, not allocated to this order at all, but something else holds it: short.
        (1, 1, 1, 0, False, "INSUFFICIENT_STOCK"),
        # Zero stock outright, nothing allocated to this order: short.
        (0, 0, 1, 0, False, "INSUFFICIENT_STOCK"),
        # 1 unit, fully self-allocated, requesting more than 1: still short.
        (1, 1, 2, 1, False, "INSUFFICIENT_STOCK"),
        # 2 units, 1 allocated to this order, 1 free: OK but not the boundary case
        # (quantity != 1).
        (2, 1, 1, 1, True, "OK"),
    ],
)
def test_decide_fulfillment_allowed(
    quantity, quantity_allocated, requested_qty, already_allocated, expect_allowed, reason_prefix
):
    stock = {"quantity": quantity, "quantityAllocated": quantity_allocated}
    result = decide_fulfillment_allowed(stock, requested_qty, already_allocated)
    assert result["allowed"] is expect_allowed
    assert result["reason"].startswith(reason_prefix)


def test_boundary_case_reason_is_human_readable():
    result = decide_fulfillment_allowed({"quantity": 1, "quantityAllocated": 1}, 1, 1)
    assert result["reason"] == (
        "BOUNDARY_CASE: quantity=1 fully allocated to this order, fulfillment should proceed"
    )


def test_insufficient_stock_reason_reports_true_available():
    result = decide_fulfillment_allowed({"quantity": 1, "quantityAllocated": 2}, 1, 1)
    assert result["reason"] == "INSUFFICIENT_STOCK: only 0 of 1 requested available"
fulfillment-decision.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { decideFulfillmentAllowed } from "./fix-stock-one-fulfillment.js";

const cases = [
  // The classic boundary case: 1 unit, fully allocated to this order, requesting 1.
  { quantity: 1, quantityAllocated: 1, requestedQty: 1, alreadyAllocated: 1, expectAllowed: true, reasonPrefix: "BOUNDARY_CASE" },
  // Plenty of stock, nothing allocated: a plain OK, not the boundary case.
  { quantity: 10, quantityAllocated: 0, requestedQty: 1, alreadyAllocated: 0, expectAllowed: true, reasonPrefix: "OK" },
  // 1 unit, allocated to this order, but another order also holds an allocation: genuinely short.
  { quantity: 1, quantityAllocated: 2, requestedQty: 1, alreadyAllocated: 1, expectAllowed: false, reasonPrefix: "INSUFFICIENT_STOCK" },
  // 1 unit, not allocated to this order at all, but something else holds it: short.
  { quantity: 1, quantityAllocated: 1, requestedQty: 1, alreadyAllocated: 0, expectAllowed: false, reasonPrefix: "INSUFFICIENT_STOCK" },
  // Zero stock outright, nothing allocated to this order: short.
  { quantity: 0, quantityAllocated: 0, requestedQty: 1, alreadyAllocated: 0, expectAllowed: false, reasonPrefix: "INSUFFICIENT_STOCK" },
  // 1 unit, fully self-allocated, requesting more than 1: still short.
  { quantity: 1, quantityAllocated: 1, requestedQty: 2, alreadyAllocated: 1, expectAllowed: false, reasonPrefix: "INSUFFICIENT_STOCK" },
  // 2 units, 1 allocated to this order, 1 free: OK but not the boundary case (quantity != 1).
  { quantity: 2, quantityAllocated: 1, requestedQty: 1, alreadyAllocated: 1, expectAllowed: true, reasonPrefix: "OK" },
];

for (const c of cases) {
  test(`decideFulfillmentAllowed quantity=${c.quantity} allocated=${c.quantityAllocated} requested=${c.requestedQty} selfAllocated=${c.alreadyAllocated}`, () => {
    const stock = { quantity: c.quantity, quantityAllocated: c.quantityAllocated };
    const result = decideFulfillmentAllowed(stock, c.requestedQty, c.alreadyAllocated);
    assert.equal(result.allowed, c.expectAllowed);
    assert.ok(result.reason.startsWith(c.reasonPrefix));
  });
}

test("boundary case reason is human readable", () => {
  const result = decideFulfillmentAllowed({ quantity: 1, quantityAllocated: 1 }, 1, 1);
  assert.equal(
    result.reason,
    "BOUNDARY_CASE: quantity=1 fully allocated to this order, fulfillment should proceed"
  );
});

test("insufficient stock reason reports true available", () => {
  const result = decideFulfillmentAllowed({ quantity: 1, quantityAllocated: 2 }, 1, 1);
  assert.equal(result.reason, "INSUFFICIENT_STOCK: only 0 of 1 requested available");
});

Case studies

Limited edition

The last unit of a drop refused to fulfill

A streetwear brand ran a limited release where several sizes had exactly one unit in the warehouse. The moment each of those sizes sold, staff moved straight to fulfillment, since the drop demanded fast shipping to build hype, and every single one of those orders bounced with INSUFFICIENT_STOCK.

Running the detection script showed every flagged line was the same shape: quantity of one, quantityAllocated of one, and the allocation entirely belonging to the order trying to fulfill it. The dry-run orderFulfill call confirmed the same INSUFFICIENT_STOCK code, so the team let the script retry with allowStockToBeExceeded true, and the drop shipped on time.

Restock race

A single restocked unit sold and shipped within the hour

A tools retailer restocked a discontinued part to a single unit while sourcing more, expecting it to sit for a few days. It sold within minutes, and the warehouse team, working from a same-day pick list, hit the fulfillment rejection before anyone had touched the order.

The pure function and the dry-run check both agreed on the boundary case, and the script's log made it obvious this was not a double sale, just the order's own reservation. It fulfilled cleanly once DRY_RUN was turned off for that one confirmed line, and the part shipped the same day.

What good looks like

After this script runs before a fulfillment attempt, a one-unit variant stops looking like an oversell to your team. The confirmed boundary cases retry cleanly with allowStockToBeExceeded, and anything that is a genuine shortage from another order gets reported instead of forced. Nobody has to guess whether INSUFFICIENT_STOCK means "really out of stock" or "Saleor's own math against itself," because the script already tells them which one it is.

FAQ

Why does Saleor reject orderFulfill with INSUFFICIENT_STOCK when the variant has exactly one unit?

Saleor allocates stock the moment an order is placed, not when it is fulfilled. For a warehouse Stock row with quantity equal to one, that single unit becomes fully written to quantityAllocated as soon as the order is created. When staff later call orderFulfill, the mutation re-checks availability as quantity minus quantityAllocated, which reads one minus one, or zero, so it rejects the fulfillment with INSUFFICIENT_STOCK even though the one unit was reserved for this exact order and should be fulfillable.

Is it safe to just set allowStockToBeExceeded to true on every orderFulfill call?

No. That flag tells Saleor to let the fulfillment go through even if it would oversell, so using it blindly on every order can mask a genuine stock shortage caused by other orders. The safe pattern is to first confirm this is the quantity equals one boundary case, meaning the only allocation against that stock is this order's own line, and only then retry with allowStockToBeExceeded true behind a dry run flag. If other orders also hold allocations against the same stock, stop and send the SKU to a human for warehouse reconciliation.

How do I detect the stock equals one boundary case before calling orderFulfill?

Query the order's lines with their variant stocks, reading quantity and quantityAllocated per warehouse, along with quantity and quantityFulfilled per order line. Compute the true available stock as quantity minus quantityAllocated plus whatever this order already holds. If that true available figure covers the requested quantity, the naive zero reading is the false positive Saleor issue 6136 describes, and the record is safe to retry with allowStockToBeExceeded rather than something a human needs to chase down first.

Related field notes

Citations

On the problem:

  1. Cannot fulfill order when product quantity is 1. github.com/saleor/saleor/issues/6136
  2. Inaccurate Insufficient Stock Error in Checkout Flow: Unable to place multiple order for the same product. github.com/saleor/apps/issues/1175
  3. Concurrent checkouts allocate more stocks to customers than the quantity available. github.com/saleor/saleor/issues/543

On the solution:

  1. Saleor Commerce Documentation: the orderFulfill mutation, including allowStockToBeExceeded. docs.saleor.io/api-reference/orders/mutations/order-fulfill
  2. Saleor Commerce Documentation: Stock Allocation. docs.saleor.io/developer/stock/stock-allocation
  3. Saleor Commerce Documentation: the Stock object, including quantity and quantityAllocated. docs.saleor.io/docs/3.x/api-reference/products/objects/stock

Stuck on a tricky one?

If you have a problem in Saleor checkout, stock, orders, or fulfillment that you would rather hand off, this is the kind of work I do. Message me and we can work through it together.

Contact me on LinkedIn

Did this clear up a fulfillment mystery?

If this saved you from chasing a phantom oversell on your last unit in stock, you can buy me a coffee. It is the best way to keep these field notes free and growing.

Buy me a coffee on Ko-fi

Back to all Saleor field notes