Magento2 | PWA | GraphQL

Magento 2 Credit Memo Export Module Using Message Queue | XML Export & Asynchronous Processing


This Magento2 module explains how to Add a new button on Credit Memo View page. In this module we have added export button using Admin UI, ACL, Dependency Injection, Message Queue, Queue Consumer, Repository Pattern, XML generation, file and backup export, CLI commands, and database-based export status tracking. It demonstrates how a Credit Memo is queued from the Admin panel, processed asynchronously by a consumer, converted into XML, exported to a configured location, and tracked using published_at and exported_at timestamps.

You can find the complete module on GitHub at Magelearn_CreditMemoExport

Or Check the images below for a better understanding of the functionality of this module.

Let's start it by creating a custom extension. 

Create a folder inside app/code/Magelearn/CreditMemoExport



Add registration.php file in it:

<?php

use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magelearn_CreditMemoExport', __DIR__);

Add composer.json file in it:

{
    "name": "magelearn/module-credit-memo-export",
    "description": "Magento 2 module for exporting Credit Memos asynchronously via Message Queue.",
    "type": "magento2-module",
    "license": "OSL-3.0",
    "authors": [
        {
            "name": "Vijay Rami",
            "email": "vijaymrami@gmail.com"
        }
    ],
    "minimum-stability": "dev",
    "require": {},
    "autoload": {
        "files": [
            "registration.php"
        ],
        "psr-4": {
            "Magelearn\\CreditMemoExport\\": ""
        }
    }
}

Add etc/module.xml file in it:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Magelearn_CreditMemoExport" active="true">
        <sequence>
            <module name="Magento_Sales"/>
        </sequence>
    </module>
</config>

Now to display the Export button on the Credit memo view page, add sales_order_creditmemo_view.xml file.

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="page.actions.toolbar">
            <block class="Magelearn\CreditMemoExport\Block\Adminhtml\Creditmemo\ExportButton"
                name="creditmemo_export_button"
                template="Magelearn_CreditMemoExport::order/creditmemo/export_button.phtml"/>
        </referenceBlock>
    </body>
</page>
Now Add template and Block file. Add Block at Block/Adminhtml/Creditmemo/ExportButton.php
<?php

declare(strict_types=1);

namespace Magelearn\CreditMemoExport\Block\Adminhtml\Creditmemo;

use Magento\Backend\Block\Template;
use Magento\Backend\Block\Template\Context;
use Magento\Backend\Model\UrlInterface;
use Magento\Framework\Registry;

class ExportButton extends Template
{
    private const EXPORT_ROUTE = 'creditmemoexport/creditmemo/export';

    /**
     * @param array<string, mixed> $data
     */
    public function __construct(
        Context $context,
        private readonly Registry $coreRegistry,
        private readonly UrlInterface $backendUrl,
        array $data = []
    ) {
        parent::__construct($context, $data);
    }

    public function getExportUrl(): ?string
    {
        $creditmemo = $this->coreRegistry->registry('current_creditmemo');

        if ($creditmemo === null) {
            return null;
        }

        return $this->backendUrl->getUrl(
            self::EXPORT_ROUTE,
            [
                'creditmemo_id' => (int) $creditmemo->getEntityId(),
            ]
        );
    }
}
And template file at view/adminhtml/templates/order/creditmemo/export_button.phtml
<?php

declare(strict_types=1);

/** @var \Magelearn\CreditMemoExport\Block\Adminhtml\Creditmemo\ExportButton $block */

$exportUrl = $block->getExportUrl();
?>
<?php if ($exportUrl) : ?>
    <form method="post" action="<?= $block->escapeUrl($exportUrl) ?>">
        <?= $block->getBlockHtml('formkey') ?>

        <button type="submit"
                class="action-default scalable"
                title="<?= $block->escapeHtmlAttr(__('Export to Service')) ?>">
            <span><?= $block->escapeHtml(__('Export to Service')) ?></span>
        </button>
    </form>
<?php endif; ?>
Now to manage the Export Process for Credit memo, we will add our custom table and some database fields.

Add etc/db_schema.xml
<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
    <table name="magelearn_creditmemo_export" resource="default" engine="innodb" comment="CM Export related data">
        <column name="entity_id" xsi:type="int" padding="10" unsigned="true" nullable="false" identity="true" comment="Export Data Id"/>
        <column name="cm_entity_id" xsi:type="int" padding="10" unsigned="true" nullable="false" comment="Related CM Id" />
        <column name="published_at" xsi:type="timestamp" on_update="false" nullable="true" comment="Timestamp when CM Published to Export Queue" />
        <column name="exported_at" xsi:type="timestamp" on_update="false" nullable="true" comment="CM Exported At" />
        <constraint xsi:type="primary" referenceId="PRIMARY">
            <column name="entity_id" />
        </constraint>
        <constraint xsi:type="foreign"
                    referenceId="MAGELEARN_CREDITMEMO_EXPORT_CM_ENTITY_ID_SALES_CREDITMEMO_ENTITY_ID"
                    table="magelearn_creditmemo_export"
                    column="cm_entity_id"
                    referenceTable="sales_creditmemo"
                    referenceColumn="entity_id"
                    onDelete="CASCADE" />
        <index referenceId="MAGELEARN_CREDITMEMO_EXPORT_CM_ENTITY_ID" indexType="btree">
            <column name="cm_entity_id" />
        </index>
    </table>
</schema>

