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.
-
\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 viadi.xml
. -
\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’sdi.xml
.
Example
-
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; }
-
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 ); } }
-
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"/>
-
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()); } }
-
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
-
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); } } }
-
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
-
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(); } }
-
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](../../_images/architecture/pacemaker-dynamics-bc-entity-export-customer/Runtime-diagram_Customer-export.png)
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](../../_images/architecture/pacemaker-dynamics-bc-entity-export-customer/Runtime-diagram_Address-creation-and-update.png)
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.