diff --git a/Api/Json/BaseInterface.php b/Api/Json/BaseInterface.php
new file mode 100644
index 0000000..d8badeb
--- /dev/null
+++ b/Api/Json/BaseInterface.php
@@ -0,0 +1,33 @@
+allmethods = $allmethods;
+ }
+
+ /**
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function setInputName($value)
+ {
+ /* @phpstan-ignore-next-line */
+ return $this->setName($value);
+ }
+
+ /**
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function setInputId($value)
+ {
+ return $this->setId($value);
+ }
+
+ /**
+ * @return string
+ */
+ public function _toHtml(): string
+ {
+ if (!$this->getOptions()) {
+ $this->setOptions($this->getSourceOptions());
+ }
+
+ return parent::_toHtml();
+ }
+
+ /**
+ * @return array
+ */
+ private function getSourceOptions(): array
+ {
+ return $this->allmethods->toOptionArray();
+ }
+}
diff --git a/Block/Adminhtml/Form/Field/DeliveryOptions.php b/Block/Adminhtml/Form/Field/DeliveryOptions.php
new file mode 100644
index 0000000..56fab7c
--- /dev/null
+++ b/Block/Adminhtml/Form/Field/DeliveryOptions.php
@@ -0,0 +1,73 @@
+addColumn('delivery_method', [
+ 'label' => __('Delivery Method'),
+ 'renderer' => $this->getDeliveryMethodRenderer()
+ ]);
+
+ $this->addColumn('delivery_name', [
+ 'label' => __('Delivery JSON Name'),
+ 'class' => 'required-entry'
+ ]);
+
+ $this->_addAfter = false;
+ $this->_addButtonLabel = __('Add')->render();
+ }
+
+ /**
+ * @return Types|BlockInterface
+ * @throws LocalizedException
+ */
+ private function getDeliveryMethodRenderer()
+ {
+ $this->typeRenderer = $this->getLayout()->createBlock(
+ DeliveryColumn::class,
+ '',
+ ['data' => ['is_render_to_js_template' => true]]
+ );
+
+ return $this->typeRenderer;
+ }
+
+ /**
+ * @param DataObject $row
+ *
+ * @throws LocalizedException
+ */
+ protected function _prepareArrayRow(DataObject $row): void
+ {
+ $options = [];
+ $type = $row->getDeliveryMethod();
+
+ if ($type !== null) {
+ /* @phpstan-ignore-next-line */
+ $options['option_' . $this->getDeliveryMethodRenderer()->calcOptionHash($type)] = 'selected="selected"';
+ }
+
+ $row->setData('option_extra_attrs', $options);
+ }
+}
diff --git a/Block/Adminhtml/Form/Field/OrderMagentoColumn.php b/Block/Adminhtml/Form/Field/OrderMagentoColumn.php
new file mode 100644
index 0000000..5e502dc
--- /dev/null
+++ b/Block/Adminhtml/Form/Field/OrderMagentoColumn.php
@@ -0,0 +1,78 @@
+orderStatusCollection = $orderStatusCollection;
+ }
+
+ /**
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function setInputName($value)
+ {
+ /* @phpstan-ignore-next-line */
+ return $this->setName($value);
+ }
+
+ /**
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function setInputId($value)
+ {
+ return $this->setId($value);
+ }
+
+ /**
+ * @return string
+ */
+ public function _toHtml(): string
+ {
+ if (!$this->getOptions()) {
+ $this->setOptions($this->getSourceOptions());
+ }
+
+ return parent::_toHtml();
+ }
+
+ /**
+ * @return array
+ */
+ private function getSourceOptions(): array
+ {
+ return $this->orderStatusCollection->toOptionArray();
+ }
+}
diff --git a/Block/Adminhtml/Form/Field/OrderMappingOptions.php b/Block/Adminhtml/Form/Field/OrderMappingOptions.php
new file mode 100644
index 0000000..6f48d8e
--- /dev/null
+++ b/Block/Adminhtml/Form/Field/OrderMappingOptions.php
@@ -0,0 +1,89 @@
+addColumn('order_status', [
+ 'label' => __('Magento 2 Order Status'),
+ 'renderer' => $this->getOrderStatusRenderer()
+ ]);
+ $this->addColumn('schema_status', [
+ 'label' => __('Schema Order Status'),
+ 'renderer' => $this->getSchemaStatusRenderer()
+ ]);
+ $this->_addAfter = false;
+ $this->_addButtonLabel = __('Add')->render();
+ }
+
+ /**
+ * @return BlockInterface
+ * @throws LocalizedException
+ */
+ private function getOrderStatusRenderer()
+ {
+ $this->orderStatusRenderer = $this->getLayout()->createBlock(
+ OrderMagentoColumn::class,
+ '',
+ ['data' => ['is_render_to_js_template' => true]]
+ );
+
+ return $this->orderStatusRenderer;
+ }
+
+ /**
+ * @return BlockInterface
+ * @throws LocalizedException
+ */
+ private function getSchemaStatusRenderer()
+ {
+ $this->schemaStatusRenderer = $this->getLayout()->createBlock(
+ OrderSchemaColumn::class,
+ '',
+ ['data' => ['is_render_to_js_template' => true]]
+ );
+
+ return $this->schemaStatusRenderer;
+ }
+
+ /**
+ * @param DataObject $row
+ *
+ * @throws LocalizedException
+ */
+ protected function _prepareArrayRow(DataObject $row): void
+ {
+ $options = [];
+ $type = $row->getSchemaStatus();
+
+ if ($type !== null) {
+ /* @phpstan-ignore-next-line */
+ $options['option_' . $this->getSchemaStatusRenderer()->calcOptionHash($type)] = 'selected="selected"';
+ }
+
+ $row->setData('option_extra_attrs', $options);
+ }
+}
diff --git a/Block/Adminhtml/Form/Field/OrderSchemaColumn.php b/Block/Adminhtml/Form/Field/OrderSchemaColumn.php
new file mode 100644
index 0000000..596b674
--- /dev/null
+++ b/Block/Adminhtml/Form/Field/OrderSchemaColumn.php
@@ -0,0 +1,76 @@
+schemaOrderTypes = $schemaOrderTypes;
+ }
+
+ /**
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function setInputName($value)
+ {
+ /* @phpstan-ignore-next-line */
+ return $this->setName($value);
+ }
+
+ /**
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function setInputId($value)
+ {
+ return $this->setId($value);
+ }
+
+ /**
+ * @return string
+ */
+ public function _toHtml(): string
+ {
+ if (!$this->getOptions()) {
+ $this->setOptions($this->getSourceOptions());
+ }
+
+ return parent::_toHtml();
+ }
+
+ /**
+ * @return array
+ */
+ private function getSourceOptions(): array
+ {
+ return $this->schemaOrderTypes->toOptionArray();
+ }
+}
diff --git a/Block/Adminhtml/Form/Field/TrackingOptions.php b/Block/Adminhtml/Form/Field/TrackingOptions.php
new file mode 100644
index 0000000..236a6f9
--- /dev/null
+++ b/Block/Adminhtml/Form/Field/TrackingOptions.php
@@ -0,0 +1,73 @@
+addColumn('delivery_method', [
+ 'label' => __('Delivery Method'),
+ 'renderer' => $this->getDeliveryMethodRenderer()
+ ]);
+
+ $this->addColumn('tracking_url', [
+ 'label' => __('Tracking URL'),
+ 'class' => 'required-entry'
+ ]);
+
+ $this->_addAfter = false;
+ $this->_addButtonLabel = __('Add')->render();
+ }
+
+ /**
+ * @return BlockInterface
+ * @throws LocalizedException
+ */
+ private function getDeliveryMethodRenderer()
+ {
+ $this->deliveryMethodRenderer = $this->getLayout()->createBlock(
+ DeliveryColumn::class,
+ '',
+ ['data' => ['is_render_to_js_template' => true]]
+ );
+
+ return $this->deliveryMethodRenderer;
+ }
+
+ /**
+ * @param DataObject $row
+ *
+ * @throws LocalizedException
+ */
+ protected function _prepareArrayRow(DataObject $row): void
+ {
+ $options = [];
+ $type = $row->getDeliveryMethod();
+
+ if ($type !== null) {
+ /* @phpstan-ignore-next-line */
+ $options['option_' . $this->getDeliveryMethodRenderer()->calcOptionHash($type)] = 'selected="selected"';
+ }
+
+ $row->setData('option_extra_attrs', $options);
+ }
+}
diff --git a/LICENSE_MTRZK b/LICENSE_MTRZK
new file mode 100644
index 0000000..d06666c
--- /dev/null
+++ b/LICENSE_MTRZK
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Marcin Materzok - MTRZK Sp. z o .o.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Model/Config.php b/Model/Config.php
new file mode 100644
index 0000000..072843b
--- /dev/null
+++ b/Model/Config.php
@@ -0,0 +1,309 @@
+scopeConfig = $scopeConfig;
+ $this->serializer = $serializer;
+ }
+
+ /**
+ * @param int $storeId
+ *
+ * @return bool
+ */
+ public function isEnabled(int $storeId = 0): bool
+ {
+ return $this->scopeConfig->isSetFlag(
+ self::XML_GENERAL_IS_ENABLED,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+
+ /**
+ * @param int $storeId
+ *
+ * @return bool
+ */
+ public function isOrderEnabled(int $storeId = 0): bool
+ {
+ return $this->scopeConfig->isSetFlag(
+ self::XML_GENERAL_IS_ORDER_ENABLED,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+
+ /**
+ * @param int $storeId
+ *
+ * @return bool
+ */
+ public function isShipmentEnabled(int $storeId = 0): bool
+ {
+ return $this->scopeConfig->isSetFlag(
+ self::XML_GENERAL_IS_SHIPMENT_ENABLED,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+
+ /**
+ * @param int|null $storeId
+ *
+ * @return string
+ */
+ public function getMerchantName(int $storeId = 0): string
+ {
+ return (string) $this->scopeConfig->getValue(
+ self::XML_GENERAL_MERCHANT_NAME,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+
+ /**
+ * @param int $storeId
+ *
+ * @return bool
+ */
+ public function isOrderAddBillingData(int $storeId = 0): bool
+ {
+ return $this->scopeConfig->isSetFlag(
+ self::XML_ORDER_ADD_BILLING_DATA,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+
+ /**
+ * @param int|null $storeId
+ *
+ * @return string
+ */
+ public function getOrderStatusMapping(int $storeId = 0): string
+ {
+ return (string) $this->scopeConfig->getValue(
+ self::XML_ORDER_ORDER_STATUS_MAPPING,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+
+ /**
+ * @param int|null $storeId
+ *
+ * @return array
+ */
+ public function getOrderStatusMappingArray(int $storeId = 0): array
+ {
+ try {
+ return (array) $this->serializer->unserialize(
+ $this->getOrderStatusMapping($storeId)
+ );
+ } catch (InvalidArgumentException $e) {
+ return [];
+ }
+ }
+
+ /**
+ * @param int $storeId
+ *
+ * @return bool
+ */
+ public function isOrderAddViewActon(int $storeId = 0): bool
+ {
+ return $this->scopeConfig->isSetFlag(
+ self::XML_ORDER_ADD_ACTION_FOR_CUSTOMER,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+
+ /**
+ * @param int|null $storeId
+ *
+ * @return string
+ */
+ public function getOrderViewActionLabel(int $storeId = 0): string
+ {
+ return (string) $this->scopeConfig->getValue(
+ self::XML_ORDER_CUSTOMER_VIEW_ACTION_LABEL,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+
+ /**
+ * @param int|null $storeId
+ *
+ * @return string
+ */
+ public function getOrderViewActionDescription(int $storeId = 0): string
+ {
+ return (string) $this->scopeConfig->getValue(
+ self::XML_ORDER_CUSTOMER_VIEW_ACTION_DESCRIPTION,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+
+ /**
+ * @param int|null $storeId
+ *
+ * @return string
+ */
+ public function getOrderViewActionType(int $storeId = 0): string
+ {
+ return (string) $this->scopeConfig->getValue(
+ self::XML_ORDER_CUSTOMER_VIEW_ACTION_TYPE,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+
+ /**
+ * @param int|null $storeId
+ *
+ * @return string
+ */
+ public function getOrderViewActionCustomUrl(int $storeId = 0): string
+ {
+ return (string) $this->scopeConfig->getValue(
+ self::XML_ORDER_CUSTOMER_VIEW_ACTION_CUSTOM_URL,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+
+ /**
+ * @param int $storeId
+ *
+ * @return bool
+ */
+ public function isShipmentAddViewActon(int $storeId = 0): bool
+ {
+ return $this->scopeConfig->isSetFlag(
+ self::XML_SHIPMENT_ADD_TRACK_ACTION,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+
+ /**
+ * @param int|null $storeId
+ *
+ * @return string
+ */
+ public function getShipmentDeliveryMapping(int $storeId = 0): string
+ {
+ return (string) $this->scopeConfig->getValue(
+ self::XML_SHIPMENT_SHIPMENT_DELIVERY_MAPPING,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+
+ /**
+ * @param int|null $storeId
+ *
+ * @return array
+ */
+ public function getShipmentDeliveryMappingArray(int $storeId = 0): array
+ {
+ try {
+ return (array) $this->serializer->unserialize(
+ $this->getShipmentDeliveryMapping($storeId)
+ );
+ } catch (InvalidArgumentException $e) {
+ return [];
+ }
+ }
+
+ /**
+ * @param int|null $storeId
+ *
+ * @return string
+ */
+ public function getShipmentTrackingMapping(int $storeId = 0): string
+ {
+ return (string) $this->scopeConfig->getValue(
+ self::XML_SHIPMENT_SHIPMENT_TRACKING_MAPPING,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+
+ /**
+ * @param int|null $storeId
+ *
+ * @return array
+ */
+ public function getShipmentTrackingMappingArray(int $storeId = 0): array
+ {
+ try {
+ return (array) $this->serializer->unserialize(
+ $this->getShipmentTrackingMapping($storeId)
+ );
+ } catch (InvalidArgumentException $e) {
+ return [];
+ }
+ }
+
+ /**
+ * @param int|null $storeId
+ *
+ * @return string
+ */
+ public function getAddMarkupEmailSettings(int $storeId = 0): string
+ {
+ return (string) $this->scopeConfig->getValue(
+ self::XML_ADVANCED_ADD_MARKUP_TO_EMAILS,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+}
diff --git a/Model/Config/Backend/Serialized.php b/Model/Config/Backend/Serialized.php
new file mode 100644
index 0000000..37cd630
--- /dev/null
+++ b/Model/Config/Backend/Serialized.php
@@ -0,0 +1,91 @@
+serializer = $serializer;
+ }
+
+ /**
+ * @return Serialized | Value
+ */
+ public function beforeSave()
+ {
+ $value = $this->getValue();
+
+ if (is_array($value)) {
+ unset($value['__empty']);
+ }
+
+ $this->setValue($value);
+
+ if (is_array($this->getValue())) {
+ $this->setValue($this->serializer->serialize($this->getValue()));
+ }
+
+ parent::beforeSave();
+
+ return $this;
+ }
+
+ /**
+ * @return Serialized
+ */
+ protected function _afterLoad()
+ {
+ $value = $this->getValue();
+ if (!is_array($value)) {
+ $this->setValue(empty($value) ? false : $this->serializer->unserialize($value));
+ }
+
+ return $this;
+ }
+}
diff --git a/Model/Config/Source/EmailMx.php b/Model/Config/Source/EmailMx.php
new file mode 100644
index 0000000..1d0a66a
--- /dev/null
+++ b/Model/Config/Source/EmailMx.php
@@ -0,0 +1,52 @@
+toArray() as $value => $label) {
+ $optionArray[] = [
+ 'value' => $value,
+ 'label' => $label
+ ];
+ }
+
+ return $optionArray;
+ }
+
+ /**
+ * Get options in "key-value" format
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ return [
+ self::DEFAULT_TYPE => __('Add to all emails'),
+ self::GMAIL_TYPE => __('Add only for @gmail.com'),
+ self::GMAIL_MX_TYPE => __('Add only for @gmail.com and emails with Google Workspace MX records')
+ ];
+ }
+}
+
diff --git a/Model/Config/Source/SchemaOrderTypes.php b/Model/Config/Source/SchemaOrderTypes.php
new file mode 100644
index 0000000..06b6f81
--- /dev/null
+++ b/Model/Config/Source/SchemaOrderTypes.php
@@ -0,0 +1,54 @@
+toArray() as $value => $label) {
+ $optionArray[] = [
+ 'value' => $value,
+ 'label' => $label
+ ];
+ }
+
+ return $optionArray;
+ }
+
+ /**
+ * Get options in "key-value" format
+ *
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ OrderStatusInterface::CANCELLED => OrderStatusInterface::CANCELLED,
+ OrderStatusInterface::DELIVERED => OrderStatusInterface::DELIVERED,
+ OrderStatusInterface::IN_TRANSIT => OrderStatusInterface::IN_TRANSIT,
+ OrderStatusInterface::PAYMENT_DUE => OrderStatusInterface::PAYMENT_DUE,
+ OrderStatusInterface::PICKUP_AVAILABLE => OrderStatusInterface::PICKUP_AVAILABLE,
+ OrderStatusInterface::PROBLEM => OrderStatusInterface::PROBLEM,
+ OrderStatusInterface::PROCESSING => OrderStatusInterface::PROCESSING,
+ OrderStatusInterface::RETURNED => OrderStatusInterface::RETURNED
+ ];
+ }
+}
+
diff --git a/Model/Config/Source/UrlType.php b/Model/Config/Source/UrlType.php
new file mode 100644
index 0000000..763e5cb
--- /dev/null
+++ b/Model/Config/Source/UrlType.php
@@ -0,0 +1,50 @@
+toArray() as $value => $label) {
+ $optionArray[] = [
+ 'value' => $value,
+ 'label' => $label
+ ];
+ }
+
+ return $optionArray;
+ }
+
+ /**
+ * Get options in "key-value" format
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ return [
+ self::CUSTOM_TYPE => __('Custom Url'),
+ self::DEFAULT_TYPE => __('Default Magento Url')
+ ];
+ }
+}
+
diff --git a/Model/EmailLookup.php b/Model/EmailLookup.php
new file mode 100644
index 0000000..44d713b
--- /dev/null
+++ b/Model/EmailLookup.php
@@ -0,0 +1,90 @@
+config = $config;
+ }
+
+ /**
+ * @param string $email
+ * @param int $storeId
+ *
+ * @return bool
+ */
+ public function checkEmail(string $email, int $storeId): bool
+ {
+ if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ return false;
+ }
+
+ $markupSettings = $this->config->getAddMarkupEmailSettings($storeId);
+
+ if ($markupSettings === EmailMx::GMAIL_TYPE) {
+ return $this->checkGmailAccount($email);
+ }
+
+ if ($markupSettings === EmailMx::GMAIL_MX_TYPE) {
+ return $this->checkMxRecords($email);
+ }
+
+ return true;
+ }
+
+ /**
+ * @param string $email
+ *
+ * @return bool
+ */
+ private function checkMxRecords(string $email): bool
+ {
+ if ($this->checkGmailAccount($email)) {
+ return true;
+ }
+
+ getmxrr(substr($email, strrpos($email, '@') + 1), $hosts);
+
+ if (!$hosts) {
+ return false;
+ }
+
+ return !empty(array_intersect(self::GMAIL_MX_SERVERS, $hosts));
+ }
+
+ /**
+ * @param string $email
+ *
+ * @return bool
+ */
+ private function checkGmailAccount(string $email): bool
+ {
+ return stripos($email, self::GMAIL_SURFIX) !== false;
+ }
+}
diff --git a/Model/Processor/AbstractProcessor.php b/Model/Processor/AbstractProcessor.php
new file mode 100644
index 0000000..f59767e
--- /dev/null
+++ b/Model/Processor/AbstractProcessor.php
@@ -0,0 +1,168 @@
+config = $config;
+ $this->emailLookup = $emailLookup;
+ $this->serializer = $serializer;
+ $this->url = $url;
+ $this->orderStatus = $orderStatus;
+ $this->logger = $logger;
+ $this->storeManager = $storeManager;
+ }
+
+ /**
+ * @param string $email
+ * @param int $storeId
+ *
+ * @return bool
+ */
+ protected function isEmailEnabledToSend(string $email, int $storeId): bool
+ {
+ return $this->emailLookup->checkEmail($email, $storeId);
+ }
+
+ /**
+ * @param array $array
+ *
+ * @return string
+ */
+ private function toJson(array $array): string
+ {
+
+
+ try {
+ return (string) $this->serializer->serialize($array);
+ } catch (InvalidArgumentException $e) {
+ return "";
+ }
+ }
+
+ /**
+ * @param array $array
+ *
+ * @return string
+ */
+ protected function generateScript(array $array): string
+ {
+ $html = '';
+
+ return $html;
+ }
+
+ /**
+ * @param string $schema
+ *
+ * @return string
+ */
+ protected function getSchemaUrl(string $schema): string
+ {
+ return self::SCHEMA_URL . $schema;
+ }
+
+ /**
+ * @param OrderInterface $order
+ *
+ * @return string
+ */
+ protected function getOrderViewUrl(OrderInterface $order): string
+ {
+ $storeId = (int) $order->getStoreId();
+
+ if ($this->config->getOrderViewActionType($storeId) === UrlType::CUSTOM_TYPE) {
+ return str_replace(
+ [
+ "{{order_id}}",
+ "{{increment_id}}"
+ ],
+ [
+ $order->getEntityId(),
+ $order->getIncrementId()
+ ],
+ $this->config->getOrderViewActionCustomUrl($storeId)
+ );
+ }
+
+ return $this->localhostFixUrl(
+ $this->url->getUrl('sales/order/view', [
+ '_scope' => $storeId,
+ 'id' => $order->getEntityId(),
+ '_nosid' => true
+ ])
+ );
+ }
+
+ /**
+ * @param int $storeId
+ *
+ * @return string
+ * @throws NoSuchEntityException
+ */
+ protected function getMediaUrl(int $storeId): string
+ {
+ return $this->localhostFixUrl($this->storeManager->getStore($storeId)->getBaseUrl(UrlInterface::URL_TYPE_MEDIA));
+ }
+
+ /**
+ * Google if url is localhost show error, function change http://localhost to http://example.com/
+ *
+ * @param string|null $url
+ *
+ * @return string
+ */
+ protected function localhostFixUrl(?string $url): ?string
+ {
+ return str_replace("localhost", "example.com", $url);
+ }
+}
diff --git a/Model/Processor/OrderProcessor.php b/Model/Processor/OrderProcessor.php
new file mode 100644
index 0000000..7830fc7
--- /dev/null
+++ b/Model/Processor/OrderProcessor.php
@@ -0,0 +1,137 @@
+getCustomerEmail();
+ $storeId = (int) $order->getStoreId();
+
+ if (!$this->isEmailEnabledToSend($email, $storeId)) {
+ return "";
+ }
+
+ $currencyCode = $order->getOrderCurrencyCode();
+
+ $arrayJson = [
+ BaseInterface::CONTEXT => BaseInterface::SCHEMA_HTTP,
+ BaseInterface::TYPE => BaseInterface::TYPE_ORDER,
+ BaseInterface::MERCHANT => [
+ BaseInterface::TYPE => BaseInterface::TYPE_ORGANIZATION,
+ BaseInterface::NAME => $this->config->getMerchantName($storeId)
+ ],
+ JsonOrderInterface::ORDER_NUMBER => $order->getIncrementId(),
+ JsonOrderInterface::PRICE_CURRENCY => $currencyCode,
+ JsonOrderInterface::PRICE => number_format((float) $order->getGrandTotal(), 2),
+ JsonOrderInterface::ORDER_DATE => $order->getCreatedAt(),
+ JsonOrderInterface::ORDER_STATUS => $this->orderStatus->getSchemaOrder($order->getState(), $storeId),
+ ];
+
+ if ($this->config->isOrderAddViewActon($storeId) && !$order->getCustomerIsGuest()) {
+ $url = $this->getOrderViewUrl($order);
+
+ $arrayJson[BaseInterface::POTENTIAL_ACTION] = [
+ BaseInterface::TYPE => BaseInterface::TYPE_VIEW_ACTION,
+ JsonOrderInterface::URL => $url,
+ JsonOrderInterface::NAME => $this->config->getOrderViewActionLabel($storeId)
+ ];
+
+ $arrayJson[BaseInterface::URL] = $url;
+ }
+
+ $productsJson = [];
+
+ foreach ($order->getAllVisibleItems() as $item) {
+ $productImage = $this->getMediaUrl($storeId) . $item->getProduct()->getImage();
+ $itemJson = [
+ BaseInterface::TYPE => BaseInterface::TYPE_OFFER,
+ JsonOrderInterface::ITEM_OFFERED => [
+ BaseInterface::TYPE => BaseInterface::TYPE_PRODUCT,
+ ProductInterface::NAME => $item->getName(),
+ ProductInterface::SKU => $item->getProduct()->getSku(),
+ ProductInterface::URL => $this->localhostFixUrl($item->getProduct()->getProductUrl()),
+ ProductInterface::IMAGE => $productImage,
+ ],
+ JsonOrderInterface::PRICE => number_format((float) $item->getPriceInclTax(), 2),
+ JsonOrderInterface::PRICE_CURRENCY => $currencyCode,
+ JsonOrderInterface::ELIGIBLE_QUANTITY => [
+ BaseInterface::TYPE => BaseInterface::TYPE_QUANTITATIVE_VALUE,
+ JsonOrderInterface::VALUE => (int) $item->getQtyOrdered()
+ ]
+ ];
+
+ $productsJson[] = $itemJson;
+ }
+
+ if ($order->getDiscountAmount() > 0) {
+ $arrayJson[JsonOrderInterface::DISCOUNT] = $order->getDiscountAmount();
+ $arrayJson[JsonOrderInterface::DISCOUNT_CURRENCY] = $currencyCode;
+ }
+
+ $arrayJson[JsonOrderInterface::ACCEPTED_OFFER] = $productsJson;
+
+ if ($this->config->isOrderAddBillingData($storeId)) {
+ $billingAddress = $order->getBillingAddress();
+ $companyName = $billingAddress->getCompany();
+
+ if (!empty($companyName)) {
+ $customerData = [
+ BaseInterface::TYPE => BaseInterface::TYPE_ORGANIZATION,
+ BaseInterface::NAME => $companyName
+ ];
+ } else {
+ $customerName = $billingAddress->getFirstname() . " " . $billingAddress->getLastname();
+ $customerData = [
+ BaseInterface::TYPE => BaseInterface::TYPE_PERSON,
+ BaseInterface::NAME => $customerName
+ ];
+ $companyName = $customerName;
+ }
+
+ $billingData = [
+ JsonOrderInterface::CUSTOMER => $customerData,
+ JsonOrderInterface::BILLING_ADDRESS => [
+ BaseInterface::TYPE => BaseInterface::TYPE_POSTAL_ADDRESS,
+ BaseInterface::NAME => $companyName,
+ JsonOrderInterface::STREET_ADDRESS => implode(" ", $billingAddress->getStreet()),
+ JsonOrderInterface::ADDRESS_REGION => $billingAddress->getRegion(),
+ JsonOrderInterface::ADDRESS_LOCALITY => $billingAddress->getCity(),
+ JsonOrderInterface::ADDRESS_COUNTRY => $billingAddress->getCountryId(),
+ ]
+ ];
+
+ $arrayJson = array_merge($arrayJson, $billingData);
+ }
+
+ return $this->generateScript($arrayJson);
+
+ } catch (Exception $e) {
+ $this->logger->error("OrderProcessor error on Increment ID ".$order->getIncrementId().": ".$e->getMessage());
+
+ return "";
+ }
+ }
+}
diff --git a/Model/Processor/ShipmentProcessor.php b/Model/Processor/ShipmentProcessor.php
new file mode 100644
index 0000000..a95e48e
--- /dev/null
+++ b/Model/Processor/ShipmentProcessor.php
@@ -0,0 +1,166 @@
+trackingNumber = $trackingNumber;
+ $this->deliveryName = $deliveryName;
+ }
+
+ /**
+ * @see https://developers.google.com/gmail/markup/reference/parcel-delivery
+ *
+ * @param ShipmentInterface $shipment
+ * @param OrderInterface $order
+ *
+ * @return string
+ */
+ public function processShipment(ShipmentInterface $shipment, OrderInterface $order): string
+ {
+ try {
+ $email = $order->getCustomerEmail();
+ $storeId = (int) $order->getStoreId();
+
+ if (!$this->isEmailEnabledToSend($email, $storeId)) {
+ return "";
+ }
+
+ $deliveryMethod = $order->getShippingMethod(true)->getData();
+ $deliveryMethodName = $deliveryMethod['carrier_code']."_".$deliveryMethod['method'];
+ $shippingAddress = $order->getShippingAddress();
+ $shippingName = $shippingAddress->getCompany();
+
+ if (empty($shippingName)) {
+ $shippingName = $shippingAddress->getFirstname() . " " . $shippingAddress->getLastname();
+ }
+
+ $arrayJson = [
+ BaseInterface::CONTEXT => BaseInterface::SCHEMA_HTTP,
+ BaseInterface::TYPE => BaseInterface::TYPE_PARCEL_DELIVERY,
+ JsonShipmentInterface::CARRIER => [
+ BaseInterface::TYPE => BaseInterface::TYPE_ORGANIZATION,
+ BaseInterface::NAME => $this->deliveryName->getDeliveryName($deliveryMethodName, $storeId)
+ ],
+ JsonShipmentInterface::DELIVERY_ADDRESS => [
+ BaseInterface::TYPE => BaseInterface::TYPE_POSTAL_ADDRESS,
+ BaseInterface::NAME => $shippingName,
+ JsonShipmentInterface::STREET_ADDRESS => implode(" ", $shippingAddress->getStreet()),
+ JsonShipmentInterface::ADDRESS_REGION => $shippingAddress->getRegion(),
+ JsonShipmentInterface::ADDRESS_LOCALITY => $shippingAddress->getCity(),
+ JsonShipmentInterface::ADDRESS_COUNTRY => $shippingAddress->getCountryId(),
+ JsonShipmentInterface::POSTAL_CODE => $shippingAddress->getPostcode()
+ ],
+ JsonShipmentInterface::PART_OF_ORDER => [
+ BaseInterface::TYPE => BaseInterface::TYPE_ORDER,
+ JsonOrderInterface::ORDER_NUMBER => $order->getIncrementId(),
+ BaseInterface::MERCHANT => [
+ BaseInterface::TYPE => BaseInterface::TYPE_ORGANIZATION,
+ BaseInterface::NAME => $this->config->getMerchantName($storeId)
+ ],
+ ],
+ JsonShipmentInterface::TRACKING_NUMBER => $this->trackingNumber->getTrackingNumber($shipment),
+ JsonShipmentInterface::ORDER_STATUS => $this->orderStatus->getSchemaOrder($order->getState(), $storeId),
+ ];
+
+ if ($this->config->isShipmentAddViewActon($storeId)) {
+ $trackingUrl = $this->trackingNumber->getTrackingUrl($shipment, $order);
+
+ if (!$trackingUrl) {
+ $trackingUrl = $this->getOrderViewUrl($order);
+ }
+
+ $arrayJson[BaseInterface::POTENTIAL_ACTION] = [
+ BaseInterface::TYPE => BaseInterface::TYPE_TRACK_ACTION,
+ JsonShipmentInterface::URL => $trackingUrl
+ ];
+
+ $arrayJson[JsonShipmentInterface::TRACKING_URL] = $trackingUrl;
+ }
+
+ $productsJson = [];
+
+ foreach ($order->getAllVisibleItems() as $item) {
+ $productImage = $this->getMediaUrl($storeId) . $item->getProduct()->getImage();
+
+ $itemJson = [
+ BaseInterface::TYPE => BaseInterface::TYPE_PRODUCT,
+ ProductInterface::NAME => $item->getName(),
+ ProductInterface::SKU => $item->getProduct()->getSku(),
+ ProductInterface::URL => $this->localhostFixUrl($item->getProduct()->getProductUrl()),
+ ProductInterface::IMAGE => $productImage
+ ];
+
+ $productsJson[] = $itemJson;
+ }
+
+ $arrayJson[JsonShipmentInterface::ITEM_SHIPPED] = $productsJson;
+
+ $arrayJson[JsonShipmentInterface::EXPECTED_ARRIVAL_UNTIL] = Date('y:m:d', strtotime('+3 days'));
+
+ return $this->generateScript($arrayJson);
+ } catch (Exception $e) {
+ $this->logger->error("OrderProcessor error on Increment ID " . $order->getIncrementId() . ": " . $e->getMessage());
+
+ return "";
+ }
+ }
+}
diff --git a/Model/Renderer/DeliveryName.php b/Model/Renderer/DeliveryName.php
new file mode 100644
index 0000000..4c16738
--- /dev/null
+++ b/Model/Renderer/DeliveryName.php
@@ -0,0 +1,57 @@
+config = $config;
+ $this->allmethods = $allmethods;
+ }
+
+ /**
+ * @param string $deliveryCode
+ * @param int $storeId
+ *
+ * @return string
+ */
+ public function getDeliveryName(string $deliveryCode, int $storeId): string
+ {
+ $mapping = $this->config->getShipmentDeliveryMappingArray($storeId);
+
+ if (count($mapping) > 0) {
+ foreach ($mapping as $map) {
+ if ($map['delivery_method'] === $deliveryCode) {
+ return $map['delivery_name'];
+ }
+ }
+ }
+
+ foreach ($this->allmethods->toOptionArray() as $map) {
+ if ($map['value'] === $deliveryCode) {
+ return $map['label'];
+ }
+ }
+
+ return '';
+ }
+}
diff --git a/Model/Renderer/OrderStatus.php b/Model/Renderer/OrderStatus.php
new file mode 100644
index 0000000..982d6c0
--- /dev/null
+++ b/Model/Renderer/OrderStatus.php
@@ -0,0 +1,48 @@
+config = $config;
+ }
+
+ /**
+ * @param string $orderStatus
+ * @param int $storeId
+ *
+ * @return string
+ */
+ public function getSchemaOrder(string $orderStatus, int $storeId): string
+ {
+ $mapping = $this->config->getOrderStatusMappingArray($storeId);
+
+ if (count($mapping) > 0) {
+ foreach ($mapping as $map) {
+ if ($map['order_status'] === $orderStatus) {
+ return $map['schema_status'];
+ }
+ }
+ }
+
+ return OrderStatusInterface::PROCESSING;
+ }
+}
diff --git a/Model/Renderer/TrackingNumber.php b/Model/Renderer/TrackingNumber.php
new file mode 100644
index 0000000..c458c71
--- /dev/null
+++ b/Model/Renderer/TrackingNumber.php
@@ -0,0 +1,78 @@
+config = $config;
+ $this->allmethods = $allmethods;
+ }
+
+ /**
+ * Here is class with function to make preference if you have some custom tracking Url method
+ *
+ * @param ShipmentInterface $shipment
+ * @param OrderInterface $order
+ *
+ * @return null|string
+ */
+ public function getTrackingUrl(ShipmentInterface $shipment, OrderInterface $order): ?string
+ {
+ $deliveryMethod = $order->getShippingMethod(true)->getData();
+ $deliveryMethodName = $deliveryMethod['carrier_code'] . "_" . $deliveryMethod['method'];
+ $storeId = (int) $order->getStoreId();
+ $mapping = $this->config->getShipmentTrackingMappingArray($storeId);
+
+ if (count($mapping) > 0) {
+ foreach ($mapping as $map) {
+ if ($map['delivery_method'] === $deliveryMethodName) {
+ return str_replace(
+ [
+ "{{shipment_id}}",
+ "{{tracking_number}}"
+ ],
+ [
+ $shipment->getEntityId(),
+ $this->getTrackingNumber($shipment)
+ ],
+ $map['tracking_url']
+ );
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param ShipmentInterface $shipment
+ *
+ * @return string
+ */
+ public function getTrackingNumber(ShipmentInterface $shipment): string
+ {
+ return current($shipment->getTracks())->getTrackNumber();
+ }
+}
diff --git a/Observer/OrderEmailObserver.php b/Observer/OrderEmailObserver.php
new file mode 100644
index 0000000..a710281
--- /dev/null
+++ b/Observer/OrderEmailObserver.php
@@ -0,0 +1,66 @@
+orderProcessor = $orderProcessor;
+ $this->config = $config;
+ }
+
+ /** {@inheritDoc} */
+ public function execute(Observer $observer): void
+ {
+ $sender = $observer->getSender();
+
+ if (!$sender instanceof OrderSender) {
+ return;
+ }
+
+ /** @var DataObject $transport */
+ $transport = $observer->getData('transportObject');
+
+ /** @var OrderInterface $order */
+ $order = $transport->getOrder();
+
+ if (!$order) {
+ return;
+ }
+
+ $storeId = (int) $order->getStoreId();
+
+ if (!$this->config->isEnabled($storeId) && !$this->config->isOrderEnabled($storeId)) {
+ return;
+ }
+
+ $jsonData = $this->orderProcessor->processOrder($order);
+
+ $transport->setData('gmailMarkup', $jsonData);
+ }
+}
diff --git a/Observer/ShipmentEmailObserver.php b/Observer/ShipmentEmailObserver.php
new file mode 100644
index 0000000..1d333f3
--- /dev/null
+++ b/Observer/ShipmentEmailObserver.php
@@ -0,0 +1,70 @@
+shipmentProcessor = $shipmentProcessor;
+ $this->config = $config;
+ }
+
+ /** {@inheritDoc} */
+ public function execute(Observer $observer): void
+ {
+ $sender = $observer->getSender();
+
+ if (!$sender instanceof EmailSender && !$sender instanceof ShipmentSender) {
+ return;
+ }
+
+ /** @var DataObject $transport */
+ $transport = $observer->getData('transportObject');
+
+ /** @var OrderInterface $order */
+ $order = $transport->getOrder();
+
+ /** @var ShipmentInterface $shipment */
+ $shipment = $transport->getShipment();
+
+ if (!$order || !$shipment) {
+ return;
+ }
+
+ $storeId = (int) $order->getStoreId();
+
+ if (!$this->config->isEnabled($storeId) && !$this->config->isShipmentEnabled($storeId)) {
+ return;
+ }
+
+ $jsonData = $this->shipmentProcessor->processShipment($shipment, $order);
+
+ $transport->setData('gmailMarkup', $jsonData);
+ }
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..549d620
--- /dev/null
+++ b/README.md
@@ -0,0 +1,45 @@
+# Magento 2 Gmail Markup extenstion
+
+## 1. Documentation
+
+- [Contribute on Github](https://github.com/marcinmaterzok/magento2-email-gmail-markup)
+- [Releases](https://github.com/marcinmaterzok/magento2-email-gmail-markup/releases)
+- [Google Registration Guidelines](https://developers.google.com/gmail/markup/registering-with-google)
+- [What is Gmail Markup?](https://developers.google.com/gmail/markup)
+
+
+## 2. How to install
+
+### Install via composer (recommend)
+**1. Run the following command in Magento 2 root folder:**
+```
+composer require mtrzk/magento2-gmail-markup
+php bin/magento setup:upgrade
+php bin/magento setup:static-content:deploy
+```
+**2. Configure module in Magneto 2 Admin panel**
+
+**3. IMPORTANT! You need to manually add variable in you email templates (in header in head tag).**
+```
+{{var gmailMarkup|raw}}
+```
+
+## 3. How to register in Google
+
+1. If you want to use this module you need to check "Email Sender Quality guidelines" section on
+https://developers.google.com/gmail/markup/registering-with-google
+2. Enable module, and check order and shipment email via https://www.mail-tester.com/
+3. Register on this form:
+https://docs.google.com/forms/d/e/1FAIpQLSfT5F1VJXtBjGw2mLxY2aX557ctPTsCrJpURiKJjYeVrugHBQ/viewform?pli=1
+4. If you also want to use ViewOrder and TrackAction action you need to check "Actions / Schema Guidelines" section
+
+
+## 4. CHANGELOG
+Version 1.0.0
+
+```
+- First commit
+- Added support for Order emails
+- Added support for Shipment emails
+- Advanced configuration per store
+```
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..89ab098
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,29 @@
+{
+ "name": "mtrzk/magento2-gmail-markup",
+ "description": "Magento 2 for Email Gmail Markup functionality",
+ "version": "1.0.0",
+ "require": {
+ "php": ">=7.4.0",
+ "magento/module-sales": "*",
+ "magento/module-email": "*",
+ "magento/module-backend": "*"
+ },
+ "license": [
+ "proprietary"
+ ],
+ "authors": [
+ {
+ "name": "Marcin Materzok",
+ "email": "marcin@mtrzk.com"
+ }
+ ],
+ "type": "magento2-module",
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Mtrzk\\GmailMarkup\\": ""
+ }
+ }
+}
diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml
new file mode 100644
index 0000000..1350858
--- /dev/null
+++ b/etc/adminhtml/system.xml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+ mtrzk_modules
+ Mtrzk_GmailMarkup::faqpage
+
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+
+ 1
+
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+ Mtrzk\GmailMarkup\Model\Config\Backend\Serialized
+ Mtrzk\GmailMarkup\Block\Adminhtml\Form\Field\OrderMappingOptions
+ This field is for mapping Magento 2 Order status with JSON Schema status
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+ Mtrzk\GmailMarkup\Model\Config\Source\UrlType
+
+
+
+
+ custom
+
+ This field is for custom order view URL (for example in PWA). Replace variable is: order_id or increment_id
+
+
+ 1
+
+
+
+
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+ Mtrzk\GmailMarkup\Model\Config\Backend\Serialized
+ Mtrzk\GmailMarkup\Block\Adminhtml\Form\Field\DeliveryOptions
+ This field is for mapping Magento 2 Delivery methods with Courier names
+
+
+
+ Mtrzk\GmailMarkup\Model\Config\Backend\Serialized
+ Mtrzk\GmailMarkup\Block\Adminhtml\Form\Field\TrackingOptions
+ This field is for mapping Magento 2 Delivery with tracking URL. Replace variable is: tracking_number or shipment_id
+
+
+ 1
+
+
+
+
+
+
+
+ Mtrzk\GmailMarkup\Model\Config\Source\EmailMx
+
+
+ 1
+
+
+
+
+
diff --git a/etc/config.xml b/etc/config.xml
new file mode 100644
index 0000000..969f066
--- /dev/null
+++ b/etc/config.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ 1
+ 1
+ 1
+ Your Store Name
+
+
+ 0
+ View Order
+ Show your order on store site
+ default
+ http://yourstore.com/order/view/id/{{order_id}}/
+ {"_1662154108769_769":{"order_status":"pending","schema_status":"http:\/\/schema.org\/OrderProcessing"},"_1662154115688_688":{"order_status":"canceled","schema_status":"http:\/\/schema.org\/OrderCancelled"},"_1662154123960_960":{"order_status":"payment_review","schema_status":"http:\/\/schema.org\/OrderPaymentDue"},"_1662154132124_124":{"order_status":"pending_payment","schema_status":"http:\/\/schema.org\/OrderProcessing"},"_1662154142564_564":{"order_status":"complete","schema_status":"http:\/\/schema.org\/OrderDelivered"},"_1662154147793_793":{"order_status":"holded","schema_status":"http:\/\/schema.org\/OrderProblem"},"_1662154161029_29":{"order_status":"paypal_reversed","schema_status":"http:\/\/schema.org\/OrderProblem"},"_1662154179561_561":{"order_status":"fraud","schema_status":"http:\/\/schema.org\/OrderProblem"}}
+
+
+ 0
+
+
+ default
+
+
+
+
diff --git a/etc/di.xml b/etc/di.xml
new file mode 100644
index 0000000..f348c04
--- /dev/null
+++ b/etc/di.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/etc/events.xml b/etc/events.xml
new file mode 100644
index 0000000..f73451e
--- /dev/null
+++ b/etc/events.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/etc/module.xml b/etc/module.xml
new file mode 100644
index 0000000..4676b5f
--- /dev/null
+++ b/etc/module.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/registration.php b/registration.php
new file mode 100644
index 0000000..9ed449b
--- /dev/null
+++ b/registration.php
@@ -0,0 +1,13 @@
+