Skip to main content

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

Order validation functions run at the final step of checkout to verify orders meet your requirements. Use them to enforce purchase limits, validate addresses, check inventory, or implement compliance rules.

How It Works

Function Manifest

{
  "handle": "my-validation-app",
  "name": "Order Validator",
  "version": "1.0.0",
  "functions": {
    "order_validation": {
      "handle": "order-rules",
      "name": "Order Validation Rules",
      "config": {
        "maxItemsPerOrder": 50,
        "maxQuantityPerSku": 10,
        "blockedCountries": ["XX", "YY"]
      }
    }
  }
}

Input Schema

interface OrderValidationInput {
  order: {
    lineItems: LineItem[];
    subtotal: number;
    total: number;
    shippingTotal: number;
    taxTotal: number;
    discountTotal: number;
    currency: string;
    discountCodes: string[];
    note: string;
    attributes: Record<string, string>;
  };
  shippingAddress: Address;
  billingAddress: Address;
  customer: {
    id: string;
    email: string;
    tags: string[];
    ordersCount: number;
    totalSpent: number;
  } | null;
  paymentMethod: {
    type: string;    // "credit_card", "paypal", etc.
    gateway: string;
  };
  shop: {
    id: string;
    name: string;
    currency: string;
  };
}

interface LineItem {
  variantId: string;
  productId: string;
  title: string;
  quantity: number;
  price: number;
  sku: string;
  productType: string;
  vendor: string;
  tags: string[];
  properties: Record<string, string>;
  requiresShipping: boolean;
}

interface Address {
  firstName: string;
  lastName: string;
  address1: string;
  address2: string;
  city: string;
  province: string;
  provinceCode: string;
  country: string;
  countryCode: string;
  zip: string;
  phone: string;
  company: string;
}

Output Schema

interface OrderValidationOutput {
  valid: boolean;
  errors?: ValidationError[];
}

interface ValidationError {
  code: string;       // Machine-readable code
  message: string;    // Human-readable message shown to customer
  field?: string;     // Optional: field that caused error
}

Response Examples

Valid Order

{
  "valid": true
}

Invalid Order

{
  "valid": false,
  "errors": [
    {
      "code": "QUANTITY_LIMIT_EXCEEDED",
      "message": "Maximum 10 units per product. Please reduce the quantity of 'Premium Widget' from 15 to 10.",
      "field": "lineItems"
    },
    {
      "code": "RESTRICTED_DESTINATION",
      "message": "Sorry, we cannot ship to PO Box addresses.",
      "field": "shippingAddress"
    }
  ]
}

Example Implementations

Quantity Limits

function validateOrder(input, config) {
  const errors = [];
  
  // Per-SKU quantity limit
  const maxPerSku = config.maxQuantityPerSku || 10;
  
  for (const item of input.order.lineItems) {
    if (item.quantity > maxPerSku) {
      errors.push({
        code: 'QUANTITY_LIMIT_EXCEEDED',
        message: `Maximum ${maxPerSku} units allowed for "${item.title}". Please reduce quantity from ${item.quantity} to ${maxPerSku}.`,
        field: 'lineItems'
      });
    }
  }
  
  // Total items limit
  const totalItems = input.order.lineItems.reduce(
    (sum, item) => sum + item.quantity, 0
  );
  
  const maxItems = config.maxItemsPerOrder || 50;
  if (totalItems > maxItems) {
    errors.push({
      code: 'ORDER_TOO_LARGE',
      message: `Orders are limited to ${maxItems} items. Your cart has ${totalItems} items.`,
      field: 'lineItems'
    });
  }
  
  return {
    valid: errors.length === 0,
    errors: errors.length > 0 ? errors : undefined
  };
}

Geographic Restrictions

function validateOrder(input, config) {
  const errors = [];
  const { shippingAddress, order } = input;
  
  // Blocked countries
  const blockedCountries = config.blockedCountries || [];
  if (blockedCountries.includes(shippingAddress.countryCode)) {
    errors.push({
      code: 'RESTRICTED_COUNTRY',
      message: `Sorry, we cannot ship to ${shippingAddress.country}.`,
      field: 'shippingAddress.country'
    });
  }
  
  // No PO Box for certain items
  const requiresPhysicalAddress = order.lineItems.some(
    item => item.tags.includes('no-po-box')
  );
  
  if (requiresPhysicalAddress) {
    const isPOBox = /\b(P\.?O\.?\s*BOX|POST\s*OFFICE)\b/i.test(
      shippingAddress.address1 + ' ' + shippingAddress.address2
    );
    
    if (isPOBox) {
      errors.push({
        code: 'PO_BOX_NOT_ALLOWED',
        message: 'Some items in your order cannot be shipped to PO Box addresses. Please enter a physical address.',
        field: 'shippingAddress.address1'
      });
    }
  }
  
  // State restrictions (e.g., for alcohol)
  const hasAlcohol = order.lineItems.some(
    item => item.productType === 'alcohol'
  );
  
  const dryStates = ['UT', 'MS'];  // Example: restricted states
  if (hasAlcohol && dryStates.includes(shippingAddress.provinceCode)) {
    errors.push({
      code: 'PRODUCT_RESTRICTED_IN_STATE',
      message: `Alcohol cannot be shipped to ${shippingAddress.province}. Please remove alcohol items or use a different shipping address.`,
      field: 'lineItems'
    });
  }
  
  return {
    valid: errors.length === 0,
    errors: errors.length > 0 ? errors : undefined
  };
}

Customer Verification

function validateOrder(input, config) {
  const errors = [];
  const { customer, order } = input;
  
  // Require account for high-value orders
  if (order.total > 50000 && !customer) {  // $500+
    errors.push({
      code: 'ACCOUNT_REQUIRED',
      message: 'Orders over $500 require an account. Please log in or create an account to continue.',
      field: 'customer'
    });
  }
  
  // Block flagged customers
  if (customer?.tags.includes('blocked')) {
    errors.push({
      code: 'CUSTOMER_BLOCKED',
      message: 'Your account has been restricted. Please contact support.',
      field: 'customer'
    });
  }
  
  // Limit for new customers (fraud prevention)
  if (customer && customer.ordersCount === 0 && order.total > 30000) {
    errors.push({
      code: 'NEW_CUSTOMER_LIMIT',
      message: 'First orders are limited to $300. Place a smaller order first.',
      field: 'order.total'
    });
  }
  
  return {
    valid: errors.length === 0,
    errors: errors.length > 0 ? errors : undefined
  };
}

Product Combination Rules

function validateOrder(input, config) {
  const errors = [];
  const { order } = input;
  
  // Check for incompatible products
  const productTypes = order.lineItems.map(item => item.productType);
  
  const hasFlammable = productTypes.includes('flammable');
  const hasOxidizer = productTypes.includes('oxidizer');
  
  if (hasFlammable && hasOxidizer) {
    errors.push({
      code: 'INCOMPATIBLE_PRODUCTS',
      message: 'Flammable and oxidizer products cannot be shipped together. Please place separate orders.',
      field: 'lineItems'
    });
  }
  
  // Check required accessories
  const mainProducts = order.lineItems.filter(
    item => item.tags.includes('requires-accessory')
  );
  
  const accessories = order.lineItems.filter(
    item => item.tags.includes('accessory')
  );
  
  for (const product of mainProducts) {
    const requiredAccessory = product.properties?.required_accessory;
    const hasRequired = accessories.some(
      acc => acc.sku === requiredAccessory
    );
    
    if (!hasRequired) {
      errors.push({
        code: 'MISSING_ACCESSORY',
        message: `"${product.title}" requires an accessory. Please add the required item to your cart.`,
        field: 'lineItems'
      });
    }
  }
  
  return {
    valid: errors.length === 0,
    errors: errors.length > 0 ? errors : undefined
  };
}

Payment Method Validation