And add etc/db_schema_whitelist.json
{
    "magelearn_creditmemo_export": {
        "column": {
            "entity_id": true,
            "cm_entity_id": true,
            "published_at": true,
            "exported_at": true
        },
        "index": {
            "MAGELEARN_CREDITMEMO_EXPORT_CM_ENTITY_ID": true
        },
        "constraint": {
            "PRIMARY": true,
            "MAGELEARN_CREDITMEMO_EXPORT_CM_ENTITY_ID_SALES_CREDITMEMO_ENTITY_ID": true
        }
    }
}
Now add System Management by Providing Enable/Disable and Export and Backup Path for this module.

Add etc/adminhtml/system.xml file.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <tab id="integrations" translate="label" sortOrder="600">
            <label>Integrations</label>
        </tab>
        <section id="magelearn_export" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1">
            <class>separator-top</class>
            <label>Magelearn Export</label>
            <tab>integrations</tab>
            <resource>Magelearn_CreditMemoExport::export_creditmemo</resource>
            <group id="creditmemo" translate="label" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Credit Memo</label>
                <field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Enabled</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="export_path" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1">
                    <label>Export Path</label>
                </field>
                <field id="backup_path" translate="label comment" type="text" sortOrder="30" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1">
                    <label>Backup Path</label>
                </field>
            </group>
        </section>
    </system>
</config>
Also add etc/config.xml file.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <magelearn_export>
            <creditmemo>
                <enabled>1</enabled>
                <export_path>var/integration/export/creditmemo</export_path>
                <backup_path>var/export_creditmemo</backup_path>
            </creditmemo>
        </magelearn_export>
    </default>
</config>
And etc/acl.xml file.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
    <acl>
        <resources>
            <resource id="Magento_Backend::admin">
                <resource id="Magento_Sales::sales">
                    <resource id="Magento_Sales::sales_operation">
                        <resource id="Magento_Sales::sales_order">
                            <resource id="Magento_Sales::actions">
                                <resource id="Magelearn_CreditMemoExport::export_creditmemo" title="Export Credit Memo to Service" translate="title" sortOrder="200" />
                            </resource>
                        </resource>
                    </resource>
                </resource>
            </resource>
        </resources>
    </acl>
</config>
Now as per the Block Add etc/adminhtml/routes.xml file.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="admin">
        <route id="magelearn_creditmemoexport" frontName="creditmemoexport">
            <module name="Magelearn_CreditMemoExport"/>
        </route>
    </router>
</config>
Now add Controller file at: Controller/Adminhtml/Creditmemo/Export.php
<?php

declare(strict_types=1);

namespace Magelearn\CreditMemoExport\Controller\Adminhtml\Creditmemo;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Backend\Model\Auth\Session;
use Magento\Backend\Model\View\Result\RedirectFactory;
use Magento\Framework\Controller\ResultInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\MessageQueue\PublisherInterface;
use Magento\Sales\Api\CreditmemoRepositoryInterface;
use Magento\Sales\Model\Order\Creditmemo\CommentRepository;
use Psr\Log\LoggerInterface;
use Magelearn\CreditMemoExport\General\ExportConfiguration;
use Magelearn\CreditMemoExport\Model\CreditmemoExportRepository;

use function __;

class Export extends Action
{
    /**
     * @see _isAllowed()
     */
    public const ADMIN_RESOURCE = 'Magelearn_CreditMemoExport::export_creditmemo';

    public const CREDITMEMO_VIEW_PATH = 'sales/order_creditmemo/view';

    public function __construct(
        private readonly Session $authSession,
        private readonly RedirectFactory $redirectFactory,
        private readonly PublisherInterface $publisher,
        private readonly CreditmemoRepositoryInterface $creditmemoRepository,
        private readonly CommentRepository $commentRepository,
        private readonly ExportConfiguration $config,
        private readonly LoggerInterface $logger,
        private readonly CreditmemoExportRepository $creditmemoExportRepository,
        Context $context
    ) {
        parent::__construct($context);
    }

    public function execute(): ResultInterface
    {
        $resultRedirect = $this->redirectFactory->create();
        $creditmemoId = (int) $this->getRequest()->getParam('creditmemo_id');

        try {
            if ($creditmemoId <= 0) {
                throw new LocalizedException(
                    __('A valid Credit Memo ID is required.')
                );
            }

            $this->exportCreditmemo($creditmemoId);

            $this->messageManager->addSuccessMessage(
                __('Credit Memo has been queued for export to Service.')
            );
        } catch (LocalizedException $e) {
            $this->messageManager->addNoticeMessage($e->getMessage());
        } catch (\Throwable $e) {
            $this->messageManager->addErrorMessage(__('Credit Memo export to Service failed.'));
            $this->logger->error(
                'Manual Credit Memo export failed.',
                [
                    'creditmemo_id' => $creditmemoId,
                    'exception_class' => $e::class,
                    'exception' => $e->getMessage(),
                    'trace' => $e->getTraceAsString(),
                ]
            );
        }

        return $resultRedirect->setPath(self::CREDITMEMO_VIEW_PATH, ['creditmemo_id' => $creditmemoId]);
    }

