diff --git a/Api/Config/RepositoryInterface.php b/Api/Config/RepositoryInterface.php
index 1938b9f..44a21b8 100755
--- a/Api/Config/RepositoryInterface.php
+++ b/Api/Config/RepositoryInterface.php
@@ -12,6 +12,12 @@ interface RepositoryInterface extends System\DataInterface
public const XML_PATH_EXTENSION_VERSION = 'faslet_connect/general/version';
public const XML_PATH_EXTENSION_ENABLE = 'faslet_connect/general/enable';
public const XML_PATH_DEBUG = 'faslet_connect/general/debug';
+ public const XML_PATH_RMA_EXPORT_ENABLE = 'faslet_connect/rma/export_enabled';
+ public const XML_PATH_RMA_ENDPOINT_URL = 'faslet_connect/rma/endpoint_url';
+ public const XML_PATH_RMA_AUTH_TOKEN = 'faslet_connect/rma/auth_token';
+ public const XML_PATH_RMA_TIMEOUT = 'faslet_connect/rma/timeout';
+ public const XML_PATH_RMA_LIFECYCLE_EVENTS = 'faslet_connect/rma/lifecycle_events';
+ public const DEFAULT_RMA_TIMEOUT = 5;
public const MODULE_SUPPORT_LINK = 'https://faslet.me/faslet/contact';
/**
@@ -59,4 +65,59 @@ public function getSupportLink(): string;
* @return string
*/
public function getStoreUrl(): string;
+
+ /**
+ * Check if Commerce RMA export is enabled.
+ *
+ * @param int|null $storeId
+ *
+ * @return bool
+ */
+ public function isRmaExportEnabled(?int $storeId = null): bool;
+
+ /**
+ * Get the Faslet RMA ingestion endpoint.
+ *
+ * @param int|null $storeId
+ *
+ * @return string|null
+ */
+ public function getRmaEndpointUrl(?int $storeId = null): ?string;
+
+ /**
+ * Get the Faslet auth token used for RMA exports.
+ *
+ * @param int|null $storeId
+ *
+ * @return string|null
+ */
+ public function getRmaAuthToken(?int $storeId = null): ?string;
+
+ /**
+ * Get the outbound request timeout for RMA exports.
+ *
+ * @param int|null $storeId
+ *
+ * @return int
+ */
+ public function getRmaTimeout(?int $storeId = null): int;
+
+ /**
+ * Get the configured RMA lifecycle events that should be exported.
+ *
+ * @param int|null $storeId
+ *
+ * @return array
+ */
+ public function getRmaLifecycleEvents(?int $storeId = null): array;
+
+ /**
+ * Check if a specific lifecycle event should be exported.
+ *
+ * @param string $eventCode
+ * @param int|null $storeId
+ *
+ * @return bool
+ */
+ public function shouldExportRmaLifecycleEvent(string $eventCode, ?int $storeId = null): bool;
}
diff --git a/Api/Config/System/DataInterface.php b/Api/Config/System/DataInterface.php
index ccceeac..e5f67b1 100644
--- a/Api/Config/System/DataInterface.php
+++ b/Api/Config/System/DataInterface.php
@@ -18,7 +18,7 @@ interface DataInterface
/**
* @return string|null
*/
- public function getShopId(): ?string;
+ public function getShopId(?int $storeId = null): ?string;
/**
* @return array
diff --git a/Model/Config/Repository.php b/Model/Config/Repository.php
index 6e1065d..f28120f 100755
--- a/Model/Config/Repository.php
+++ b/Model/Config/Repository.php
@@ -57,4 +57,59 @@ public function getStoreUrl(): string
{
return $this->getStore()->getBaseUrl();
}
+
+ /**
+ * @inheritDoc
+ */
+ public function isRmaExportEnabled(?int $storeId = null): bool
+ {
+ return $this->isSetFlag(self::XML_PATH_RMA_EXPORT_ENABLE, $storeId);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getRmaEndpointUrl(?int $storeId = null): ?string
+ {
+ return $this->getStoreValue(self::XML_PATH_RMA_ENDPOINT_URL, $storeId);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getRmaAuthToken(?int $storeId = null): ?string
+ {
+ return $this->getDecryptedStoreValue(self::XML_PATH_RMA_AUTH_TOKEN, $storeId);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getRmaTimeout(?int $storeId = null): int
+ {
+ $timeout = (int)$this->getStoreValue(self::XML_PATH_RMA_TIMEOUT, $storeId);
+
+ return $timeout > 0 ? $timeout : self::DEFAULT_RMA_TIMEOUT;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getRmaLifecycleEvents(?int $storeId = null): array
+ {
+ $rawValue = (string)$this->getStoreValue(self::XML_PATH_RMA_LIFECYCLE_EVENTS, $storeId);
+ if ($rawValue === '') {
+ return [];
+ }
+
+ return array_values(array_filter(array_map('trim', explode(',', $rawValue))));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function shouldExportRmaLifecycleEvent(string $eventCode, ?int $storeId = null): bool
+ {
+ return in_array($eventCode, $this->getRmaLifecycleEvents($storeId), true);
+ }
}
diff --git a/Model/Config/System/BaseRepository.php b/Model/Config/System/BaseRepository.php
index 7633043..4ad1a70 100644
--- a/Model/Config/System/BaseRepository.php
+++ b/Model/Config/System/BaseRepository.php
@@ -4,6 +4,7 @@
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ProductMetadata;
+use Magento\Framework\Encryption\EncryptorInterface;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Model\ScopeInterface;
@@ -28,6 +29,10 @@ class BaseRepository
* @var StoreManagerInterface
*/
protected $storeManager;
+ /**
+ * @var EncryptorInterface
+ */
+ protected $encryptor;
/**
* BaseRepository constructor.
@@ -35,17 +40,20 @@ class BaseRepository
* @param Json $json
* @param ProductMetadata $metadata
* @param StoreManagerInterface $storeManager
+ * @param EncryptorInterface $encryptor
*/
public function __construct(
ScopeConfigInterface $scopeConfig,
Json $json,
ProductMetadata $metadata,
- StoreManagerInterface $storeManager
+ StoreManagerInterface $storeManager,
+ EncryptorInterface $encryptor
) {
$this->scopeConfig = $scopeConfig;
$this->json = $json;
$this->metadata = $metadata;
$this->storeManager = $storeManager;
+ $this->encryptor = $encryptor;
}
/**
@@ -82,6 +90,26 @@ protected function isSetFlag(string $path, ?int $storeId = null, ?string $scope
return $this->scopeConfig->isSetFlag($path, $scope, $storeId);
}
+ /**
+ * Retrieve and decrypt a config value by path, storeId and scope.
+ *
+ * @param string $path
+ * @param int|null $storeId
+ * @param string|null $scope
+ *
+ * @return string|null
+ */
+ protected function getDecryptedStoreValue(string $path, ?int $storeId = null, ?string $scope = null): ?string
+ {
+ $value = $this->getStoreValue($path, $storeId, $scope);
+
+ if ($value === null || $value === '') {
+ return $value;
+ }
+
+ return $this->encryptor->decrypt($value);
+ }
+
/**
* @return StoreInterface
*/
diff --git a/Model/Config/System/DataRepository.php b/Model/Config/System/DataRepository.php
index 9495629..face180 100644
--- a/Model/Config/System/DataRepository.php
+++ b/Model/Config/System/DataRepository.php
@@ -10,9 +10,9 @@ class DataRepository extends BaseRepository implements DataInterface
/**
* @inheritDoc
*/
- public function getShopId(): ?string
+ public function getShopId(?int $storeId = null): ?string
{
- return $this->getStoreValue(self::SHOP_ID);
+ return $this->getStoreValue(self::SHOP_ID, $storeId);
}
/**
diff --git a/Model/Rma/ExportService.php b/Model/Rma/ExportService.php
new file mode 100644
index 0000000..cb4e9c5
--- /dev/null
+++ b/Model/Rma/ExportService.php
@@ -0,0 +1,252 @@
+configRepository = $configRepository;
+ $this->logRepository = $logRepository;
+ $this->moduleManager = $moduleManager;
+ $this->orderRepository = $orderRepository;
+ $this->payloadBuilder = $payloadBuilder;
+ $this->webhookPublisher = $webhookPublisher;
+ }
+
+ /**
+ * Export an Adobe Commerce RMA model to the configured Faslet backend.
+ *
+ * @param object $rma
+ *
+ * @return void
+ */
+ public function export(object $rma): void
+ {
+ if (!$this->moduleManager->isEnabled('Magento_Rma') || !$this->isRmaEntity($rma)) {
+ return;
+ }
+
+ $orderId = (int)$this->readValue($rma, ['order_id']);
+ if ($orderId <= 0) {
+ $this->logRepository->addErrorLog('rma_export_missing_order', $this->extractRawData($rma));
+ return;
+ }
+
+ try {
+ $order = $this->orderRepository->get($orderId);
+ } catch (\Exception $exception) {
+ $this->logRepository->addErrorLog('rma_export_order_lookup_failed', [
+ 'order_id' => $orderId,
+ 'message' => $exception->getMessage(),
+ ]);
+ return;
+ }
+
+ $storeId = (int)$order->getStoreId();
+ if (!$this->configRepository->isEnabled($storeId) || !$this->configRepository->isRmaExportEnabled($storeId)) {
+ return;
+ }
+
+ if (!$this->configRepository->getRmaEndpointUrl($storeId)) {
+ return;
+ }
+
+ $eventCode = $this->resolveLifecycleEvent($rma, $storeId);
+ if ($eventCode === null) {
+ return;
+ }
+
+ $payload = $this->payloadBuilder->build($rma, $order, $eventCode);
+ $this->logRepository->addDebugLog('rma_export_payload', $payload);
+ $this->webhookPublisher->publish($payload, $storeId);
+ }
+
+ /**
+ * @param object $rma
+ *
+ * @return bool
+ */
+ private function isRmaEntity(object $rma): bool
+ {
+ return is_a($rma, self::RMA_MODEL_CLASS);
+ }
+
+ /**
+ * @param object $rma
+ * @param int $storeId
+ *
+ * @return string|null
+ */
+ private function resolveLifecycleEvent(object $rma, int $storeId): ?string
+ {
+ $status = $this->normalizeStatus((string)$this->readValue($rma, ['status']));
+ $previousStatus = $this->normalizeStatus((string)$this->readOrigValue($rma, ['status']));
+
+ if ($this->isCreated($rma) && $this->configRepository->shouldExportRmaLifecycleEvent('created', $storeId)) {
+ return 'created';
+ }
+
+ if ($status && $status !== $previousStatus && $this->configRepository->shouldExportRmaLifecycleEvent($status, $storeId)) {
+ return $status;
+ }
+
+ return null;
+ }
+
+ /**
+ * @param object $rma
+ *
+ * @return bool
+ */
+ private function isCreated(object $rma): bool
+ {
+ if (method_exists($rma, 'isObjectNew') && $rma->isObjectNew()) {
+ return true;
+ }
+
+ $originalEntityId = $this->readOrigValue($rma, ['entity_id', 'id']);
+ $originalIncrementId = $this->readOrigValue($rma, ['increment_id']);
+
+ if ($originalEntityId === null && $originalIncrementId === null) {
+ $createdAt = (string)$this->readValue($rma, ['created_at', 'date_requested']);
+ $updatedAt = (string)$this->readValue($rma, ['updated_at']);
+
+ if ($createdAt !== '' && $createdAt === $updatedAt) {
+ return true;
+ }
+ }
+
+ return $originalEntityId === null && $originalIncrementId === null;
+ }
+
+ /**
+ * @param object $subject
+ * @param array $keys
+ *
+ * @return mixed|null
+ */
+ private function readValue(object $subject, array $keys)
+ {
+ foreach ($keys as $key) {
+ $camelizedKey = str_replace(' ', '', ucwords(str_replace('_', ' ', $key)));
+ $getter = 'get' . $camelizedKey;
+
+ if (method_exists($subject, $getter)) {
+ $value = $subject->{$getter}();
+ if ($value !== null && $value !== '') {
+ return $value;
+ }
+ }
+
+ if ($subject instanceof DataObject) {
+ $value = $subject->getData($key);
+ if ($value !== null && $value !== '') {
+ return $value;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param object $subject
+ * @param array $keys
+ *
+ * @return mixed|null
+ */
+ private function readOrigValue(object $subject, array $keys)
+ {
+ if (!method_exists($subject, 'getOrigData')) {
+ return null;
+ }
+
+ foreach ($keys as $key) {
+ $value = $subject->getOrigData($key);
+ if ($value !== null && $value !== '') {
+ return $value;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param object $subject
+ *
+ * @return array
+ */
+ private function extractRawData(object $subject): array
+ {
+ if ($subject instanceof DataObject) {
+ return $subject->getData();
+ }
+
+ if (method_exists($subject, 'getData')) {
+ $data = $subject->getData();
+ if (is_array($data)) {
+ return $data;
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * @param string $status
+ *
+ * @return string|null
+ */
+ private function normalizeStatus(string $status): ?string
+ {
+ $status = trim($status);
+ if ($status === '') {
+ return null;
+ }
+
+ $normalized = strtolower((string)preg_replace('/[^a-z0-9]+/i', '_', $status));
+
+ return trim($normalized, '_');
+ }
+}
\ No newline at end of file
diff --git a/Model/Rma/PayloadBuilder.php b/Model/Rma/PayloadBuilder.php
new file mode 100644
index 0000000..e97dca6
--- /dev/null
+++ b/Model/Rma/PayloadBuilder.php
@@ -0,0 +1,353 @@
+configRepository = $configRepository;
+ $this->productRepository = $productRepository;
+ }
+
+ /**
+ * Build a backend ingestion payload for an Adobe Commerce RMA export.
+ *
+ * @param object $rma
+ * @param OrderInterface $order
+ * @param string $eventCode
+ *
+ * @return array
+ */
+ public function build(object $rma, OrderInterface $order, string $eventCode): array
+ {
+ $storeId = (int)$order->getStoreId();
+ $status = $this->normalizeStatus((string)$this->readValue($rma, ['status']));
+ $previousStatus = $this->normalizeStatus((string)$this->readOrigValue($rma, ['status']));
+
+ return [
+ 'event' => $eventCode,
+ 'event_id' => $this->buildEventId($rma, $eventCode),
+ 'occurred_at' => $this->readValue($rma, ['updated_at', 'date_requested', 'created_at']),
+ 'shop_id' => $this->configRepository->getShopId($storeId),
+ 'store' => [
+ 'id' => $storeId,
+ 'code' => $order->getStoreName(),
+ ],
+ 'order' => [
+ 'entity_id' => (int)$order->getEntityId(),
+ 'increment_id' => $order->getIncrementId(),
+ 'customer_email' => $order->getCustomerEmail(),
+ ],
+ 'rma' => [
+ 'entity_id' => $this->readValue($rma, ['entity_id', 'id']),
+ 'increment_id' => $this->readValue($rma, ['increment_id']),
+ 'status' => $status,
+ 'previous_status' => $previousStatus,
+ 'created_at' => $this->readValue($rma, ['created_at', 'date_requested']),
+ 'updated_at' => $this->readValue($rma, ['updated_at']),
+ 'comments' => $this->extractComments($rma),
+ 'attributes' => $this->sanitizeData($this->extractRawData($rma)),
+ ],
+ 'items' => $this->buildItems($rma, $order),
+ ];
+ }
+
+ /**
+ * @param object $rma
+ * @param OrderInterface $order
+ *
+ * @return array
+ */
+ private function buildItems(object $rma, OrderInterface $order): array
+ {
+ $items = [];
+ $storeId = (int)$order->getStoreId();
+ $orderItems = $this->indexOrderItems($order);
+
+ foreach ($this->extractItems($rma) as $rmaItem) {
+ $orderItemId = (int)$this->readValue($rmaItem, ['order_item_id']);
+ $orderItem = $orderItems[$orderItemId] ?? null;
+ $productData = $this->extractProductData($orderItem, $storeId);
+
+ $items[] = [
+ 'entity_id' => $this->readValue($rmaItem, ['entity_id', 'id']),
+ 'order_item_id' => $orderItemId ?: null,
+ 'product_id' => $this->readValue($rmaItem, ['product_id']) ?: ($orderItem ? (int)$orderItem->getProductId() : null),
+ 'sku' => $this->readValue($rmaItem, ['product_sku', 'sku']) ?: ($orderItem ? $orderItem->getSku() : null),
+ 'title' => $this->readValue($rmaItem, ['product_name', 'name']) ?: ($orderItem ? $orderItem->getName() : null),
+ 'correlation_id' => $productData['correlation_id'],
+ 'variant_id' => $productData['variant_id'],
+ 'requested_qty' => $this->readValue($rmaItem, ['qty_requested', 'qty']),
+ 'authorized_qty' => $this->readValue($rmaItem, ['qty_authorized']),
+ 'returned_qty' => $this->readValue($rmaItem, ['qty_returned']),
+ 'reason' => $this->readValue($rmaItem, ['reason']),
+ 'reason_label' => $this->readValue($rmaItem, ['reason_label', 'reason']),
+ 'condition' => $this->readValue($rmaItem, ['condition']),
+ 'condition_label' => $this->readValue($rmaItem, ['condition_label', 'condition']),
+ 'resolution' => $this->readValue($rmaItem, ['resolution']),
+ 'resolution_label' => $this->readValue($rmaItem, ['resolution_label', 'resolution']),
+ 'attributes' => $this->sanitizeData($this->extractRawData($rmaItem)),
+ ];
+ }
+
+ return $items;
+ }
+
+ /**
+ * @param OrderInterface $order
+ *
+ * @return array
+ */
+ private function indexOrderItems(OrderInterface $order): array
+ {
+ $items = [];
+
+ foreach ($order->getItems() as $orderItem) {
+ $items[(int)$orderItem->getItemId()] = $orderItem;
+ }
+
+ return $items;
+ }
+
+ /**
+ * @param OrderItemInterface|null $orderItem
+ * @param int $storeId
+ *
+ * @return array
+ */
+ private function extractProductData(?OrderItemInterface $orderItem, int $storeId): array
+ {
+ if ($orderItem === null) {
+ return [
+ 'correlation_id' => null,
+ 'variant_id' => null,
+ ];
+ }
+
+ $product = $orderItem->getProduct();
+ if ($product === null && $orderItem->getProductId()) {
+ try {
+ $product = $this->productRepository->getById((int)$orderItem->getProductId(), false, $storeId);
+ } catch (\Exception $exception) {
+ $product = null;
+ }
+ }
+
+ if ($product === null) {
+ return [
+ 'correlation_id' => null,
+ 'variant_id' => null,
+ ];
+ }
+
+ $attributes = $this->configRepository->getAttributes($storeId);
+ $identifierAttribute = $attributes['identifier'] ?? null;
+
+ return [
+ 'correlation_id' => $identifierAttribute ? $product->getData($identifierAttribute) : null,
+ 'variant_id' => $identifierAttribute ? $product->getData($identifierAttribute) : null,
+ ];
+ }
+
+ /**
+ * @param object $rma
+ *
+ * @return iterable
+ */
+ private function extractItems(object $rma): iterable
+ {
+ foreach (['getItems', 'getItemsCollection'] as $method) {
+ if (method_exists($rma, $method)) {
+ $items = $rma->{$method}();
+ if (is_array($items) || $items instanceof \Traversable) {
+ return $items;
+ }
+ }
+ }
+
+ $items = $this->readValue($rma, ['items']);
+ if (is_array($items) || $items instanceof \Traversable) {
+ return $items;
+ }
+
+ return [];
+ }
+
+ /**
+ * @param object $rma
+ *
+ * @return array
+ */
+ private function extractComments(object $rma): array
+ {
+ $result = [];
+
+ foreach (['getComments', 'getCommentsCollection'] as $method) {
+ if (!method_exists($rma, $method)) {
+ continue;
+ }
+
+ $comments = $rma->{$method}();
+ if (!is_array($comments) && !$comments instanceof \Traversable) {
+ continue;
+ }
+
+ foreach ($comments as $comment) {
+ $result[] = [
+ 'entity_id' => $this->readValue($comment, ['entity_id', 'id']),
+ 'comment' => $this->readValue($comment, ['comment', 'comment_text']),
+ 'created_at' => $this->readValue($comment, ['created_at']),
+ 'status' => $this->readValue($comment, ['status']),
+ ];
+ }
+
+ if ($result) {
+ return $result;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param object $subject
+ * @param array $keys
+ *
+ * @return mixed|null
+ */
+ private function readValue(object $subject, array $keys)
+ {
+ foreach ($keys as $key) {
+ $camelizedKey = str_replace(' ', '', ucwords(str_replace('_', ' ', $key)));
+ $getter = 'get' . $camelizedKey;
+
+ if (method_exists($subject, $getter)) {
+ $value = $subject->{$getter}();
+ if ($value !== null && $value !== '') {
+ return $value;
+ }
+ }
+
+ if ($subject instanceof DataObject) {
+ $value = $subject->getData($key);
+ if ($value !== null && $value !== '') {
+ return $value;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param object $subject
+ * @param array $keys
+ *
+ * @return mixed|null
+ */
+ private function readOrigValue(object $subject, array $keys)
+ {
+ if (!method_exists($subject, 'getOrigData')) {
+ return null;
+ }
+
+ foreach ($keys as $key) {
+ $value = $subject->getOrigData($key);
+ if ($value !== null && $value !== '') {
+ return $value;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param object $subject
+ *
+ * @return array
+ */
+ private function extractRawData(object $subject): array
+ {
+ if ($subject instanceof DataObject) {
+ return $subject->getData();
+ }
+
+ if (method_exists($subject, 'getData')) {
+ $data = $subject->getData();
+ if (is_array($data)) {
+ return $data;
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * @param array $data
+ *
+ * @return array
+ */
+ private function sanitizeData(array $data): array
+ {
+ foreach ($data as $key => $value) {
+ if (is_object($value)) {
+ unset($data[$key]);
+ continue;
+ }
+
+ if (is_array($value)) {
+ $data[$key] = $this->sanitizeData($value);
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * @param object $rma
+ * @param string $eventCode
+ *
+ * @return string
+ */
+ private function buildEventId(object $rma, string $eventCode): string
+ {
+ $rmaId = (string)$this->readValue($rma, ['increment_id', 'entity_id', 'id']);
+
+ return sprintf('faslet_rma:%s:%s', $rmaId, $eventCode);
+ }
+
+ /**
+ * @param string $status
+ *
+ * @return string|null
+ */
+ private function normalizeStatus(string $status): ?string
+ {
+ $status = trim($status);
+ if ($status === '') {
+ return null;
+ }
+
+ $normalized = strtolower((string)preg_replace('/[^a-z0-9]+/i', '_', $status));
+
+ return trim($normalized, '_');
+ }
+}
\ No newline at end of file
diff --git a/Model/Rma/WebhookPublisher.php b/Model/Rma/WebhookPublisher.php
new file mode 100644
index 0000000..4c2d775
--- /dev/null
+++ b/Model/Rma/WebhookPublisher.php
@@ -0,0 +1,97 @@
+configRepository = $configRepository;
+ $this->curlFactory = $curlFactory;
+ $this->json = $json;
+ $this->logRepository = $logRepository;
+ }
+
+ /**
+ * Publish the given payload to the configured Faslet endpoint.
+ *
+ * @param array $payload
+ * @param int $storeId
+ *
+ * @return void
+ */
+ public function publish(array $payload, int $storeId): void
+ {
+ $url = $this->configRepository->getRmaEndpointUrl($storeId);
+ if (!$url) {
+ return;
+ }
+
+ $requestBody = $this->json->serialize($payload);
+ $eventId = (string)($payload['event_id'] ?? 'unknown');
+
+ try {
+ $curl = $this->curlFactory->create();
+ $timeout = $this->configRepository->getRmaTimeout($storeId);
+ $curl->setOption(CURLOPT_CONNECTTIMEOUT, $timeout);
+ $curl->setOption(CURLOPT_TIMEOUT, $timeout);
+ $curl->addHeader('Accept', 'application/json');
+ $curl->addHeader('Content-Type', 'application/json');
+ $curl->addHeader('X-Faslet-Event-Id', $eventId);
+
+ if ($token = $this->configRepository->getRmaAuthToken($storeId)) {
+ $curl->addHeader('Authorization', 'Bearer ' . $token);
+ }
+
+ $curl->post($url, $requestBody);
+
+ $logPayload = [
+ 'url' => $url,
+ 'store_id' => $storeId,
+ 'status' => $curl->getStatus(),
+ 'body' => $curl->getBody(),
+ 'event_id' => $eventId,
+ ];
+
+ if ($curl->getStatus() >= 400) {
+ $this->logRepository->addErrorLog('rma_export_response', $logPayload);
+ return;
+ }
+
+ $this->logRepository->addDebugLog('rma_export_response', $logPayload);
+ } catch (\Exception $exception) {
+ $this->logRepository->addErrorLog('rma_export_exception', [
+ 'store_id' => $storeId,
+ 'event_id' => $eventId,
+ 'message' => $exception->getMessage(),
+ ]);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Model/System/Config/Source/RmaLifecycleEvent.php b/Model/System/Config/Source/RmaLifecycleEvent.php
new file mode 100644
index 0000000..90a696c
--- /dev/null
+++ b/Model/System/Config/Source/RmaLifecycleEvent.php
@@ -0,0 +1,27 @@
+ 'created', 'label' => __('RMA Created / Requested')],
+ ['value' => 'authorized', 'label' => __('Authorized')],
+ ['value' => 'partially_authorized', 'label' => __('Partially Authorized')],
+ ['value' => 'return_received', 'label' => __('Return Received')],
+ ['value' => 'return_partially_received', 'label' => __('Return Partially Received')],
+ ['value' => 'approved', 'label' => __('Approved')],
+ ['value' => 'rejected', 'label' => __('Rejected')],
+ ['value' => 'processed_and_closed', 'label' => __('Processed and Closed')],
+ ['value' => 'closed', 'label' => __('Closed')],
+ ];
+ }
+}
\ No newline at end of file
diff --git a/Observer/ExportRmaAfterSave.php b/Observer/ExportRmaAfterSave.php
new file mode 100644
index 0000000..d154051
--- /dev/null
+++ b/Observer/ExportRmaAfterSave.php
@@ -0,0 +1,46 @@
+exportService = $exportService;
+ $this->moduleManager = $moduleManager;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(Observer $observer): void
+ {
+ if (!$this->moduleManager->isEnabled('Magento_Rma')) {
+ return;
+ }
+
+ $object = $observer->getEvent()->getObject();
+ if (!is_object($object)) {
+ return;
+ }
+
+ $this->exportService->export($object);
+ }
+}
\ No newline at end of file
diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml
index 713439e..c03191f 100755
--- a/etc/adminhtml/system.xml
+++ b/etc/adminhtml/system.xml
@@ -11,6 +11,7 @@
+
diff --git a/etc/adminhtml/system/rma.xml b/etc/adminhtml/system/rma.xml
new file mode 100644
index 0000000..789e40a
--- /dev/null
+++ b/etc/adminhtml/system/rma.xml
@@ -0,0 +1,48 @@
+
+
+
+
+ This feature exports Adobe Commerce RMA events to the Faslet backend. It remains inactive on Magento Open Source because the native RMA module is not available there.
+
+
+ Magento\Config\Model\Config\Source\Yesno
+ faslet_connect/rma/export_enabled
+
+
+
+ Internal Faslet ingestion endpoint that receives raw Adobe Commerce RMA events.
+ faslet_connect/rma/endpoint_url
+ validate-url
+
+ 1
+
+
+
+
+ Optional bearer token used for outbound authentication.
+ faslet_connect/rma/auth_token
+ Magento\Config\Model\Config\Backend\Encrypted
+
+ 1
+
+
+
+
+ Timeout in seconds for outbound RMA export calls.
+ faslet_connect/rma/timeout
+ validate-digits validate-greater-than-zero
+
+ 1
+
+
+
+
+ Select which Adobe Commerce RMA lifecycle events should be exported to Faslet.
+ faslet_connect/rma/lifecycle_events
+ Faslet\Connect\Model\System\Config\Source\RmaLifecycleEvent
+
+ 1
+
+
+
+
\ No newline at end of file
diff --git a/etc/config.xml b/etc/config.xml
index 9485701..cfa5551 100755
--- a/etc/config.xml
+++ b/etc/config.xml
@@ -6,6 +6,11 @@
v1.1.0
0
+
+ 0
+ 5
+ created,return_received,approved,rejected,closed
+
entity_id
sku
diff --git a/etc/di.xml b/etc/di.xml
index b7c7070..19499a6 100755
--- a/etc/di.xml
+++ b/etc/di.xml
@@ -29,5 +29,5 @@
FasletErrorMonolog
-
+
diff --git a/etc/events.xml b/etc/events.xml
new file mode 100644
index 0000000..e97a621
--- /dev/null
+++ b/etc/events.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file