function validateOrder(input, config) {
  const errors = [];
  const { paymentMethod, order, customer } = input;
  
  // COD limits
  if (paymentMethod.type === 'cod') {
    // Max COD amount
    if (order.total > 10000) {  // $100
      errors.push({
        code: 'COD_LIMIT_EXCEEDED',
        message: 'Cash on Delivery is limited to orders under $100. Please choose a different payment method.',
        field: 'paymentMethod'
      });
    }
    
    // Require verified customer for COD
    if (!customer || !customer.tags.includes('verified')) {
      errors.push({
        code: 'COD_VERIFICATION_REQUIRED',
        message: 'Cash on Delivery requires a verified account. Please verify your phone number or use a different payment method.',
        field: 'customer'
      });
    }
  }
  
  // Subscription requires saved payment
  const hasSubscription = order.lineItems.some(
    item => item.properties?.subscription_id
  );
  
  if (hasSubscription && paymentMethod.type !== 'credit_card') {
    errors.push({
      code: 'SUBSCRIPTION_PAYMENT_REQUIRED',
      message: 'Subscriptions require a credit card for recurring billing.',
      field: 'paymentMethod'
    });
  }
  
  return {
    valid: errors.length === 0,
    errors: errors.length > 0 ? errors : undefined
  };
}

Compliance Validation

function validateOrder(input, config) {
  const errors = [];
  const { order, customer, shippingAddress } = input;
  
  // Age-restricted products
  const ageRestrictedItems = order.lineItems.filter(
    item => item.tags.includes('age-restricted')
  );
  
  if (ageRestrictedItems.length > 0) {
    // Check for age verification
    if (!customer?.tags.includes('age-verified')) {
      errors.push({
        code: 'AGE_VERIFICATION_REQUIRED',
        message: 'Your order contains age-restricted items. Please verify your age before purchasing.',
        field: 'customer'
      });
    }
  }
  
  // Export compliance
  const controlledItems = order.lineItems.filter(
    item => item.tags.includes('export-controlled')
  );
  
  if (controlledItems.length > 0) {
    const domesticCountries = ['US'];
    if (!domesticCountries.includes(shippingAddress.countryCode)) {
      errors.push({
        code: 'EXPORT_RESTRICTED',
        message: 'Some items in your order cannot be exported. Please remove restricted items or ship to a domestic address.',
        field: 'lineItems'
      });
    }
  }
  
  // Reseller check
  if (order.total > 200000) {  // $2000+
    const hasResellerAgreement = customer?.tags.includes('reseller-approved');
    if (!hasResellerAgreement) {
      errors.push({
        code: 'RESELLER_AGREEMENT_REQUIRED',
        message: 'Bulk orders over $2000 require a reseller agreement. Please contact our sales team.',
        field: 'order.total'
      });
    }
  }
  
  return {
    valid: errors.length === 0,
    errors: errors.length > 0 ? errors : undefined
  };
}

Error Display

When validation fails, errors appear at checkout:
Unable to complete your order
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

⚠ Maximum 10 units allowed for "Premium Widget".
  Please reduce quantity from 15 to 10.

⚠ Some items in your order cannot be shipped
  to PO Box addresses. Please enter a physical
  address.

[Update Cart]  [Change Address]

Multiple Functions

If multiple apps have order validation functions, they all run. An order is blocked if any function returns valid: false:

Error Codes

Use clear, specific error codes:
CodeMeaning
QUANTITY_LIMIT_EXCEEDEDItem quantity too high
ORDER_TOO_LARGETotal items exceed limit
RESTRICTED_COUNTRYCannot ship to country
PO_BOX_NOT_ALLOWEDPhysical address required
CUSTOMER_BLOCKEDAccount restricted
AGE_VERIFICATION_REQUIREDAge check needed
PAYMENT_METHOD_INVALIDWrong payment for order type
MISSING_ACCESSORYRequired product not in cart

Best Practices

Tell customers exactly what’s wrong and how to fix it. “Maximum 10 units for Widget” is better than “Quantity invalid”.
Some validations can happen in cart transforms or checkout UI, giving earlier feedback.
If you check quantity limits in cart transform, don’t duplicate in order validation.
Guest checkouts won’t have customer data. Handle null customer gracefully.
Test with various addresses, payment methods, and product combinations.
Track why orders fail validation to identify patterns or bugs.

Performance

Order validation must complete within:
  • Timeout: 2 seconds
  • Memory: 128 MB
If your function times out or errors, the order will be blocked with a generic error message. Ensure fast, reliable execution.

See Also