    private function exportCreditmemo(int $creditmemoId): void
    {
        $creditmemo = $this->creditmemoRepository->get($creditmemoId);
        $storeId = (int) $creditmemo->getStoreId();

        if (!$this->config->getIsEnabled($storeId)) {
            throw new LocalizedException(
                __('Service export is disabled for this store.')
            );
        }

        $exportData = $this->creditmemoExportRepository->findByCreditmemoEntityId($creditmemoId);
        $username = $this->getAdminUserName();

        if ($exportData->getPublishedAt() || $exportData->getExportedAt()) {
            throw new LocalizedException(
                __('Credit Memo has already been queued or exported to Service.')
            );
        }

        $this->publisher->publish($this->config->getTopicName(), (string) $creditmemoId);
        $exportData->publish();
        $this->creditmemoExportRepository->save($exportData);

        $comment = $creditmemo->addComment(
            __(
                'Credit Memo manually queued for export to Service by %1.',
                $username
            )
        );

        $this->commentRepository->save($comment);

        $this->logger->info(
            'Credit Memo manually queued for Service export.',
            [
                'creditmemo_id' => $creditmemoId,
                'user' => $username,
            ]
        );
    }

    private function getAdminUserName(): string
    {
        $user = $this->authSession->getUser();

        return $user ? $user->getName() : 'System';
    }
}
Now as per the highlighted code above add General/ExportConfiguration.php file.
<?php

namespace Magelearn\CreditMemoExport\General;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;

class ExportConfiguration
{
    private const XML_PATH_EXPORT_PREFIX = 'magelearn_export/';
    private const XML_PATH_EXPORT_ENABLED = '/enabled';
    private const XML_PATH_EXPORT_FOLDER = '/export_path';
    private const XML_PATH_BACKUP_FOLDER = '/backup_path';

    /**
     * @var ScopeConfigInterface
     */
    private $scopeConfig;

    /**
     * @var string
     */
    private $entityName;

    /**
     * @var string
     */
    private $topicName;

    public function __construct(
        ScopeConfigInterface $scopeConfig,
        string $entityName,
        string $topicName
    ) {
        $this->scopeConfig = $scopeConfig;
        $this->entityName = $entityName;
        $this->topicName = $topicName;
    }

    public function getIsEnabled(int $storeId): bool
    {
        return $this->scopeConfig->isSetFlag(
            self::XML_PATH_EXPORT_PREFIX . $this->entityName . self::XML_PATH_EXPORT_ENABLED,
            ScopeInterface::SCOPE_STORE,
            $storeId
        );
    }

    public function getExportPath(): ?string
    {
        return $this->scopeConfig->getValue(
            self::XML_PATH_EXPORT_PREFIX . $this->entityName . self::XML_PATH_EXPORT_FOLDER
        );
    }

    public function getBackupPath(): ?string
    {
        return $this->scopeConfig->getValue(
            self::XML_PATH_EXPORT_PREFIX . $this->entityName . self::XML_PATH_BACKUP_FOLDER
        );
    }

    public function getTopicName(): ?string
    {
        return $this->topicName;
    }
}
Also add Model/CreditmemoExportRepository.php file.
<?php

namespace Magelearn\CreditMemoExport\Model;

use Magento\Framework\Exception\AlreadyExistsException;
use Magelearn\CreditMemoExport\Api\CreditmemoExportInterface;

class CreditmemoExportRepository
{
    /**
     * @var CreditmemoExportFactory
     */
    private $creditmemoExportFactory;

    /**
     * @var ResourceModel\CreditmemoExport
     */
    private $creditmemoExportResource;

    private $cache = [];

    public function __construct(
        CreditmemoExportFactory $creditmemoExportFactory,
        ResourceModel\CreditmemoExport $creditmemoExportResource
    ) {
        $this->creditmemoExportFactory = $creditmemoExportFactory;
        $this->creditmemoExportResource = $creditmemoExportResource;
    }

    public function findByCreditmemoEntityId($creditmemoEntityId): CreditmemoExportInterface
    {
        if (!array_key_exists($creditmemoEntityId, $this->cache)) {
            /** @var CreditmemoExport $object */
            $object = $this->creditmemoExportFactory->create();
            $this->creditmemoExportResource->load($object, $creditmemoEntityId, 'cm_entity_id');

            if ($object->isObjectNew()) {
                $object->setCreditmemoEntityId($creditmemoEntityId);
                $object->setDataChanges(false);
            }

            $this->cache[$creditmemoEntityId] = $object;
        }

        return $this->cache[$creditmemoEntityId];
    }

    /**
     * @param CreditmemoExport $creditmemoExport
     * @throws AlreadyExistsException
     */
    public function save(CreditmemoExportInterface $creditmemoExport)
    {
        if (!$creditmemoExport->hasDataChanges()) {
            return;
        }

        $this->creditmemoExportResource->save($creditmemoExport);
        unset($this->cache[$creditmemoExport->getCreditmemoEntityId()]);
    }
}
As per the highlighted code above add Api/CreditmemoExportInterface.php
<?php

namespace Magelearn\CreditMemoExport\Api;

interface CreditmemoExportInterface extends ExportInterface
{
    public function setCreditmemoEntityId(int $creditmemoEntityId): CreditmemoExportInterface;
}
Also add Api/ExportInterface.php
<?php

namespace Magelearn\CreditMemoExport\Api;

interface ExportInterface
{
    /**
     * @return int|null
     */
    public function getCreditmemoEntityId(): ?int;

    public function setCreditmemoEntityId(int $creditmemoEntityId): self;

    /**
     * @return string|null
     */
    public function getExportedAt(): ?string;

    /**
     * @return string|null
     */
    public function getPublishedAt(): ?string;

    public function publish(): void;

    public function export(): void;
}
Now We will add our etc/di.xml file to add the concrete class definations.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <type name="Magento\Framework\Console\CommandList">
        <arguments>
            <argument name="commands" xsi:type="array">
                <item name="magelearn_export_creditmemo" xsi:type="object">Magelearn\CreditMemoExport\Console\Command\ExportCreditmemo</item>
            </argument>
        </arguments>
    </type>

    <type name="Magelearn\CreditMemoExport\Console\Command\ExportCreditmemo">
        <arguments>
            <argument name="consumer" xsi:type="object">Magelearn\CreditMemoExport\Creditmemo\Consumer\Proxy</argument>
            <argument name="creditmemoRepository" xsi:type="object">Magento\Sales\Api\CreditmemoRepositoryInterface\Proxy</argument>
            <argument name="searchCriteriaBuilder" xsi:type="object">Magelearn\CreditMemoExport\Console\Command\SearchCriteriaBuilder\Proxy</argument>
            <argument name="creditmemoExportRepository" xsi:type="object">Magelearn\CreditMemoExport\Model\CreditmemoExportRepository\Proxy</argument>
        </arguments>
    </type>

    <type name="Magelearn\CreditMemoExport\Controller\Adminhtml\Creditmemo\Export">
        <arguments>
            <argument name="config" xsi:type="object">
                Magelearn\CreditMemoExport\Creditmemo\ExportConfiguration
            </argument>
        </arguments>
    </type>

    <type name="Magelearn\CreditMemoExport\Creditmemo\Consumer">
        <arguments>
            <argument name="exporter" xsi:type="object">Magelearn\CreditMemoExport\Creditmemo\Exporter</argument>
        </arguments>
    </type>

    <virtualType name="Magelearn\CreditMemoExport\Logger" type="Magento\Framework\Logger\Monolog">
        <arguments>
            <argument name="name" xsi:type="string">MagelearnExport</argument>
            <argument name="handlers" xsi:type="array">
                <item name="system" xsi:type="object">Magelearn\CreditMemoExport\Logger\FileHandler</item>
            </argument>
        </arguments>
    </virtualType>

    <virtualType name="Magelearn\CreditMemoExport\Logger\FileHandler" type="Magento\Framework\Logger\Handler\Base">
        <arguments>
            <argument name="fileName" xsi:type="string">var/log/message-queue.log</argument>
        </arguments>
    </virtualType>

    <virtualType name="Magelearn\CreditMemoExport\Xml\CreditmemoWriter" type="Magelearn\CreditMemoExport\Xml\Writer">
        <arguments>
            <argument name="rootName" xsi:type="string">returnRequest</argument>
            <argument name="arrayItemMap" xsi:type="array">
                <item name="vatDetail" xsi:type="string">vat</item>
            </argument>
        </arguments>
    </virtualType>

    <virtualType name="Magelearn\CreditMemoExport\Creditmemo\Exporter\File"
                 type="Magelearn\CreditMemoExport\General\Exporter\File">
        <arguments>
            <argument name="exportConfiguration" xsi:type="object">Magelearn\CreditMemoExport\Creditmemo\ExportConfiguration</argument>
        </arguments>
    </virtualType>

    <virtualType name="Magelearn\CreditMemoExport\Creditmemo\Exporter\Resolver"
                 type="Magelearn\CreditMemoExport\General\Exporter\Resolver">
        <arguments>
            <argument name="fileExporter" xsi:type="object">Magelearn\CreditMemoExport\Creditmemo\Exporter\File</argument>
        </arguments>
    </virtualType>

    <virtualType name="Magelearn\CreditMemoExport\Creditmemo\Exporter"
                 type="Magelearn\CreditMemoExport\General\Exporter">
        <arguments>
            <argument name="dataBuilder" xsi:type="object">Magelearn\CreditMemoExport\Creditmemo\DataBuilder</argument>
            <argument name="exporterResolver" xsi:type="object">Magelearn\CreditMemoExport\Creditmemo\Exporter\Resolver</argument>
            <argument name="xmlWriter" xsi:type="object">Magelearn\CreditMemoExport\Xml\CreditmemoWriter</argument>
        </arguments>
    </virtualType>

    <virtualType name="Magelearn\CreditMemoExport\Creditmemo\DataBuilder"
                 type="Magelearn\CreditMemoExport\General\DataBuilder">
        <arguments>
            <argument name="dataBuilders" xsi:type="array">
                <item name="client" xsi:type="object" sortOrder="10">Magelearn\CreditMemoExport\General\DataBuilder\Client</item>
                <item name="additionalInfo" xsi:type="object" sortOrder="20">Magelearn\CreditMemoExport\General\DataBuilder\AdditionalInfo</item>
            </argument>
        </arguments>
    </virtualType>

    <virtualType name="Magelearn\CreditMemoExport\Creditmemo\ExportConfiguration"
                 type="Magelearn\CreditMemoExport\General\ExportConfiguration">
        <arguments>
            <argument name="entityName" xsi:type="string">creditmemo</argument>
            <argument name="topicName" xsi:type="string">magelearn.creditmemo_export</argument>
        </arguments>
    </virtualType>
</config>
Also add Model/CreditmemoExport.php file.
<?php

declare(strict_types=1);

namespace Magelearn\CreditMemoExport\Model;

