> ## Documentation Index
> Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Order Validation Functions

> Block order placement based on cart, customer, or destination rules

# Order Validation Functions

An `order_validation` function runs at **both** cart verification and
order placement. It fires during cart verification (with `step: 'cart'`)
so your rules can surface in the cart/checkout preview, and again as the
final gate at order placement (with `step: 'review'`). Return one or more
errors and the order is rejected with a `400 Bad Request`. Return nothing
and the order proceeds.

Use it for purchase limits, geographic blocks, age/compliance gates,
required-accessory rules, or anything that needs to block the order
itself — not just hide a payment method or warn the customer in the
cart.

## How It Works

```mermaid theme={null}
sequenceDiagram
    participant Customer
    participant Checkout
    participant Backend
    participant YourFunction

    Customer->>Checkout: Place Order
    Checkout->>Backend: Place order
    Backend->>YourFunction: cart + customer + shippingAddress
    alt No errors returned
        YourFunction->>Backend: { } (or no errors)
        Backend->>Checkout: Order created
    else Errors returned
        YourFunction->>Backend: { errors: [...] }
        Backend->>Checkout: 400 with first error as headline + full list in `data.errors`
    end
```

This function runs at cart verification (`step: 'cart'`) **and** at the
order-place endpoint (`step: 'review'`), so your rules can be reflected in
the cart/checkout preview as well as enforced as the final gate when the
customer presses "Place order".

## Function Manifest

```json theme={null}
{
  "handle": "my-validation-app",
  "name": "Order Validator",
  "version": "1.0.0",
  "functions": {
    "order_validation": {
      "handle": "order-rules",
      "name": "Order Validation Rules",
      "config": {
        "maxQuantityPerLine": 10,
        "blockedCountries": ["XX", "YY"]
      }
    }
  }
}
```

## Input Schema

```typescript theme={null}
interface OrderValidationInput {
  cart: {
    items: CartItem[];
    totalPrice: number;       // Subtotal in display currency
    itemCount: number;
    currency: string;
  };
  customer?: {
    id: string;
    email: string;
  };
  shippingAddress?: {      // optional — absent on some dispatches
    firstName: string;
    lastName: string;
    address1: string;
    address2: string;
    city: string;
    province: string;
    country: string;
    zip: string;
    phone: string;
  };
  step?: 'cart' | 'shipping' | 'payment' | 'review';
  //  Which dispatch point this call is for: 'cart' at cart verification,
  //  'review' at order placement. Defaults to 'review' when absent.
}

interface CartItem {
  id: string;                 // lineItemId
  variantId: string;
  productId: string;
  title: string;
  quantity: number;
  price: number;              // Effective unit price (display currency)
  originalPrice: number;
}
```

<Note>
  The input is intentionally minimal. There is no `paymentMethod`, no
  `shop`, no `tags`/`productType`/`vendor` on items, and `customer`
  carries only `id` and `email`. If you need richer data, fetch it from
  your own backend keyed on the customer/order ids you receive here.
</Note>

## Output Schema

```typescript theme={null}
interface OrderValidationOutput {
  errors?: ValidationError[];
}

interface ValidationError {
  message: string;          // Required — shown to the customer
  target: 'cart' | 'line' | 'line_item' | 'shipping' | 'customer';
  //  REQUIRED. An error whose `target` isn't one of these values fails
  //  output validation, so the whole result is dropped and the order is
  //  allowed through. (`line_item` is an alias for `line`.)
  lineId?: string;          // CartItem.id when target='line' / 'line_item'
  code?: string;            // Machine-readable code for analytics
}
```

The backend treats `errors.length > 0` as **invalid**. There is no
`valid: true|false` boolean — an empty `errors` array (or an absent
field) means the order is allowed.

When multiple errors are returned, the **first** message becomes the
HTTP 400 headline. The full list is attached to the response as
`data.errors` so the frontend can render per-line/per-field callouts.

