How to Build a Custom Pre-Order Module in Magento 2

In Magento 2, pre-orders can be a powerful way to gauge demand, smooth out cash flow, and launch new products when your customers are ready. This guide walks you through building a custom pre-order module from scratch, with a relaxed, colleague-to-colleague tone and concrete code examples. We’ll cover architecture, database design, payment integration, admin workflows, customer notifications, and performance best practices. The goal is to give you a practical blueprint you can adapt to your store without relying on a paid extension, while keeping the code approachable for a neophyte developer.

Why a Custom Pre-Order Module in Magento 2?

Pre-orders let you offer products before they are in stock, manage waitlists, and capture customer interest early. A custom solution gives you: - Full control over when pre-orders start and end, and how they convert to real orders. - The ability to keep your main catalog undisturbed, ensuring stock levels aren’t affected until you actually fulfill an order. - The chance to tailor notifications and admin workflows to your business rules. - A learning path for your team: you’ll understand data flows, payments, and Magento internals better than with a black-box extension.

High-Level Architecture: How to Structure Pre-Orders Without Hurting Stock

The core idea is to keep pre-orders in a separate data structure from the main stock. Instead of decrementing inventory when a customer adds a pre-order item to the cart, you store a pre-order record and only adjust stock when the pre-order transitions to a confirmed order at the capture moment. This approach reduces risk of stock inconsistencies and makes reporting clearer.

Key components we’ll design:

  • Pre-order data model to track who reserved what and when it becomes available.
  • Link between pre-orders and actual orders so you can trace conversions and handle refunds cleanly.
  • Payment authorization vs. capture to secure funds without finally charging customers before fulfillment.

We’ll also discuss how to keep the main catalog performant, with careful queries and indexing so the pre-order logic doesn’t slow down product browsing for normal customers.

Database Design: The Pre-Order Tables

First, create a dedicated table to store pre-order data. Below is a pragmatic SQL sketch you can adapt to your environment. This design avoids touching inventory counts until a pre-order becomes an actual order.

