This article is a step by step tutorial for building a Magento 2 module to manage catalogue release by a pre-specified date.

This module was built as part of the selection process for the Magento 2 developer profile, but I decided to write my complete thought process and development to help my fellow learners!

  1. Gather Requirements

    • Product should have a Release Date Time attribute. The attribute should be editable in the Admin
    • If the Release Date Time attribute is defined for the product consider two cases:

      • The value of the Release Date Time attribute is in the future:

        • Do not allow the product to be purchased.
        • Inform the customer about the release date and time on product details and product listing page.
      • The value of the Release Date Time attribute is now or in the past:

        • Fallback to standard Magento 2 behaviour on product details and product listing pages.
  2. Research & Analysis

    • We should do some analysis and search for a solution that can be implemented in Magento 2.
    • Entry points for the catalogue release date validation:

      • API:

        • Add New Product

          • /rest/:storeId/V1/carts/mine/items
          • /rest/:storeId/V1/guest-carts/:cartId/items
        • Update Product Quantity

          • /rest/:storeId/V1/carts/mine/items
          • /rest/:storeId/V1/guest-carts/:cartId/items
      • Legacy Checkout:

        • Add New Product from Product Listing Page
        • Add New Product from Product View Page
        • Update Quantity from Mini Cart
    • Timer display (using knockout.js component) in Product Listing Page and Product View Page
    • Add the Release Date Time attribute in the product (website scope) and display it in the Product Description
  3. Implimentation

  • Add a new attribute release_date_time using data patch.

                    
                        <?php

namespace Adapttive\Catalog\Setup\Patch\Data;

use Adapttive\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Setup\Patch\Data\UpdateProductAttributes;
use Magento\Eav\Model\Entity\Attribute\Backend\Datetime;
use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface;
use Magento\Eav\Setup\EavSetup;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Eav\Setup\EavSetupFactory;

class AddReleaseDateTimeAttribute implements DataPatchInterface
{
    /**
     * @var ModuleDataSetupInterface
     */
    private $moduleDataSetup;

    /**
     * @var EavSetupFactory
     */
    private $eavSetupFactory;

    /**
     * AddReleaseAttribute constructor.
     * @param ModuleDataSetupInterface $moduleDataSetup
     * @param EavSetupFactory $eavSetupFactory
     */
    public function __construct(
        ModuleDataSetupInterface $moduleDataSetup,
        EavSetupFactory $eavSetupFactory
    ) {
        $this->moduleDataSetup = $moduleDataSetup;
        $this->eavSetupFactory = $eavSetupFactory;
    }

    public static function getDependencies()
    {
        return [
            UpdateProductAttributes::class
        ];
    }

    public function getAliases()
    {
        return [];
    }

    public function apply()
    {
        /** @var EavSetup $eavSetup */
        $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]);
        $eavSetup->addAttribute(
            Product::ENTITY,
            ProductInterface::RELEASE_DATE_TIME,
            [
                'type' => 'datetime',
                'label' => 'Release Date Time',
                'input' => 'date',
                'required' => false,
                'sort_order' => 50,
                'backend' => Datetime::class,
                'global' => ScopedAttributeInterface::SCOPE_WEBSITE,
                'visible_on_front' => true,
                'used_in_product_listing' => true,
                'is_used_in_grid' => true,
                'is_visible_in_grid' => false,
                'is_filterable_in_grid' => false,
            ]
        );
    }
}
                    
                

  • Create a common validator for the release date of the product.

                    
                        <?php

namespace Adapttive\Catalog\Model;

use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Stdlib\DateTime;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;

class ReleaseValidator
{
    /**
     * @var TimezoneInterface
     */
    private $timezone;

    public function __construct(
        TimezoneInterface $timezone
    ) {
        $this->timezone = $timezone;
    }

    /**
     * Validate if product is released
     * @param $product
     * @return bool
     * @throws LocalizedException
     */
    public function validate($product)
    {
        if ($product && $product->getReleaseDateTime()) {
            // if product not released throw exception.
            $releaseDateTime = $this->timezone->date((string)$product->getReleaseDateTime());
            $current = $this->timezone->date();
            if ($releaseDateTime && $releaseDateTime > $current) {
                throw new LocalizedException(
                    __(
                        "The product is not available for purchase. Please retry after %1.",
                        $releaseDateTime->format(DateTime::DATETIME_PHP_FORMAT
                        )
                    )
                );
            }
        }

        return true;
    }
}
                    
                

  • Create an Observer to trigger validation and throw an error.

                    
                        <?php

namespace Adapttive\Catalog\Observer;

use Adapttive\Catalog\Helper\Config;
use Adapttive\Catalog\Model\ReleaseValidator;
use Magento\Framework\Event\Observer;
use Magento\Framework\Exception\LocalizedException;
use Magento\Quote\Model\Quote\Item;
use Magento\Framework\Event\ObserverInterface;

/**
 * Class ReleaseObserver
 * Trigger the release date validation..
 * Reference: catalog inventory validation
 */
class ReleaseObserver implements ObserverInterface
{
    /**
     * @var ReleaseValidator
     */
    private $validator;

    /**
     * @var Config
     */
    private $config;

    public function __construct(
        Config $config,
        ReleaseValidator $validator
    ) {
        $this->validator = $validator;
        $this->config = $config;
    }

    /**
     * @param Observer $observer
     * @return void
     * @throws LocalizedException
     */
    public function execute(Observer $observer)
    {
        /* @var $quoteItem Item */
        $quoteItem = $observer->getEvent()->getItem();
        if (!$quoteItem ||
            !$quoteItem->getProductId() ||
            !$quoteItem->getQuote() ||
            !$this->config->isEnabled()
        ) {
            return;
        }
        $product = $quoteItem->getProduct();

        $this->validator->validate($product);
    }
}
                    
                

References: