# Stock

> Your granny knitted ten pairs of socks, so you can’t sell eleven. Give the product a `stock` field and Merx does the counting.

## Add a stock field

The [product blueprint](https://merx2.wagnerwagner.de/reference/blueprints/pages/product.md) that comes with Merx already has one. If your products use a blueprint of their own, add [`fields/stock`](https://merx2.wagnerwagner.de/reference/blueprints/fields/stock.md) to it.

```yaml title="/site/blueprints/pages/product.yml"
fields:
  stock: fields/stock
```

An empty field means no limit, so a product you don’t count sells as before.

## What Merx does with it

The cart never holds more of a product than its stock. [`add()`](https://merx2.wagnerwagner.de/reference/classes/cart/add.md) and [`updateItem()`](https://merx2.wagnerwagner.de/reference/classes/cart/update-item.md) throw [`error.merx.cart.stock`](https://merx2.wagnerwagner.de/reference/options/translations/error-merx-cart-stock.md), which reads *“Only 3 of “Knitted socks” left.”*, and [`error.merx.cart.soldOut`](https://merx2.wagnerwagner.de/reference/options/translations/error-merx-cart-soldout.md) once there are none. All lines of a product count together, so a [personalized product](https://merx2.wagnerwagner.de/cookbook/personalized-products.md) with one line per name can’t get past the stock one name at a time.

A cart can sit in the session for days, so the checkout checks again: when the order is [initialized](https://merx2.wagnerwagner.de/reference/classes/merx/initialize-order.md), and once more when the customer comes back from the payment provider, before the payment is captured.

Once the order is written, Merx lowers the stock of every product in it. That includes orders which are paid later, by invoice or SEPA Direct Debit, because the goods are promised from that moment on.

> [!NOTE]
> The stock only goes down while [`production`](https://merx2.wagnerwagner.de/reference/options/config-options/production.md) is `true`, so test orders leave it alone.

## Show what’s left

[`$page->stock()`](https://merx2.wagnerwagner.de/reference/classes/product-page/stock.md) returns the number, or `null` for a product without a limit.

```php title="/site/templates/product.php"
<?php if ($page->stock() !== null && $page->stock() <= 0): ?>
  <p>Sold out</p>
<?php else: ?>
  <form id="product-form" action="<?= url('api/shop/cart') ?>" method="post">
    …
  </form>
<?php endif ?>
```

The cart and the checkout return their errors from the [API](https://merx2.wagnerwagner.de/guide/api/concept.md) as JSON, with status 400 and a translated `message`. The checkout script in [Validate checkout](https://merx2.wagnerwagner.de/guide/getting-started/validate-checkout.md) already shows any error that is not about a field above the submit button.

## When the stock is off

A SEPA debit that bounces or an invoice that is never paid leaves the stock lowered, because Merx counts the order, not the payment. Put it back by hand in the Panel.

Changes an editor has not published yet are a copy of the whole page, stock included. Publishing them after an order came in writes the old stock back, so don’t leave changes to a product you sell lying around.

Two customers who buy the last item at the same moment can both get through, because both pass the check before either order is written. The stock goes to −1 and Merx logs it, rather than refusing an order that is already paid.

> [!CAUTION]
> The last check only works while the payment has not been captured yet. Merx asks Stripe to capture manually, through [`stripe.paymentIntentParameters`](https://merx2.wagnerwagner.de/reference/options/config-options/stripe-payment-intent-parameters.md). If you change `capture_method`, the money is taken before that check, and a product that sold out in the meantime leaves a payment without an order.

## Your own stock source

When the stock lives in an ERP or a warehouse system, override [`stock()`](https://merx2.wagnerwagner.de/reference/classes/product-page/stock.md) and [`reduceStock()`](https://merx2.wagnerwagner.de/reference/classes/product-page/reduce-stock.md) in your own [product model](https://merx2.wagnerwagner.de/guide/getting-started/adding-products.md). Merx asks `stock()` for every check and calls `reduceStock()` once per product of a new order, in production only.
