Pacemaker Dynamics BC Entity Export - Customer

Customer synchronization is not part of Dynamic BC’s standard REST-API, hence every installation will provide an API of their own with their own needs in terms of data structure, field labels and required values. The basic workflow though, remains the same, customers must be exported to Dynamics BC when they are newly created (e.g. via registration) or updated.

In case your project has this requirement, the package techdivision/pacemaker-dynamics-bc-entity-export-customer was created to perform this exact task without having to reinvent the wheel.

Configuration

see page Configuration.

Requirements

Transformation

Prior to sending customer data to Dynamics BC, the customer payload must be created for a customer. Since your project specific endpoint will have its specific requirements (in terms of the data and structure it has to deliver), this module provides the interfaces \TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\CustomerTransformerInterface and \TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\Data\CustomerPayloadInterface which must be implemented in your project.

  1. \TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\CustomerTransformerInterface The implementation of this interface is responsible for transforming a Magento customer object into a transportable payload. A project has to provide an implementation of this interface and set it as the preferred implementation via di.xml.

  2. \TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\Data\CustomerPayloadInterface Since every Dynamics BC installation defines its own data structure for customers, a project specific extension of this interface should be created (our interface here just provides the special type and is the anchor point for a factory but does not provide any special accessors / mutators). A preferred implementation of this interface must then be registered in your module’s di.xml.

Example

  1. Extend the specialized payload interface for the sake of type strictness:

    declare(strict_types=1);
    
    namespace MyVendor\MyModule\Api\Data;
    
    interface CustomerPayloadInterface
       extends \TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\Data\CustomerPayloadInterface
    {
       public function getWebshopLoginId(): ?int;
       public function setWebshopLoginId(int $customerId): self;
       public function getFirstname(): ?string;
       public function setFirstname(string $firstname): self;
       public function getLastname(): ?string;
       public function setLastname(string $firstname): self;
    }
  2. Implement the project specific payload:

    declare(strict_types=1);
    
    namespace MyVendor\MyModule\Model\Data;
    
    use MyVendor\MyModule\Api\Data\CustomerPayloadInterface;
    
    class CustomerPayload implements CustomerPayloadInterface
    {
       private const KEY_CUSTOMER_ID = 'webshopLoginId';
       private const KEY_FIRSTNAME   = 'contactFirstName';
       private const KEY_LASTNAME    = 'contactLastName';
    
       private array $data = [];
    
       public function getWebshopLoginId(): ?int
       {
           return $this->data[self::KEY_CUSTOMER_ID] ?? null;
       }
    
       public function setWebshopLoginId(int $customerId): CustomerPayloadInterface
       {
           $this->data[self::KEY_CUSTOMER_ID] = $customerId;
           return $this;
       }
    
       public function getFirstname(): ?string
       {
           return $this->data[self::KEY_FIRSTNAME] ?? null;
       }
    
       public function setFirstname(string $firstname): CustomerPayloadInterface
       {
           $this->data[self::KEY_FIRSTNAME] = $firstname;
           return $this;
       }
    
       public function getLastname(): ?string
       {
           return $this->data[self::KEY_LASTNAME] ?? null;
       }
    
       public function setLastname(string $lastname): CustomerPayloadInterface
       {
           $this->data[self::KEY_LASTNAME] = $lastname;
           return $this;
       }
    
       public function deserialize(array $payload): void
       {
           $this->data = [];
    
           if (isset($payload[self::KEY_CUSTOMER_ID])) {
               $this->setWebshopLoginId($payload[self::KEY_CUSTOMER_ID]);
           }
    
           if (isset($payload[self::KEY_FIRSTNAME])) {
               $this->setIncrementId($payload[self::KEY_FIRSTNAME]);
           }
    
           if (isset($payload[self::KEY_LASTNAME])) {
               $this->setIncrementId($payload[self::KEY_LASTNAME]);
           }
       }
    
       public function serialize(): array
       {
           return array_filter(
               $this->data,
               static fn($value) => $value !== null
           );
       }
    }
  3. Register your implementation as the preferred payload in app/code/MyVendor/MyModule/etc/di.xml:

      <preference for="TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\Data\CustomerPayloadInterface"
                  type="MyVendor\MyModule\Model\Data\CustomerPayload"/>
  4. Implement the transformer interface:

    declare(strict_types=1);
    
    namespace MyVendor\MyModule\Model;
    
    use Magento\Customer\Api\Data\CustomerInterface;
    use TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\Data\CustomerPayloadInterfaceFactory;
    use TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\Data\CustomerPayloadInterface;
    use TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\CustomerTransformerInterface;
    
    class CustomerTransformer implements CustomerTransformerInterface
    {
       private CustomerPayloadInterfaceFactory $payloadFactory;
    
       public function __construct(CustomerPayloadInterfaceFactory $payloadFactory)
       {
           $this->payloadFactory = $payloadFactory;
       }
    
       public function getPayload(CustomerInterface $customer) : CustomerPayloadInterface
       {
           $payload = $this->payloadFactory->create();
    
           return $payload
               ->setWebshopLoginId($customer->getEntityId())
               ->setFirstname($customer->getFirstname())
               ->setLastname($customer->getLastname());
       }
    }
  5. Register a preference for the transformer in app/code/MyVendor/MyModule/etc/di.xml:

      <preference for="TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\CustomerTransformerInterface"
                  type="MyVendor\MyModule\Model\CustomerTransformer"/>