-- Pre-order table (SKU-based, multi-store friendly)
CREATE TABLE IF NOT EXISTS mage_pre_order (
  pre_order_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  product_id INT NOT NULL,
  sku VARCHAR(64) NOT NULL,
  qty INT NOT NULL,
  customer_id BIGINT DEFAULT NULL,
  reserved_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  available_from DATETIME NULL,
  status ENUM('PENDING','ACTIVE','FULFILLED','CANCELLED') NOT NULL DEFAULT 'PENDING',
  order_id BIGINT DEFAULT NULL,
  PRIMARY KEY (pre_order_id),
  KEY idx_product_id (product_id),
  KEY idx_status (status),
  KEY idx_available_from (available_from)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Notes: - We store product_id (and optionally sku) to tie the pre-order to a product and its stock-keeping unit. - available_from lets you auto-launch pre-orders after a date, or trigger a notification window. - status helps you distinguish between awaiting fulfillment and closed cases.

Optionally, you can create a companion table to map pre-orders to orders when fulfillment happens, for cleaner reportage:

CREATE TABLE IF NOT EXISTS mage_pre_order_mapping (
  map_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  pre_order_id BIGINT NOT NULL,
  order_id BIGINT NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (map_id),
  KEY idx_pre_order (pre_order_id),
  KEY idx_order (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

In Magento, you’ll typically place these in a module’s setup script. The exact file structure depends on your Magento version, but the logic remains the same: create tables during module install/upgrade, and reference them from your models/repositories.

Step-by-Step Setup: Create the Module Skeleton

Let’s create a minimal Magento 2 module named Magefine_PreOrder. We’ll go through registration, module.xml, and a basic setup script to create our mage_pre_order table. This is intentionally approachable for a beginner, but robust enough to evolve with your needs.

// app/code/Magefine/PreOrder/registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magefine_PreOrder', __DIR__);

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
  <module name="Magefine_PreOrder" setup_version="0.1.0">
  </module>
</config>

Next, create a basic InstallSchema to create mage_pre_order:

// app/code/Magefine/PreOrder/Setup/InstallSchema.php
namespace Magefine\PreOrder\Setup;

use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\DB\Ddl\Table;

class InstallSchema implements InstallSchemaInterface
{
    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        $installer = $setup;
        $installer->startSetup();

        if (!$installer->tableExists('mage_pre_order')) {
            $table = $installer->getConnection()->newTable(
                $installer->getTable('mage_pre_order')
            )
            ->addColumn('pre_order_id', Table::TYPE_BIGINT, null, ['identity' => true, 'unsigned' => true, 'nullable' => false], 'Pre-Order ID')
            ->addColumn('product_id', Table::TYPE_INTEGER, null, ['nullable' => false], 'Product ID')
            ->addColumn('qty', Table::TYPE_INTEGER, null, ['nullable' => false], 'Quantity')
            ->addColumn('customer_id', Table::TYPE_BIGINT, null, ['nullable' => true], 'Customer ID')
            ->addColumn('reserved_at', Table::TYPE_DATETIME, null, ['nullable' => false], 'Reserved At')
            ->addColumn('available_from', Table::TYPE_DATETIME, null, ['nullable' => true], 'Available From')
            ->addColumn('status', Table::TYPE_TEXT, 20, [], 'Status')
            ->addColumn('order_id', Table::TYPE_BIGINT, null, ['nullable' => true], 'Linked Order ID')
            ->setComment('Magefine Pre-Order Table')
            ->setOption('type', 'InnoDB');

            $installer->getConnection()->createTable($table);
        }

        $installer->endSetup();
    }
}

Finally, register the Setup script in di.xml if needed by your Magento version. This skeleton gives you a working database layer to start experimenting with pre-orders.

Checkout and Catalog: How to Handle Pre-Orders Without Touching Stock

The heart of the implementation is to detect pre-order products at checkout and avoid decrementing the main stock until the order is captured. We’ll outline a clean approach using an is_pre_order flag on the product and a small observer to intercept the add-to-cart process.

Assumptions:

  • Pre-order products have a custom attribute pre_order (boolean) or a product type extension flag.
  • When a cart contains a pre-order item, we flag the quote item with is_pre_order = true.
  • We do not reduce stock for pre-orders; stock is adjusted when the order is captured.

Code sketch: detect pre-order during quote collection and skip stock deduction:

// Observer: Magefine\PreOrder\Observer\AddToCartPreOrder.php
namespace Magefine\PreOrder\Observer;

use Magento\Framework\Event\ObserverInterface;
use Magento\Checkout\Model\Cart;

class AddToCartPreOrder implements ObserverInterface
{
    public function execute(\Magento\Framework\Event\Observer $observer)
    {
        $quoteItem = $observer->getEvent()->getQuoteItem();
        $product = $quoteItem->getProduct();
        if (method_exists($product, 'getData') && $product->getData('pre_order')) {
            $quoteItem->setData('is_pre_order', true);
        }
    }
}

Then, ensure Magento’s stock deduction logic respects is_pre_order. You may override the stock management flow or implement a plugin on the stock inventory module to skip deduction for items where is_pre_order is true. The exact approach depends on your Magento version and whether you use MSI (Multi-Source Inventory). A practical starting point is to wrap the stock update in a conditional that checks the quote item flag.

Per-quote totals and taxes should still be calculated normally for pre-orders, so customers see accurate pricing and tax on checkout even though stock remains unchanged until capture.

Payment Integration: Temporary Authorizations and Deferred Capture

One of the trickiest parts is handling money without charging customers before fulfillment. The standard approach is to place a temporary authorization for the order total and perform capture only when the pre-order converts to a real order. Magento supports authorization at checkout with many payment methods, but you typically need to add a few guardrails for pre-orders:

  • Authorize the amount at checkout for pre-order items.
  • Store the authorization id alongside the pre-order record.

High-level steps:

  1. Detect pre-order in the quote during the payment step.
  2. Call the payment method’s authorize() for the order total.
  3. Save the authorization ID and mark the pre-order row as PENDING.
  4. On the actual order capture date (or when stock becomes available), perform the capture automatically and associate the captured amount with the pre-order row.

Code sketch: a minimal payment helper that triggers authorization for pre-orders:

// Magefine\PreOrder\Model\Payment\PreOrderAuthorization.php
namespace Magefine\PreOrder\Model\Payment;

class PreOrderAuthorization
{
    public function authorize($payment, $amount, $preOrderId)
    {
        // Pseudo-code: call Magento payment interface
        // This will depend on your payment method, e.g., authorize() call
        $result = $payment->authorize($amount);
        if ($result->isSuccessful()) {
            // Save authorization id against pre-order
            // e.g., save $result->getAuthorizationId() in mage_pre_order
        }
        return $result;
    }
}

Capture workflow:

// Capture when pre-order becomes a real order
$authorizationId = $preOrder->getAuthorizationId();
$payment = $order->getPayment();
$payment->capture($authorizationId, $preOrder->getTotal());
// Bridge cancellation/partial captures as needed

Important notes:

  • Support for authorization and capture depends on the payment gateway and Magento version. Always test in a sandbox environment.
  • Handle edge cases: partial captures, card expiration, refunds on cancellations, and failed authorizations.

Admin Workflows: Date Control and Availability Thresholds

A good admin interface lets your team manage pre-orders without wrestling with spreadsheets. Build a dedicated section under Admin > Magefine Pre-Order to view, edit, and schedule pre-orders. Core features to implement:

  • List of pre-order items by product with status and available_from date.
  • Bulk actions to activate/deactivate pre-orders and adjust thresholds.
  • Automated triggers when a pre-order becomes available or when inventory reaches the threshold.

UI sketch (adminhtml): a grid for pre-orders and a detail form for thresholds:

// Admin grid and form via UI components would go here. This is a scaffold.
// Magefine\PreOrder\Controller\Adminhtml\PreOrder\Index::execute()
// Magefine\PreOrder\Block\Adminhtml\PreOrder\Grid

Notifications: Automatic Alerts When Pre-Orders Move to Real Orders

Customer communication matters. Set up automatic emails or in-app notifications when pre-orders are fulfilled and converted into real orders. Typical flows:

  • Reminder emails a few days before the available_from date.
  • Notification on the day of availability with a link to track the order.
  • Post-purchase confirmation once the pre-order is converted to a standard order in the system.

Implementation approach:

  • Event-driven: listen for changes to mage_pre_order.available_from and status transitions to ACTIVE.
  • Mailer integration: utilize Magento mail templates or a custom template stored in the database.

Code sketch: a simple observer to send a notification when a pre-order becomes ACTIVE:

// Magefine\PreOrder\Observer\PreOrderActivated.php
namespace Magefine\PreOrder\Observer;

use Magento\Framework\Event\ObserverInterface;

class PreOrderActivated implements ObserverInterface
{
    public function execute(\Magento\Framework\Event\Observer $observer)
    {
        $preOrder = $observer->getEvent()->getData('pre_order');
        // send email - pseudo code
        // $this->emailService->sendPreOrderAvailableNotification($preOrder);
    }
}

Test scenarios: ensure you cover successful conversions, failed authorizations, and partial shipments. Verify your email templates render correctly in multiple languages if you serve a global audience.

Performance Best Practices: Keep the Pre-Order Layer Lightweight

A big risk with any new feature is slowing down the catalog. Here are practical patterns to minimize performance impact:

  • Index pre-order queries carefully, especially on product_id, status, and available_from.
  • Use read replicas or Magento’s indexers to keep pre-order queries isolated from the product catalog fetch path.
  • Cache results for frequently queried pre-order counts per product, with invalidation on status changes.
  • Minimize joins in the main catalog queries; pull pre-order data only when displaying a pre-order-enabled product.

Code tip: a lean query to fetch pre-order counts for a set of products without touching the main stock table:

// Pseudo repository method to fetch pre-order counts for a batch of product IDs
public function getPreOrderCounts(array $productIds) {
    $db = $this->getConnection();
    $select = $db->select()
        ->from(['po' => 'mage_pre_order'], ['product_id', 'COUNT(*) AS pre_order_count'])
        ->where('product_id IN (?)', $productIds)
        ->group('product_id');
    return $db->fetchAll($select);
}

Cache strategy suggestion: - Use tag-based caching with a short TTL for pre-order related data. - Invalidate caches when a pre-order status changes or when an available_from date triggers a transition to ACTIVE.

End-to-End Implementation Checklist

  1. Define the data model for pre-orders (mage_pre_order) and the optional mapping table.
  2. Create a Magento 2 module skeleton and register the module.
  3. Set up InstallSchema to create pre-order tables.
  4. Detect pre-order items during cart/checkout and avoid stock deductions.
  5. Implement temporary payment authorization at checkout and a deferred capture plan.
  6. Build an admin UI to manage pre-order windows and thresholds.
  7. Configure automated customer notifications for pre-order progression.
  8. Apply performance optimizations to protect the main catalog.
  9. Test end-to-end in a staging environment with multiple scenarios.

With this structure in place, you’ll have a solid foundation to iterate on UX, reporting, and automation as your business grows.

Testing Strategy: How to Validate Your Pre-Order Module

Testing is crucial. Create test cases for common paths and edge cases:

  • Single pre-order item with an available_from date in the future.
  • Multiple pre-order items in the same cart, mixing pre-order and standard products.
  • Cart checkout with a pre-order item and a payment gateway that supports authorization only.
  • Conversion of pre-orders to orders after the available_from date, including auto-captures.
  • Refunds and partial refunds for pre-orders that are canceled before fulfillment.

Automated tests are highly recommended. Use Magento's testing framework or your favorite PHP testing tool to cover these scenarios. Document each case with expected vs. actual outcomes for future maintenance.

Deployment and Maintenance: How to Roll This Out

Deployment best practices matter. Consider the following when moving from development to production:

  • Back up your database and codebase before enabling pre-orders on a live store.
  • Run Magento setup:upgrade and indexer:reindex after installing or updating the module.
  • Monitor performance and error logs during the first week and adjust caching as needed.
  • Provide a simple rollback path if a critical issue arises (disable the pre-order feature and revert changes).

Documentation and internal knowledge sharing are essential. Keep a living doc with the data model, admin actions, and a change log so future developers can understand the system quickly.

Conclusion

Building a custom pre-order module in Magento 2 is a rewarding challenge. It gives you granular control over the pre-sales process, ensures your stock remains accurate, and enhances your customer experience with timely notifications and precise administration. The steps above provide a practical blueprint—from database design to payment integration, admin workflows, and performance tuning. Start small, validate each piece, and iterate with your team. Happy coding, and may your pre-orders convert into delighted customers.

Appendix: Quick Reference Commands

These commands are handy in a local development environment:

# Enable Magento modules (run in Magento root)
php bin/magento module:enable Magefine_PreOrder
php bin/magento setup:upgrade
php bin/magento cache:flush
php bin/magento indexer:reindex