use Magento\Framework\Data\Collection\AbstractDb;
use Magento\Framework\Model\AbstractModel;
use Magento\Framework\Model\Context;
use Magento\Framework\Model\ResourceModel\AbstractResource;
use Magento\Framework\Registry;
use Magento\Framework\Stdlib\DateTime\DateTime;
use Magelearn\CreditMemoExport\Api\CreditmemoExportInterface;
use Magelearn\CreditMemoExport\Model\ResourceModel\CreditmemoExport as CreditmemoExportResource;

class CreditmemoExport extends AbstractModel implements CreditmemoExportInterface
{
    private DateTime $dateTime;

    /**
     * @param mixed[] $data
     */
    public function __construct(
        Context $context,
        Registry $registry,
        DateTime $dateTime,
        ?AbstractResource $resource = null,
        ?AbstractDb $resourceCollection = null,
        array $data = []
    ) {
        parent::__construct($context, $registry, $resource, $resourceCollection, $data);
        $this->dateTime = $dateTime;
    }

    protected function _construct(): void
    {
        $this->_init(CreditmemoExportResource::class);
        parent::_construct();
    }

    public function getCreditmemoEntityId(): ?int
    {
        return (int) $this->getData('cm_entity_id') ?: null;
    }

    public function setCreditmemoEntityId(int $creditmemoEntityId): CreditmemoExportInterface
    {
        return $this->setData('cm_entity_id', $creditmemoEntityId);
    }

    public function getExportedAt(): ?string
    {
        return $this->getData('exported_at');
    }

    public function getPublishedAt(): ?string
    {
        return $this->getData('published_at');
    }

    public function publish(): void
    {
        $this->setData('published_at', $this->dateTime->date());
    }

    public function export(): void
    {
        $this->setData('exported_at', $this->dateTime->date());
    }
}
And Model/ResourceModel/CreditmemoExport.php file.
<?php

namespace Magelearn\CreditMemoExport\Model\ResourceModel;

use Magento\Framework\Model\ResourceModel\Db\AbstractDb;

class CreditmemoExport extends AbstractDb
{
    private const TABLE_NAME = 'magelearn_creditmemo_export';
    private const ID_FIELD = 'entity_id';

    /**
     * Resource initialization
     *
     * @return void
     */
    protected function _construct()
    {
        $this->_init(self::TABLE_NAME, self::ID_FIELD);
    }
}
Now to Publish the Credit memo, we will implement Queue Consumer.
For that add etc/communication.xml file.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd">
    <topic name="magelearn.creditmemo_export" request="string">
        <handler name="magelearn.creditmemo_export"
                 type="Magelearn\CreditMemoExport\Creditmemo\Consumer"
                 method="process" />
    </topic>
</config>
add etc/queue_consumer.xml file.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd">
    <consumer name="magelearn.creditmemo_export" queue="magelearn.creditmemo_export"
              connection="db"
              maxMessages="50"
              consumerInstance="Magento\Framework\MessageQueue\Consumer"
              handler="Magelearn\CreditMemoExport\Creditmemo\Consumer::process" />
</config>
add etc/queue_publisher.xml file.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/publisher.xsd">
    <publisher topic="magelearn.creditmemo_export">
        <connection name="db" exchange="magento-db" />
    </publisher>
</config>
add etc/queue_topology.xml file.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/topology.xsd">
    <exchange name="magento-db" type="topic" connection="db">
        <binding id="creditmemo_exportBinding" topic="magelearn.creditmemo_export" destinationType="queue" destination="magelearn.creditmemo_export" />
    </exchange>
</config>
Now we will add our Consumer and Process method at Creditmemo/Consumer.php file.
<?php

declare(strict_types=1);

namespace Magelearn\CreditMemoExport\Creditmemo;

use Magento\Framework\Exception\AlreadyExistsException;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Sales\Api\CreditmemoRepositoryInterface;
use Magento\Sales\Model\Order\Creditmemo;
use Magento\Sales\Model\Order\Creditmemo\CommentRepository;
use Psr\Log\LoggerInterface;
use Magelearn\CreditMemoExport\Exception\CreditmemoExportException;
use Magelearn\CreditMemoExport\General\Exporter;
use Magelearn\CreditMemoExport\Model\CreditmemoExportRepository;

class Consumer
{
    public const SERVICES_CREDITMEMO_EXPORT_COMMENT = 'Creditmemo exported to Services';

    public function __construct(
        private readonly CreditmemoRepositoryInterface $creditmemoRepository,
        private readonly CreditmemoExportRepository $creditmemoExportRepository,
        private readonly CommentRepository $commentRepository,
        private readonly Exporter $exporter,
        private readonly LoggerInterface $logger
    ) {
    }