Transport

Since the requirements for creation and update of customers will vary between Dynamics BC installations (some use a single endpoint and always expect POST to be used, others expect a POST for creation and a PATCH for update etc.), this module provides the interface \TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\Command\CreateOrUpdateCustomerInterface which must be implemented and registered as the preferred implementation in your project.

Hint: If you follow the idea to use the repository pattern for entity based interaction with Dynamics BC, then you already have to decide whether the save(CustomerInterface) method of your repository will address the creation or update endpoint. Instead of making the decision in your repository, which should be a facade anyway, simply delegate the decision-making to an implementation of \TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\Command\CreateOrUpdateCustomerInterface and reuse the code.

Example

  1. Implement the command interface

    declare(strict_types=1);
    
    namespace MyVendor\MyModule\Model\Command;
    
    use MyVendor\MyModule\Api\Command\CreateCustomerInterface;
    use MyVendor\MyModule\Api\Command\UpdateCustomerInterface;
    use MyVendor\MyModule\Api\Query\IsCustomerNewInterface;
    use TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\Command\CreateOrUpdateCustomerInterface;
    use TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\Data\CustomerPayloadInterface;
    
    class CreateOrUpdateCustomer implements CreateOrUpdateCustomerInterface
    {
       private CreateCustomerInterface $createCustomer;
       private UpdateCustomerInterface $updateCustomer;
       private IsCustomerNewInterface  $isCustomerNew;
    
       public function __construct(
           CreateCustomerInterface $createCustomer,
           UpdateCustomerInterface $updateCustomer,
           IsCustomerNewInterface  $isCustomerNew
       ) {
           $this->createCustomer = $createCustomer;
           $this->updateCustomer = $updateCustomer;
           $this->isCustomerNew  = $isCustomerNew;
       }
    
       public function execute(CustomerPayloadInterface $payload, int $websiteId=null): CustomerPayloadInterface
       {
           if ($this->isCustomerNew->execute($payload)) {
               return $this->createCustomer->execute($payload);
           } else {
               return $this->updateCustomer->execute($payload);
           }
       }
    }
  2. Register your implementation as the preferred implementation in app/code/MyVendor/MyModule/etc/di.xml:

      <preference for="TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\Command\CreateOrUpdateCustomerInterface"
                  type="MyVendor\MyModule\Model\Command\CreateOrUpdateCustomer"/>

Extension points

Export customer upon address change

By default, customers are enqueued (re-enqueued actually) for export upon changing any of their addresses. Depending on your project’s requirements, you may want to export customer data only when certain types of addresses are changed (Magento does not have address types but Dynamics BC has and is extremely picky about these). In order to fit your project’s needs, you can implement the public interface \TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\Query\MustExportCustomerAfterAddressChangeInterface and set your implementation as the preferred one in your module’s etc/di.xml.

Example

  1. Implement the query interface

    namespace MyVendor\MyModule\Model\Query;
    
    use Magento\Customer\Api\Data\AddressInterface;
    use TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\Query\MustExportCustomerAfterAddressChangeInterface;
    
    class ExportCustomerOnlyAfterBillingAddressChange implements MustExportCustomerAfterAddressChangeInterface
    {
       public function execute(AddressInterface $address) : bool
       {
           return (bool)$address->isDefaultBilling();
       }
    }
  2. Register your implementation as the preferred implementation in app/code/MyVendor/MyModule/etc/di.xml:

      <preference for="TechDivision\PacemakerDynamicsBcEntityExportCustomer\Api\Query\MustExportCustomerAfterAddressChangeInterface"
                  type="MyVendor\MyModule\Model\Query\ExportCustomerOnlyAfterBillingAddressChange"/>

Runtime view

Customer export flow

Customer export flow

The customer entity is marked for export after a customer has successfully saved her data (customer entity data or customer address data). The actual synchronization (aka transport of data) is then performed asynchronously by an export pipeline, such that there is no latency for the customer.

Address creation and update flow

Address creation / update flow

After a customer has created a new or changed an existing address, the associated customer entity is marked for export. The actual synchronization (aka transport of data) is then performed asynchronously by an export pipeline, such that there is no latency for the customer.