```http theme={null}
HTTP/1.1 400 Bad Request

{
  "status": "error",
  "message": "Maximum 10 units allowed for \"Premium Widget\". Reduce quantity from 15.",
  "data": {
    "errors": [
      {
        "message": "Maximum 10 units allowed for \"Premium Widget\". Reduce quantity from 15.",
        "target": "line",
        "lineId": "line_42",
        "code": "QUANTITY_LIMIT_EXCEEDED",
        "appId": "my-validation-app"
      }
    ]
  }
}
```

The platform automatically stamps each error with the originating
`appId` so the frontend can attribute blame.

## Examples

### Per-line quantity cap

```js theme={null}
function validateOrder(input, config) {
  const max = config.maxQuantityPerLine || 10;
  const errors = input.cart.items
    .filter(i => i.quantity > max)
    .map(i => ({
      message: `Maximum ${max} units allowed for "${i.title}". Reduce from ${i.quantity}.`,
      target: 'line',
      lineId: i.id,
      code: 'QUANTITY_LIMIT_EXCEEDED'
    }));
  return { errors };
}
```

### Geographic block

```js theme={null}
function validateOrder(input, config) {
  const blocked = config.blockedCountries || [];
  if (blocked.includes(input.shippingAddress.country)) {
    return {
      errors: [{
        message: `We can't ship to ${input.shippingAddress.country}.`,
        target: 'shipping',
        code: 'RESTRICTED_COUNTRY'
      }]
    };
  }
  return { errors: [] };
}
```

### Guest checkout limit

```js theme={null}
function validateOrder(input) {
  if (!input.customer && input.cart.totalPrice > 500) {
    return {
      errors: [{
        message: 'Orders over ₹500 require an account. Please sign in to continue.',
        target: 'customer',
        code: 'ACCOUNT_REQUIRED'
      }]
    };
  }
  return { errors: [] };
}
```

### Required-accessory check

```js theme={null}
function validateOrder(input) {
  const errors = [];
  const ids = new Set(input.cart.items.map(i => i.productId));

  // Console controller (prod_console) needs at least one cable (prod_cable_a/b)
  if (ids.has('prod_console') && !ids.has('prod_cable_a') && !ids.has('prod_cable_b')) {
    errors.push({
      message: 'Add a USB-C cable to your cart to use the console.',
      target: 'cart',
      code: 'MISSING_ACCESSORY'
    });
  }
  return { errors };
}
```

## How errors render in the UI

The checkout converts the 400 into an inline error banner above
the place-order button. When `target='line'` and `lineId` matches a
cart line, the checkout can additionally highlight that specific
line (subject to theme support).

```
⚠ Maximum 10 units allowed for "Premium Widget". Reduce from 15.

[Edit cart] [Continue editing]
```

## Multiple apps

All installed validation functions run in install order. Errors from
every function are concatenated, and the order is blocked if **any**
function returns at least one error.

## Best Practices

<AccordionGroup>
  <Accordion title="Validate early when possible">
    A cart-transform or storefront-side check that prevents the bad
    state at all is a better customer experience than a 400 after
    they pressed "Place order". Use order\_validation as the
    server-side gate, not the primary signal.
  </Accordion>

  <Accordion title="Write actionable messages">
    "Reduce quantity from 15 to 10" beats "Quantity invalid". Tell
    the customer exactly what to change.
  </Accordion>

  <Accordion title="Use `target` + `lineId`">
    Themes that support per-line error rendering rely on these
    fields. Plain string messages still work, but lose the
    inline-highlight UX.
  </Accordion>

  <Accordion title="Handle missing customer">
    Guest checkouts arrive with `customer === undefined`. Don't
    crash — return `{ errors: [] }` if your rule doesn't apply.
  </Accordion>

  <Accordion title="Keep functions fast">
    The order-place flow blocks on this dispatch. A slow validator
    means a slow checkout. If you need to call an external API,
    cache aggressively and short-circuit on identical inputs.
  </Accordion>
</AccordionGroup>

## See Also

* [Cart Transform Functions](/functions/cart-transform) — modify or
  remove lines instead of blocking the order.
* [Payment Customization Functions](/functions/payment-customization) —
  hide a payment method rather than reject the whole order.
* [Discount Functions](/functions/discount) — apply or withhold a
  discount based on cart state.