    /**
     * @throws CreditmemoExportException
     */
    public function process(string $creditmemoId): void
    {
        try {
            /** @var Creditmemo $creditmemo */
            $creditmemo = $this->creditmemoRepository->get($creditmemoId);

            $this->exporter->process($creditmemo);

            $this->addComment($creditmemo);
            $this->export($creditmemo);
            $this->logger->info(self::SERVICES_CREDITMEMO_EXPORT_COMMENT, ['CreditmemoId' => $creditmemoId]);
        } catch (\Throwable $e) {
            $this->logger->critical($e->getMessage(), ['CreditmemoId' => $creditmemoId]);

            throw new CreditmemoExportException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * @throws CouldNotSaveException
     */
    private function addComment(Creditmemo $creditmemo): void
    {
        $comment = $creditmemo->addComment(self::SERVICES_CREDITMEMO_EXPORT_COMMENT);
        $this->commentRepository->save($comment);
    }

    /**
     * @throws AlreadyExistsException
     */
    private function export(Creditmemo $creditmemo): void
    {
        $exportData = $this->creditmemoExportRepository->findByCreditmemoEntityId($creditmemo->getEntityId());
        $exportData->export();
        $this->creditmemoExportRepository->save($exportData);
    }
}
And as per highlighted code above, we will add Exception/CreditmemoExportException.php file.
<?php

declare(strict_types=1);

namespace Magelearn\CreditMemoExport\Exception;

class CreditmemoExportException extends \Exception
{
}
And General/Exporter.php file:
<?php

declare(strict_types=1);

namespace Magelearn\CreditMemoExport\General;

use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\MessageQueue\ConnectionLostException;
use Magento\Sales\Model\Order\Creditmemo;
use Magelearn\CreditMemoExport\General\Exporter\Resolver;
use Magelearn\CreditMemoExport\Xml\Writer;

class Exporter
{
    public function __construct(
        private readonly DataBuilder $dataBuilder,
        private readonly Resolver $exporterResolver,
        private readonly Writer $xmlWriter
    ) {
    }

    /**
     * @param Creditmemo $entity
     * @throws ConnectionLostException
     * @throws LocalizedException
     */
    public function process($entity): void
    {
        try {
            $xml = $this->buildXmlString($entity);
            $exporter = $this->exporterResolver->resolve($entity);
            $exporter->export($entity, $xml);
        } catch (\Throwable $e) {
            // ConnectionLostException will force queue framework to retry
            throw new ConnectionLostException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * @param Creditmemo $entity
     * @throws LocalizedException
     * @throws InputException
     */
    private function buildXmlString($entity): string
    {
        $xml = $this->xmlWriter->toXml($this->dataBuilder->build($entity));

        return $xml->saveXML();
    }
}
Now as per highlighted code in Exporter.php file, we will add General/Exporter/Resolver.php file.
<?php

declare(strict_types=1);

namespace Magelearn\CreditMemoExport\General\Exporter;

use Magento\Sales\Model\Order\Creditmemo;
use Magelearn\CreditMemoExport\Api\ExporterStrategyInterface;

class Resolver
{
    public function __construct(
        private readonly File $fileExporter
    ) {
    }

    /**
     * @param Creditmemo $entity
     */
    public function resolve($entity): ExporterStrategyInterface
    {
        return $this->fileExporter;
    }
}
And Xml/Writer.php file.
<?php

namespace Magelearn\CreditMemoExport\Xml;

use DOMDocument;
use DOMNode;
use Magento\Framework\Exception\LocalizedException;

class Writer
{
    private const DEFAULT_ITEM_NAME = 'item';

    /**
     * @var string
     */
    private $rootName;

    /**
     * @var string[]
     */
    private $arrayItemMap;

    /**
     * @param string $rootName
     * @param string[] $arrayItemMap key->value pairs of array item names e.g. ['people' => 'person']
     */
    public function __construct(string $rootName = 'root', $arrayItemMap = [])
    {
        $this->rootName = $rootName;
        $this->arrayItemMap = $arrayItemMap;
    }

    /**
     * @param mixed[] $data
     * @return DOMDocument
     * @throws LocalizedException if unable to format data as XML.
     */
    public function toXml(array $data): DOMDocument
    {
        $dataRoot = $this->extractRootFromData($data);

        if (!$dataRoot) {
            return $this->toXml([$this->rootName => $data]);
        }

        $data = $data[$dataRoot];

        $document = new DOMDocument('1.0', 'UTF-8');
        $document->appendChild($this->toDomNode($document, $dataRoot, $data));

        // we want it to be pretty
        $document->preserveWhiteSpace = false;
        $document->formatOutput = true;

        return $document;
    }

    /**
     * @param mixed[] $data
     */
    private function extractRootFromData(array $data): ?string
    {
        if (count($data) !== 1) {
            return null;
        }

        $key = key($data);

        return is_string($key) && !is_numeric($key) ? $key : null;
    }

    private function toDomNode(DOMDocument $document, $name, $data): DOMNode
    {
        $node = $document->createElement($name);

        if ($data === null) {
            return $node;
        }

        if (is_scalar($data)) {
            $node->appendChild($this->createTextNode($document, $data));

            return $node;
        }

        foreach ($data as $key => $value) {
            if ($key !== '_attributes') {
                $nodeName = is_numeric($key) ? $this->getItemName($name) : $key;
                $node->appendChild($this->toDomNode($document, $nodeName, $value));
                continue;
            }

            if (!is_array($value)) {
                continue;
            }

            foreach ($value as $attributeName => $attributeValue) {
                $node->setAttribute($attributeName, $attributeValue);
            }
        }

        return $node;
    }

    private function getItemName($parentNodeName): string
    {
        return array_key_exists($parentNodeName, $this->arrayItemMap)
            ? $this->arrayItemMap[$parentNodeName]
            : self::DEFAULT_ITEM_NAME;
    }

    /**
     * @throws LocalizedException if unable to create text node.
     */
    private function createTextNode(DOMDocument $document, $data): DOMNode
    {
        $type = gettype($data);
        switch ($type) {
            case 'boolean':
                $value = $data ? 1 : 0;
                $cdata = false;
                break;
            case 'integer':
                $value = (string) $data;
                $cdata = false;
                break;
            case 'double':
                $value = number_format($data, 4, '.', '');
                $cdata = false;
                break;
            case 'string':
                $value = $data;
                $cdata = $this->requiresCdata($value);
                break;
            default:
                throw new LocalizedException(
                    __('Data is not primitive: expected bool, int, double or string. %type given.', $type)
                );
        }

        return $cdata
            ? $document->createCDATASection($value)
            : $document->createTextNode($value);
    }

    private function requiresCdata($value): bool
    {
        $chars = ['@', ' '];

        foreach ($chars as $char) {
            if (strpos($value, $char) !== false) {
                return true;
            }
        }

        $filterUrl = filter_var($value, FILTER_VALIDATE_URL);

        return $filterUrl !== false;
    }
}
And Api/ExporterStrategyInterface.php file.
<?php

declare(strict_types=1);

namespace Magelearn\CreditMemoExport\Api;

use Magento\Sales\Model\Order\Creditmemo;

interface ExporterStrategyInterface
{
    /**
     * @param Creditmemo $entity
     */
    public function export($entity, string $xml): void;
}
Now as per the General/Exporter.php file add General/DataBuilder.php file.
<?php

declare(strict_types=1);

namespace Magelearn\CreditMemoExport\General;

use Magento\Sales\Model\Order\Creditmemo;
use Magelearn\CreditMemoExport\Api\GeneralDataBuilderInterface;

class DataBuilder implements GeneralDataBuilderInterface
{
    /**
     * @var GeneralDataBuilderInterface[]
     */
    private array $dataBuilders;

    /**
     * @param GeneralDataBuilderInterface[] $dataBuilders
     */
    public function __construct(
        array $dataBuilders
    ) {
        foreach ($dataBuilders as $name => $dataBuilder) {
            if (!$dataBuilder instanceof GeneralDataBuilderInterface) {
                throw new \TypeError(sprintf(
                    "DataBuilder with name %s must implement %s, instead it's %s",
                    $name,
                    GeneralDataBuilderInterface::class,
                    get_class($dataBuilder)
                ));
            }
        }

        $this->dataBuilders = $dataBuilders;
    }

    /**
     * @param Creditmemo $entity
     * @return array<string, array<string, mixed>>
     */
    public function build($entity): array
    {
        $data = [];
        foreach ($this->dataBuilders as $dataBuilder) {
            $data = array_merge(
                $data,
                $dataBuilder->build($entity)
            );
        }

        return $data;
    }
}
And add Api/GeneralDataBuilderInterface.php file.
<?php

declare(strict_types=1);

namespace Magelearn\CreditMemoExport\Api;

use Magento\Sales\Model\Order\Creditmemo;

interface GeneralDataBuilderInterface
{
    /**
     * @param Creditmemo $entity
     * @return array<string, array<string, mixed>>
     */
    public function build($entity): array;
}
Now as per the di.xml file add General/DataBuilder/Client.php file
<?php

declare(strict_types=1);

namespace Magelearn\CreditMemoExport\General\DataBuilder;

use Magento\Sales\Model\Order\Creditmemo;
use Magelearn\CreditMemoExport\Api\GeneralDataBuilderInterface;

class Client implements GeneralDataBuilderInterface
{
    /**
     * @param Creditmemo $entity
     * @return array<string, array<string, mixed>>
     */
    public function build($entity): array
    {
        return [
            'client' => 'MAGENTO',
        ];
    }
}
And  General/DataBuilder/AdditionalInfo.php file
<?php

declare(strict_types=1);

namespace Magelearn\CreditMemoExport\General\DataBuilder;

use Magento\Sales\Model\Order\Creditmemo;
use Magelearn\CreditMemoExport\Api\GeneralDataBuilderInterface;

class AdditionalInfo implements GeneralDataBuilderInterface
{
    /**
     * @param Creditmemo $entity
     * @return array<string, array<string, mixed>>
     */
    public function build($entity): array
    {
        return [
            'additionalInfo' => [
                'createdAt' => $entity->getCreatedAt(),
                'currency' => $entity->getOrderCurrencyCode(),
            ],
        ];
    }
}
Now as per code highlighted in General/Exporter/Resolver.php file add file resolveer at General/Exporter/File.php file:
<?php

declare(strict_types=1);

namespace Magelearn\CreditMemoExport\General\Exporter;

use Magento\Framework\Filesystem;
use Magento\Sales\Model\Order\Creditmemo;
use Magelearn\CreditMemoExport\Api\ExporterStrategyInterface;
use Magelearn\CreditMemoExport\General\ExportConfiguration;

class File implements ExporterStrategyInterface
{
    public function __construct(
        private readonly ExportConfiguration $exportConfiguration,
        private readonly Filesystem $filesystem
    ) {
    }

    /**
     * @param Creditmemo $entity
     */
    public function export($entity, string $xml): void
    {
        $folder = $this->filesystem->getDirectoryWrite('base');
        $filename = $this->generateFilename($entity);
        $folder->writeFile($filename, $xml);
        $folder->copyFile($filename, $this->generateFilename($entity, true));
    }

    /**
     * @param Creditmemo $entity
     */
    private function generateFilename($entity, bool $backup = false): string
    {
        if ($backup) {
            $basePath = $this->exportConfiguration->getBackupPath()
                . DIRECTORY_SEPARATOR
                . date('Y')
                . DIRECTORY_SEPARATOR
                . date('m');
        } else {
            $basePath = $this->exportConfiguration->getExportPath();
        }

        return $basePath
            . DIRECTORY_SEPARATOR
            . sprintf('%s.xml', $entity->getIncrementId());
    }
}
And finally as per the di.xml we will add our Console command file at Console/Command/ExportCreditmemo.php
<?php

namespace Magelearn\CreditMemoExport\Console\Command;

use Magento\Framework\App\Area;
use Magento\Framework\App\State;
use Magento\Framework\Exception\LocalizedException;
use Magento\Sales\Api\CreditmemoRepositoryInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Magelearn\CreditMemoExport\Creditmemo\Consumer;
use Magelearn\CreditMemoExport\Model\CreditmemoExportRepository;

class ExportCreditmemo extends Command
{
    /**
     * @var State
     */
    private $state;

    /**
     * @var Consumer
     */
    private $consumer;

    /**
     * @var CreditmemoRepositoryInterface
     */
    private $creditmemoRepository;

    /**
     * @var SearchCriteriaBuilder
     */
    private $searchCriteriaBuilder;

    /**
     * @var CreditmemoExportRepository
     */
    private $creditmemoExportRepository;

    public function __construct(
        State $state,
        Consumer $consumer,
        CreditmemoRepositoryInterface $creditmemoRepository,
        SearchCriteriaBuilder $searchCriteriaBuilder,
        CreditmemoExportRepository $creditmemoExportRepository,
        ?string $name = null
    ) {
        parent::__construct($name);
        $this->state = $state;
        $this->consumer = $consumer;
        $this->creditmemoRepository = $creditmemoRepository;
        $this->searchCriteriaBuilder = $searchCriteriaBuilder;
        $this->creditmemoExportRepository = $creditmemoExportRepository;
    }

    /**
     * @inheritdoc
     */
    protected function configure()
    {
        $this->setName('magelearn:export:creditmemo');
        $this->setDescription('Export Credit Memo to Service');
        $this->addArgument('increment-id', InputArgument::OPTIONAL, 'Increment Id');
        $this->addOption('store-id', null, InputOption::VALUE_REQUIRED, 'Store Id');
        $this->addOption('requested-from', null, InputOption::VALUE_REQUIRED, 'Requested from date');
        $this->addOption('requested-to', null, InputOption::VALUE_REQUIRED, 'Requested to date');
        $this->addOption('not-exported-only', null, InputOption::VALUE_NONE, 'Not exported only');
        $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show only order numbers, do not export');

        parent::configure();
    }

    /**
     * @inheritdoc
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        try {
            $this->state->setAreaCode(Area::AREA_ADMINHTML);

            $incrementId = $input->getArgument('increment-id');
            $storeId = $input->getOption('store-id');
            $requestedFrom = $input->getOption('requested-from');
            $requestedTo = $input->getOption('requested-to');
            $notExportedOnly = $input->getOption('not-exported-only');
            $dryRun = $input->getOption('dry-run');

            if (!$incrementId && !$requestedFrom && !$requestedTo) {
                throw new LocalizedException(
                    __('You need to specify either increment-id, requested-from or requested-to')
                );
            }

            $searchCriteria = $this->searchCriteriaBuilder->build(
                $incrementId,
                $storeId,
                $requestedFrom,
                $requestedTo
            );

            foreach ($this->creditmemoRepository->getList($searchCriteria)->getItems() as $creditmemo) {

                if ($notExportedOnly) {
                    $exportData = $this->creditmemoExportRepository->findByCreditmemoEntityId(
                        $creditmemo->getEntityId()
                    );

                    if ($exportData->getExportedAt()) {
                        continue;
                    }
                }

                $output->writeln($creditmemo->getIncrementId());

                if ($dryRun) {
                    continue;
                }

                $this->consumer->process($creditmemo->getEntityId());
            }

            $output->writeln('<info>[ OK ]</info>');
        } catch (\Throwable $e) {
            $output->writeln('<error>' . $e->getMessage() . '</error>');
        }
    }
}
And as per highlighted code above, we will add Console/Command/SearchCriteriaBuilder.php file.
<?php

namespace Magelearn\CreditMemoExport\Console\Command;

use Magento\Framework\Api\SearchCriteria;
use Magento\Framework\Api\SearchCriteriaBuilder as CoreSearchCriteriaBuilder;

class SearchCriteriaBuilder
{
    /**
     * @var CoreSearchCriteriaBuilder
     */
    private $searchCriteriaBuilder;

    public function __construct(CoreSearchCriteriaBuilder $searchCriteriaBuilder)
    {
        $this->searchCriteriaBuilder = $searchCriteriaBuilder;
    }

    public function build(
        ?string $incrementId,
        ?string $storeId,
        ?string $requestedFrom,
        ?string $requestedTo
    ): SearchCriteria {
        if ($incrementId) {
            $this->searchCriteriaBuilder->addFilter('increment_id', $incrementId);
        }

        if ($storeId) {
            $this->searchCriteriaBuilder->addFilter('store_id', $storeId);
        }

        if ($requestedFrom) {
            $this->searchCriteriaBuilder->addFilter('date_requested', $requestedFrom, 'gteq');
        }

        if ($requestedTo) {
            $this->searchCriteriaBuilder->addFilter('date_requested', $requestedTo, 'lteq');
        }

        return $this->searchCriteriaBuilder->create();
    }
}
0 Comments On "Magento 2 Credit Memo Export Module Using Message Queue | XML Export & Asynchronous Processing"

Back To Top