diff --git a/Model/Logger.php b/Model/Logger.php index b0d7a936..e02281a6 100644 --- a/Model/Logger.php +++ b/Model/Logger.php @@ -15,79 +15,80 @@ * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ -namespace Taxjar\SalesTax\Model; +declare(strict_types=1); -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Exception\LocalizedException; -use Taxjar\SalesTax\Model\Configuration as TaxjarConfig; +namespace Taxjar\SalesTax\Model; class Logger { /** - * @var \Magento\Framework\App\Filesystem\DirectoryList + * @var \Magento\Framework\App\Config\ScopeConfigInterface */ - protected $directoryList; + protected $scopeConfig; /** - * @var \Magento\Framework\Filesystem\Driver\File + * @var \Magento\Framework\App\Filesystem\DirectoryList */ - protected $driverFile; + protected $directoryList; /** - * @var array + * @var \Magento\Framework\Filesystem\DriverInterface */ - protected $playback = []; + protected $fileDriver; /** - * @var bool + * @var \Magento\Store\Model\StoreManagerInterface */ - protected $isRecording; + protected $storeManager; /** - * @var \Symfony\Component\Console\Output\OutputInterface + * @var \Taxjar\SalesTax\Model\Configuration */ - protected $console; + protected $taxjarConfig; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var \Symfony\Component\Console\Output\ConsoleOutput */ - protected $scopeConfig; + protected $console; /** * @var string */ - protected $filename = TaxjarConfig::TAXJAR_DEFAULT_LOG; + protected $filename = \Taxjar\SalesTax\Model\Configuration::TAXJAR_DEFAULT_LOG; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var bool */ - protected $storeManager; + protected $isForced = false; /** - * @var TaxjarConfig + * @var bool */ - protected $taxjarConfig; + protected $isRecording; /** - * @var boolean + * @var array */ - protected $isForced = false; + protected $playback = []; /** + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Framework\App\Filesystem\DirectoryList $directoryList - * @param \Magento\Framework\Filesystem\Driver\File $driverFile + * @param \Magento\Framework\Filesystem\DriverInterface $fileDriver + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Taxjar\SalesTax\Model\Configuration $taxjarConfig */ public function __construct( - \Magento\Framework\App\Filesystem\DirectoryList $directoryList, - \Magento\Framework\Filesystem\Driver\File $driverFile, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + \Magento\Framework\App\Filesystem\DirectoryList $directoryList, + \Magento\Framework\Filesystem\DriverInterface $fileDriver, \Magento\Store\Model\StoreManagerInterface $storeManager, - TaxjarConfig $taxjarConfig + \Taxjar\SalesTax\Model\Configuration $taxjarConfig ) { $this->directoryList = $directoryList; - $this->driverFile = $driverFile; $this->scopeConfig = $scopeConfig; $this->storeManager = $storeManager; + $this->fileDriver = $fileDriver; $this->taxjarConfig = $taxjarConfig; } @@ -97,19 +98,31 @@ public function __construct( * @param string $filename * @return Logger */ - public function setFilename($filename) + public function setFilename(string $filename): Logger { $this->filename = $filename; return $this; } + /** + * Manually set the playback value + * + * @param string[] $playback + * @return Logger + */ + public function setPlayback(array $playback): Logger + { + $this->playback = $playback; + return $this; + } + /** * Enables or disables the logger * * @param boolean $isForced * @return Logger */ - public function force($isForced = true) + public function force(bool $isForced = true): Logger { $this->isForced = $isForced; return $this; @@ -119,29 +132,32 @@ public function force($isForced = true) * Get the temp log filename * * @return string + * @throws \Magento\Framework\Exception\FileSystemException */ - public function getPath() + public function getPath(): string { - return $this->directoryList->getPath(DirectoryList::LOG) . DIRECTORY_SEPARATOR . 'taxjar' . DIRECTORY_SEPARATOR . $this->filename; + return sprintf( + '%s/taxjar/%s', + $this->directoryList->getPath(\Magento\Framework\App\Filesystem\DirectoryList::LOG), + $this->filename + ); } /** * Save a message to taxjar.log * - * @param string $message - * @param string $label - * @throws LocalizedException + * @param string|mixed $message + * @param mixed|null $label + * @throws \Magento\Framework\Exception\LocalizedException * @return void */ - public function log($message, $label = '') + public function log($message, $label = ''): void { if ($this->scopeConfig->getValue( - TaxjarConfig::TAXJAR_DEBUG, + \Taxjar\SalesTax\Model\Configuration::TAXJAR_DEBUG, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $this->storeManager->getStore()->getId()) - || - $this->isForced - ) { + $this->storeManager->getStore()->getId() + ) || $this->isForced) { try { if (!empty($label)) { $label = '[' . strtoupper($label) . '] '; @@ -154,12 +170,15 @@ public function log($message, $label = '') $timestamp = date('d M Y H:i:s', time()); $message = sprintf('%s%s - %s%s', PHP_EOL, $timestamp, $label, $message); - if (!is_dir(dirname($this->getPath()))) { + $path = $this->getPath(); + $dirname = $this->fileDriver->getParentDirectory($path); + + if (!$this->fileDriver->isDirectory($dirname)) { // dir doesn't exist, make it - mkdir(dirname($this->getPath())); + $this->fileDriver->createDirectory($dirname); } - $this->driverFile->filePutContents($this->getPath(), $message, FILE_APPEND); + $this->fileDriver->filePutContents($this->getPath(), $message, FILE_APPEND); if ($this->isRecording) { $this->playback[] = $message; @@ -168,9 +187,13 @@ public function log($message, $label = '') $this->console->write($message); } } catch (\Exception $e) { - // @codingStandardsIgnoreStart - throw new LocalizedException(__('Could not write to your Magento log directory under /var/log. Please make sure the directory is created and check permissions for %1.', $this->directoryList->getPath('log'))); - // @codingStandardsIgnoreEnd + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'Could not write to your Magento log directory under /var/log. ' . + 'Please make sure the directory is created and check permissions for %1.', + $this->directoryList->getPath(\Magento\Framework\App\Filesystem\DirectoryList::LOG) + ) + ); } } } @@ -180,7 +203,7 @@ public function log($message, $label = '') * * @return void */ - public function record() + public function record(): void { $this->isRecording = true; } @@ -190,7 +213,7 @@ public function record() * * @return array */ - public function playback() + public function playback(): array { return $this->playback; } @@ -198,9 +221,10 @@ public function playback() /** * Set console output interface * + * @param $output * @return void */ - public function console($output) + public function console($output): void { $this->console = $output; } diff --git a/Observer/AsynchronousObserver.php b/Observer/AsynchronousObserver.php deleted file mode 100644 index 2482f69f..00000000 --- a/Observer/AsynchronousObserver.php +++ /dev/null @@ -1,45 +0,0 @@ -massSchedule = $massSchedule; - } - - public function execute(Observer $observer): void - { - $data = $observer->getData(); - $this->schedule(...$data); - } - - protected function getMassSchedule(): MassSchedule - { - return $this->massSchedule; - } -} diff --git a/Observer/BackfillTransactions.php b/Observer/BackfillTransactions.php index 46a63a9f..d5d21a91 100644 --- a/Observer/BackfillTransactions.php +++ b/Observer/BackfillTransactions.php @@ -15,309 +15,371 @@ * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ +declare(strict_types=1); + namespace Taxjar\SalesTax\Observer; -use Magento\AsynchronousOperations\Api\Data\OperationInterface; -use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; -use Magento\Framework\Api\Filter; -use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\Api\SearchCriteria; -use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Framework\App\RequestInterface; -use Magento\Framework\Bulk\BulkManagementInterface; -use Magento\Framework\DataObject\IdentityGeneratorInterface; -use Magento\Framework\Event\Observer; -use Magento\Framework\Event\ObserverInterface; -use Magento\Framework\Exception\LocalizedException; -use Magento\Sales\Api\Data\OrderInterface; -use Magento\Sales\Api\OrderRepositoryInterface; -use Magento\Sales\Model\AbstractModel; -use Magento\Sales\Model\Order; -use Magento\Store\Model\StoreManager; -use Taxjar\SalesTax\Model\Configuration as TaxjarConfig; -use Taxjar\SalesTax\Model\Logger; - -class BackfillTransactions implements ObserverInterface +class BackfillTransactions implements \Magento\Framework\Event\ObserverInterface { - /** - * The default batch size used for bulk operations - */ - protected const BATCH_SIZE = 100; + public const BATCH_SIZE = 100; - protected const SYNCABLE_STATUSES = ['complete', 'closed']; + public const SYNCABLE_STATUSES = ['complete', 'closed']; /** - * @var RequestInterface + * @var \Magento\Framework\App\RequestInterface */ protected $request; - /** - * @var StoreManager - */ - protected $storeManager; - - /** - * @var Logger + * @var \Taxjar\SalesTax\Model\Logger */ protected $logger; - /** - * @var OrderRepositoryInterface + * @var \Magento\Sales\Api\OrderRepositoryInterface */ protected $orderRepository; - /** - * @var FilterBuilder + * @var \Magento\Store\Model\StoreManagerInterface */ - protected $filterBuilder; - + protected $storeManager; /** - * @var SearchCriteriaBuilder + * @var \Magento\Framework\Api\FilterBuilder */ - protected $searchCriteriaBuilder; - + protected $filterBuilder; /** - * @var TaxjarConfig + * @var \Magento\Framework\Api\SearchCriteriaBuilder */ - protected $taxjarConfig; - + protected $searchCriteriaBuilder; /** - * @var BulkManagementInterface + * @var \Magento\Framework\Bulk\BulkManagementInterface */ protected $bulkManagement; - /** - * @var OperationInterfaceFactory + * @var \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory */ protected $operationFactory; - - /** - * @var IdentityGeneratorInterface - */ - protected $identityService; - /** * @var \Magento\Framework\Serialize\SerializerInterface */ protected $serializer; - /** * @var \Magento\Authorization\Model\UserContextInterface */ protected $userContext; + /** + * @var \Taxjar\SalesTax\Model\Configuration + */ + protected $taxjarConfig; + /** + * @var \Magento\Framework\Event\Observer|null + */ + public $observer; + /** + * @var string|null + */ + public $uuid; + /** + * @var string[]|array|null + */ + protected $dateRange; /** - * @param RequestInterface $request - * @param StoreManager $storeManager - * @param Logger $logger - * @param OrderRepositoryInterface $orderRepository - * @param FilterBuilder $filterBuilder - * @param SearchCriteriaBuilder $searchCriteriaBuilder - * @param TaxjarConfig $taxjarConfig - * @param BulkManagementInterface $bulkManagement - * @param OperationInterfaceFactory $operationFactory - * @param IdentityGeneratorInterface $identityService + * @param \Magento\Framework\App\RequestInterface $request + * @param \Taxjar\SalesTax\Model\Logger $logger + * @param \Magento\Sales\Api\OrderRepositoryInterface $orderRepository + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder + * @param \Magento\Framework\Bulk\BulkManagementInterface $bulkManagement + * @param \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory $operationFactory + * @param \Magento\Framework\DataObject\IdentityGeneratorInterface $identityService * @param \Magento\Framework\Serialize\SerializerInterface $serializer * @param \Magento\Authorization\Model\UserContextInterface $userContext + * @param \Taxjar\SalesTax\Model\Configuration $taxjarConfig */ public function __construct( - RequestInterface $request, - StoreManager $storeManager, - Logger $logger, - OrderRepositoryInterface $orderRepository, - FilterBuilder $filterBuilder, - SearchCriteriaBuilder $searchCriteriaBuilder, - TaxjarConfig $taxjarConfig, + \Magento\Framework\App\RequestInterface $request, + \Taxjar\SalesTax\Model\Logger $logger, + \Magento\Sales\Api\OrderRepositoryInterface $orderRepository, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, \Magento\Framework\Bulk\BulkManagementInterface $bulkManagement, \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory $operationFactory, \Magento\Framework\DataObject\IdentityGeneratorInterface $identityService, \Magento\Framework\Serialize\SerializerInterface $serializer, - \Magento\Authorization\Model\UserContextInterface $userContext + \Magento\Authorization\Model\UserContextInterface $userContext, + \Taxjar\SalesTax\Model\Configuration $taxjarConfig ) { $this->request = $request; - $this->storeManager = $storeManager; - $this->logger = $logger->setFilename(TaxjarConfig::TAXJAR_TRANSACTIONS_LOG)->force(); + $this->logger = $logger; $this->orderRepository = $orderRepository; - $this->filterBuilder = $filterBuilder; + $this->storeManager = $storeManager; $this->searchCriteriaBuilder = $searchCriteriaBuilder; - $this->taxjarConfig = $taxjarConfig; $this->bulkManagement = $bulkManagement; $this->operationFactory = $operationFactory; - $this->identityService = $identityService; $this->serializer = $serializer; $this->userContext = $userContext; + $this->taxjarConfig = $taxjarConfig; + $this->uuid = $identityService->generateId(); + + $this->logger->setFilename(\Taxjar\SalesTax\Model\Configuration::TAXJAR_TRANSACTIONS_LOG)->force(); } /** - * @param Observer $observer - * @throws LocalizedException + * @throws \Magento\Framework\Exception\LocalizedException|\Exception|\Throwable */ - public function execute(Observer $observer): void + public function execute(\Magento\Framework\Event\Observer $observer) { - $this->apiKey = $this->taxjarConfig->getApiKey(); + $this->observer = $observer; - if (!$this->apiKey) { - throw new LocalizedException( - __('Could not sync transactions with TaxJar. Please make sure you have an API key.') - ); + try { + if (!$this->taxjarConfig->getApiKey()) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Could not sync transactions with TaxJar. Please make sure you have an API key.') + ); + } + + $dateRange = $this->getDateRange(); + $criteria = $this->getSearchCriteria(...$dateRange); + $orders = $this->getOrders($criteria); + + if (!empty($orders)) { + $this->syncTransactions($orders); + } + + $this->success(count($orders)); + } catch (\Exception $e) { + $this->fail($e); } + } - $this->logger->log(__('Initializing TaxJar transaction sync')); + public function getDateRange(): array + { + if (empty($this->dateRange)) { + $this->setDateRange(); + } - $criteria = $this->getSearchCriteria($observer->getData()); - $orderResult = $this->orderRepository->getList($criteria); + return $this->dateRange; + } - $this->logger->log(sprintf('%s transaction(s) found', $orderResult->getTotalCount())); + public function setDateRange(): BackfillTransactions + { + $from = $this->getInput('from') ?? 'now'; + $to = $this->getInput('to') ?? 'now'; + $fromDt = new \DateTime($from); + $toDt = new \DateTime($to); - $orderIds = array_map([$this, 'getEntityId'], $orderResult->getItems()); - $orderIdsChunks = array_chunk($orderIds, self::BATCH_SIZE); - $bulkUuid = $this->identityService->generateId(); - $bulkDescription = __('TaxJar Transaction Sync Backfill'); - $operations = []; + if ($from == 'now') { + $fromDt->sub(new \DateInterval('P1D')); + } + + $this->dateRange = [ + $fromDt->setTime(0, 0)->format('Y-m-d H:i:s'), + $toDt->setTime(23, 59, 59)->format('Y-m-d H:i:s') + ]; + + return $this; + } + + /** + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function success(int $count = 0): void + { + $message = 'No un-synced orders were found!'; - foreach ($orderIdsChunks as $orderIdsChunk) { - $operations[] = $this->makeOperation($bulkUuid, [ - 'orderIds' => $orderIdsChunk, - 'force' => (bool) ($observer->getData('force') ?? $this->request->getParam('force')), - ]); + if ($count > 0) { + $message = "Successfully scheduled $count order(s) and any related credit memos for sync with TaxJar!"; } + $this->log($message); + } + + /** + * @param \Magento\Framework\Exception\LocalizedException|\Exception|\Throwable $e + * @throws \Magento\Framework\Exception\LocalizedException|\Exception|\Throwable + */ + public function fail($e): void + { + $exceptionMessage = $e->getMessage(); + + $this->log("Failed to schedule transaction sync! Message: \"$exceptionMessage\""); + + throw $e; + } + + /** + * Because the transaction backfill process can be triggered via UI or CLI, and the process that handles the + * HTTP backfill request relies on the `Logger::class`'s `playback()` value to create the UI message, it is + * necessary to manually override the `playback` value after writing to the log file in order to prevent + * displaying unnecessary JSON configuration details in the web UI. + * + * @param $message + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function log($message): void + { + $configuration = $this->getConfiguration(); + $encodedConfig = json_encode($configuration); + $detail = "Detail: \"$encodedConfig\""; + + $playback = $this->logger->playback(); + $playback[] = $message; + + $this->logger->log("$message $detail"); + $this->logger->setPlayback($playback); + } + + /** + * @param array|\Magento\Sales\Api\Data\OrderInterface[] $orders + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function syncTransactions(array $orders): void + { + $orderIds = array_map([$this, 'getEntityId'], $orders); + $chunkedOrderIds = array_chunk($orderIds, self::BATCH_SIZE); + $operations = array_map([$this, 'makeOperation'], $chunkedOrderIds); + if (!empty($operations)) { $result = $this->bulkManagement->scheduleBulk( - $bulkUuid, + $this->uuid, $operations, - $bulkDescription, + __('TaxJar Transaction Sync Backfill'), $this->userContext->getUserId() ); + if (!$result) { - throw new LocalizedException( - __('Something went wrong while processing the request.') + throw new \Magento\Framework\Exception\LocalizedException( + __('Bulk management encountered an unknown error.') ); } - $this->logger->log(sprintf('Action scheduled. Bulk UUID: %s %s', $bulkUuid, PHP_EOL)); } } /** - * @param array $data - * @return SearchCriteria - * @throws LocalizedException + * @param \Magento\Framework\Api\SearchCriteriaInterface $criteria + * @return array + */ + public function getOrders(\Magento\Framework\Api\SearchCriteriaInterface $criteria): array + { + $orders = $this->orderRepository->getList($criteria)->getItems(); + + if (!$this->getInput('force')) { + $orders = array_filter($orders, [$this, 'isOrderSyncable']); + } + + return $orders; + } + + /** + * @param string $from + * @param string $to + * @return \Magento\Framework\Api\SearchCriteriaInterface|\Magento\Framework\Api\SearchCriteria + * @throws \Magento\Framework\Exception\LocalizedException */ - public function getSearchCriteria(array $data = []): SearchCriteria + public function getSearchCriteria(string $from, string $to): \Magento\Framework\Api\SearchCriteriaInterface { - $fromDate = $data['from_date'] ?? $this->request->getParam('from_date'); - $toDate = $data['to_date'] ?? $this->request->getParam('to_date'); + // Limit orders to specific store $storeId = $this->request->getParam('store'); $websiteId = $this->request->getParam('website'); // If the store id is empty but the website id is defined, load stores that match the website id - if (is_null($storeId) && !is_null($websiteId)) { - $storeId = $this->getWebsiteStoreIds($websiteId); + if ($websiteId && $storeId === null) { + $stores = $this->storeManager->getStores(); + $storeId = array_filter($stores, [$this, 'compareStoreWebsiteId']); } // If the store id is defined, build a filter based on it if (!empty($storeId)) { - $conditionType = is_array($storeId) ? 'in' : 'eq'; - $this->searchCriteriaBuilder->addFilter('store_id', $storeId, $conditionType); - $idString = (is_array($storeId) ? implode(',', $storeId) : $storeId); - $this->logger->log( - sprintf('Limiting transaction sync to store id(s): %s', $idString) - ); + $storeId = is_array($storeId) ? $storeId : [$storeId]; + $this->searchCriteriaBuilder->addFilter('store_id', $storeId, 'in'); + $this->logger->log(sprintf('Limiting transaction sync to store id(s): %s', implode(',', $storeId))); } - if (!empty($fromDate)) { - $fromDate = (new \DateTime($fromDate)); - } else { - $fromDate = (new \DateTime())->sub(new \DateInterval('P1D')); - } + return $this->searchCriteriaBuilder + ->addFilter('created_at', $from, 'gteq') + ->addFilter('created_at', $to, 'lteq') + ->addFilter('state', self::SYNCABLE_STATUSES, 'in') + ->create(); + } - if (!empty($toDate)) { - $toDate = (new \DateTime($toDate)); - } else { - $toDate = (new \DateTime()); - } + /** + * @param $body + * @return \Magento\AsynchronousOperations\Api\Data\OperationInterface + */ + protected function makeOperation($body): \Magento\AsynchronousOperations\Api\Data\OperationInterface + { + $dataToEncode = [ + 'meta_information' => [ + 'orderIds' => $body, + 'force' => (bool)$this->getInput('force'), + ], + ]; - if ($fromDate > $toDate) { - throw new LocalizedException(__("To date can't be earlier than from date.")); - } + $data = [ + 'data' => [ + 'bulk_uuid' => $this->uuid, + 'topic_name' => \Taxjar\SalesTax\Model\Configuration::TAXJAR_TOPIC_SYNC_TRANSACTIONS, + 'serialized_data' => $this->serializer->serialize($dataToEncode), + 'status' => \Magento\Framework\Bulk\OperationInterface::STATUS_TYPE_OPEN, + ] + ]; - $this->logger->log( - sprintf( - 'Finding transactions with statuses of %s from %s - %s', - implode(', ', self::SYNCABLE_STATUSES), - $fromDate->format('m/d/Y'), - $toDate->format('m/d/Y') - ) - ); - - $fromDate->setTime(0, 0, 0)->format('Y-m-d H:i:s'); - $toDate->setTime(23, 59, 59)->format('Y-m-d H:i:s'); - - return $this->searchCriteriaBuilder->addFilter('created_at', $fromDate, 'gteq') - ->addFilter('created_at', $toDate, 'lteq') - ->addFilters(array_map([$this, 'getOrderStateFilter'], self::SYNCABLE_STATUSES)) - ->create(); + return $this->operationFactory->create($data); } /** - * @param string $state - * @return Filter + * @param \Magento\Store\Api\Data\StoreInterface $store + * @return bool */ - protected function getOrderStateFilter(string $state): Filter + private function compareStoreWebsiteId(\Magento\Store\Api\Data\StoreInterface $store): bool { - return $this->filterBuilder->setField('state') - ->setValue($state) - ->setConditionType('eq') - ->create(); + $storeWebsiteId = $store->getWebsiteId(); + return $storeWebsiteId !== null && $storeWebsiteId == $this->getInput('websiteId'); } /** - * @param $websiteId - * @return array + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @return bool */ - protected function getWebsiteStoreIds($websiteId): array + private function isOrderSyncable(\Magento\Sales\Api\Data\OrderInterface $order): bool { - $storeIds = []; - - foreach ($this->storeManager->getStores() as $store) { - if ($store->getWebsiteId() == $websiteId) { - $storeIds[] = $store->getId(); - } - } - - return $storeIds; + return $order->getUpdatedAt() > $order->getTjSalestaxSyncDate(); } /** - * @param AbstractModel $object + * @param \Magento\Sales\Model\AbstractModel $object * @return int|null */ - protected function getEntityId($object): ?int + private function getEntityId(\Magento\Sales\Model\AbstractModel $object): ?int { - return $object->getEntityId(); + return (int)$object->getEntityId(); } /** - * @param string $bulkUuid - * @param $body - * @return OperationInterface + * @return array */ - protected function makeOperation( - string $bulkUuid, - $body - ): OperationInterface { - $dataToEncode = [ - 'meta_information' => $body, - ]; - $data = [ - 'data' => [ - 'bulk_uuid' => $bulkUuid, - 'topic_name' => TaxjarConfig::TAXJAR_TOPIC_SYNC_TRANSACTIONS, - 'serialized_data' => $this->serializer->serialize($dataToEncode), - 'status' => \Magento\Framework\Bulk\OperationInterface::STATUS_TYPE_OPEN, - ] + private function getConfiguration(): array + { + [$startDate, $endDate] = $this->getDateRange(); + + return [ + 'date_start' => $startDate, + 'date_end' => $endDate, + 'force_sync' => (bool)$this->getInput('force'), ]; + } - return $this->operationFactory->create($data); + /** + * @param string $key + * @return array|mixed + */ + private function getInput(string $key) + { + if ($this->observer) { + return $this->observer->getData($key) ?? $this->request->getParam($key); + } + + return $this->request->getParam($key); } } diff --git a/Observer/Concerns/SchedulesOperations.php b/Observer/Concerns/SchedulesOperations.php deleted file mode 100644 index fbfdb6ae..00000000 --- a/Observer/Concerns/SchedulesOperations.php +++ /dev/null @@ -1,30 +0,0 @@ -getMassSchedule()->publishMass($topic, $data); - - if (! $result) { - throw new LocalizedException( - __('Something went wrong while processing the request.') - ); - } - - return $this; - } - - abstract protected function getMassSchedule(): MassSchedule; -} diff --git a/Test/Integration/Model/Transaction/OrderTest.php b/Test/Integration/Model/Transaction/OrderTest.php index b517f4b2..593c5003 100644 --- a/Test/Integration/Model/Transaction/OrderTest.php +++ b/Test/Integration/Model/Transaction/OrderTest.php @@ -18,6 +18,7 @@ namespace Taxjar\SalesTax\Test\Integration\Model\Transaction; use Taxjar\SalesTax\Model\Transaction\Order as TaxjarOrder; +use Taxjar\SalesTax\Test\Fixture\Sales\InvoiceBuilder; use Taxjar\SalesTax\Test\Integration\IntegrationTestCase; use Taxjar\SalesTax\Test\Fixture\Catalog\ProductBuilder; use Taxjar\SalesTax\Test\Fixture\Customer\AddressBuilder; @@ -45,6 +46,8 @@ protected function setUp(): void public function testBuildTaxjarOrderTransaction() { $order = OrderBuilder::anOrder()->build(); + InvoiceBuilder::forOrder($order)->build(); + $result = $this->taxjarOrder->build($order); $this->assertEquals('api', $result['provider'], 'Invalid provider'); @@ -76,6 +79,7 @@ public function testBuildTaxjarOrderTransactionWithShipping() { $order = OrderBuilder::anOrder()->build(); $order->setShippingAmount(20.0); + InvoiceBuilder::forOrder($order)->build(); $result = $this->taxjarOrder->build($order); diff --git a/Test/Integration/Observer/BackfillTransactionsTest.php b/Test/Integration/Observer/BackfillTransactionsTest.php new file mode 100644 index 00000000..cb9b9929 --- /dev/null +++ b/Test/Integration/Observer/BackfillTransactionsTest.php @@ -0,0 +1,83 @@ +objectManager->configure([ + 'preferences' => [ + 'Taxjar\SalesTax\Model\Logger' => + 'Taxjar\SalesTax\Test\Integration\Test\Stubs\LoggerStub' + ] + ]); + + $this->bulkCollection = $this->objectManager->get(BulkCollection::class); + $this->sut = $this->objectManager->get(BackfillTransactions::class); + } + + /** + * @magentoDataFixture Magento/Sales/_files/customer_creditmemo_with_two_items.php + * @magentoConfigFixture default/tax/taxjar/sandbox 0 + * @magentoConfigFixture default/tax/taxjar/apikey valid-api-key + */ + public function testExecute(): void + { + $this->sut->uuid = 'test-uuid'; + $this->sut->execute(new Observer()); + + $bulkItems = $this->bulkCollection->addFilter('uuid', 'test-uuid')->getItems(); + $bulkId = array_keys($bulkItems)[0]; + + $expectedData = [ + 'id' => (string)$bulkId, + 'uuid' => 'test-uuid', + 'user_id' => null, + 'user_type' => '2', + 'description' => 'TaxJar Transaction Sync Backfill', + 'operation_count' => '1', + 'orig_data' => null + ]; + + // only compare array keys defined in expected data + $actualData = array_intersect_key( + $bulkItems[$bulkId]->getData(), + array_flip(array_keys($expectedData)) + ); + + $this->assertCount(1, $bulkItems); + $this->assertEquals($expectedData, $actualData); + } +} diff --git a/Test/Integration/Test/Stubs/LoggerStub.php b/Test/Integration/Test/Stubs/LoggerStub.php new file mode 100644 index 00000000..d660168e --- /dev/null +++ b/Test/Integration/Test/Stubs/LoggerStub.php @@ -0,0 +1,16 @@ +isRecording) { + $this->playback[] = $message; + } + + // Otherwise, do nothing... + } +} diff --git a/Test/Unit/Model/TransactionTest.php b/Test/Unit/Model/TransactionTest.php index 5daef47d..78ea724f 100644 --- a/Test/Unit/Model/TransactionTest.php +++ b/Test/Unit/Model/TransactionTest.php @@ -78,7 +78,7 @@ public function testBuildLineItems() 'getProductType', 'getPrice', 'getQtyInvoiced', - 'getTaxAmount', + 'getTaxInvoiced', 'getSku', 'getName', ]) @@ -88,7 +88,7 @@ public function testBuildLineItems() $mockItem->expects($this->once())->method('getPrice')->willReturn(60.0); $mockItem->expects($this->once())->method('getQtyInvoiced')->willReturn(2); $mockItem->expects($this->once())->method('getProductType')->willReturn('simple'); - $mockItem->expects($this->once())->method('getTaxAmount')->willReturn(5.0); + $mockItem->expects($this->once())->method('getTaxInvoiced')->willReturn(5.0); $mockItem->expects($this->any())->method('getTjPtc')->willReturn('22222'); $mockItem->expects($this->any())->method('getSku')->willReturn('some-sku'); $mockItem->expects($this->any())->method('getName')->willReturn('A great product'); diff --git a/Test/Unit/Observer/BackfillTransactionsTest.php b/Test/Unit/Observer/BackfillTransactionsTest.php index 274b271d..fdb576e1 100644 --- a/Test/Unit/Observer/BackfillTransactionsTest.php +++ b/Test/Unit/Observer/BackfillTransactionsTest.php @@ -1,151 +1,623 @@ request = $this->createMock(RequestInterface::class); - $this->storeManager = $this->createMock(StoreManager::class); - $this->logger = $this->createMock(Logger::class); - $this->orderRepository = $this->createMock(OrderRepositoryInterface::class); - $this->filterBuilder = $this->createMock(FilterBuilder::class); - $this->searchCriteriaBuilder = $this->createMock(SearchCriteriaBuilder::class); - $this->taxjarConfig = $this->createMock(TaxjarConfig::class); - $this->bulkManagement = $this->createMock(BulkManagementInterface::class); - $this->operationInterfaceFactory = $this->getMockBuilder(OperationInterfaceFactory::class)->disableOriginalConstructor()->getMock(); - $this->identityService = $this->createMock(IdentityService::class); - $this->serializer = $this->createMock(SerializerInterface::class); - $this->userContext = $this->createMock(UserContextInterface::class); + $this->requestMock = $this->getMockBuilder(RequestInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); - $this->logger->expects($this->once())->method('setFilename')->with('transactions.log')->willReturnSelf(); - $this->logger->expects($this->once())->method('force')->willReturnSelf(); + $this->loggerMock = $this->getMockBuilder(Logger::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->searchCriteriaBuilderMock = $this->getMockBuilder(SearchCriteriaBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->bulkManagementMock = $this->getMockBuilder(BulkManagementInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->operationFactoryMock = $this->getMockBuilder(OperationInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->identityServiceMock = $this->getMockBuilder(IdentityGeneratorInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->serializerMock = $this->getMockBuilder(SerializerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->userContextMock = $this->getMockBuilder(UserContextInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->taxjarConfigMock = $this->getMockBuilder(Configuration::class) + ->disableOriginalConstructor() + ->getMock(); + + // Set constructor expectations + $this->loggerMock->expects($this->any())->method('setFilename')->with('transactions.log')->willReturnSelf(); + $this->loggerMock->expects($this->any())->method('force')->willReturnSelf(); + $this->identityServiceMock->expects($this->any())->method('generateId')->willReturn('unique-id'); + + $this->setExpectations(); } - public function testExecuteThrowsExceptionWithoutApiKey() + public function testClassConstantBatchSize() { - $this->taxjarConfig->expects($this->once())->method('getApiKey')->willReturn(''); + $this->assertEquals(100, $this->sut::BATCH_SIZE); + } + public function testClassConstantSyncableStatuses() + { + $this->assertSame(['complete', 'closed'], $this->sut::SYNCABLE_STATUSES); + } + + public function testExecuteMethodThrowsExceptionWithoutApiKey() + { $this->expectException(LocalizedException::class); $this->expectExceptionMessage('Could not sync transactions with TaxJar. Please make sure you have an API key.'); - $sut = $this->getTestSubject(); - $sut->execute(new Observer()); + $this->taxjarConfigMock->expects($this->once())->method('getApiKey')->willReturn(''); + $this->setExpectations(); + + /** @var Observer|MockObject $observerMock */ + $observerMock = $this->createMock(Observer::class); + $observerMock->expects($this->any())->method('getData')->willReturnMap([]); + + $this->sut->execute($observerMock); } - public function testGetSearchCriteria() + /** + * @dataProvider searchCriteriaDataProvider + * @param $paramReturnMap + * @param $dataReturnMap + * @param $dateRange + */ + public function testGetSearchCriteriaMethod($paramReturnMap, $dataReturnMap, $dateRange) { - $this->request->expects($this->any())->method('getParam')->willReturn(null); + $this->requestMock->expects($this->any())->method('getParam')->willReturnMap($paramReturnMap); + $this->setStoreExpectations(); - $mockFilter = $this->createMock(Filter::class); - $this->filterBuilder->expects($this->any())->method('setField')->willReturnSelf(); - $this->filterBuilder->expects($this->any())->method('setValue')->willReturnSelf(); - $this->filterBuilder->expects($this->any())->method('setConditionType')->willReturnSelf(); - $this->filterBuilder->expects($this->any())->method('create')->willReturn($mockFilter); + $searchCriteriaMock = $this->getMockBuilder(SearchCriteriaInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); - $mockCriteria = $this->createMock(SearchCriteria::class); - $this->searchCriteriaBuilder->expects($this->exactly(2))->method('addFilter')->willReturnSelf(); - $this->searchCriteriaBuilder->expects($this->exactly(1))->method('addFilters')->willReturnSelf(); - $this->searchCriteriaBuilder->expects($this->exactly(1))->method('create')->willReturn($mockCriteria); + $this->searchCriteriaBuilderMock->expects($this->exactly(3))->method('addFilter')->willReturnSelf(); + $this->searchCriteriaBuilderMock->expects($this->once())->method('create')->willReturn($searchCriteriaMock); - $sut = $this->getTestSubject(); - $result = $sut->getSearchCriteria(); + $this->setExpectations(); + /** @var Observer|MockObject $observerMock */ + $observerMock = $this->createMock(Observer::class); + $observerMock->expects($this->any())->method('getData')->willReturnMap($dataReturnMap); + $this->sut->observer = $observerMock; - self::assertInstanceOf(SearchCriteria::class, $result); + $this->assertSame($searchCriteriaMock, $this->sut->getSearchCriteria(...$dateRange)); } - protected function getTestSubject(): BackfillTransactions + public function searchCriteriaDataProvider(): array { - return new BackfillTransactions( - $this->request, - $this->storeManager, - $this->logger, - $this->orderRepository, - $this->filterBuilder, - $this->searchCriteriaBuilder, - $this->taxjarConfig, - $this->bulkManagement, - $this->operationInterfaceFactory, - $this->identityService, - $this->serializer, - $this->userContext + $testDates = $this->getTestDates(); + + return [ + 'request_without_configuration' => [ + 'request' => [ + ['store', null, null], + ['website', null, null], + ['from', null, null], + ['to', null, null], + ['force', null, '0'], + ], + 'observer' => [ + ['from', null, null], + ['to', null, null], + ['force', null, null], + ], + 'date_range' => $testDates, + ], + + 'observer_without_configuration' => [ + 'request' => [ + ['store', null, null], + ['website', null, null], + ['from', null, null], + ['to', null, null], + ['force', null, null], + ], + 'observer' => [ + ['from', null, null], + ['to', null, null], + ['force', null, '0'], + ], + 'date_range' => $testDates, + ], + + 'request_with_force_sync_enabled' => [ + 'request' => [ + ['store', null, null], + ['website', null, null], + ['from', null, null], + ['to', null, null], + ['force', null, '1'], + ], + 'observer' => [ + ['from', null, null], + ['to', null, null], + ['force', null, null], + ], + 'date_range' => $testDates, + ], + + 'observer_with_force_sync_enabled' => [ + 'request' => [ + ['store', null, null], + ['website', null, null], + ['from', null, null], + ['to', null, null], + ['force', null, null], + ], + 'observer' => [ + ['from', null, null], + ['to', null, null], + ['force', null, '1'], + ], + 'date_range' => $testDates, + ], + + 'request_with_date_range' => [ + 'request' => [ + ['store', null, null], + ['website', null, null], + ['from', null, '2021-01-01'], + ['to', null, '2021-01-31'], + ['force', null, null], + ], + 'observer' => [ + ['from', null, null], + ['to', null, null], + ['force', null, null], + ], + 'date_range' => [ + '2021-01-01 00:00:00', + '2021-01-31 23:59:59', + ], + ], + + 'observer_with_date_range' => [ + 'request' => [ + ['store', null, null], + ['website', null, null], + ['from', null, null], + ['to', null, null], + ['force', null, null], + ], + 'observer' => [ + ['from', null, '2021-01-01'], + ['to', null, '2021-01-31'], + ['force', null, null], + ], + 'date_range' => [ + '2021-01-01 00:00:00', + '2021-01-31 23:59:59', + ], + ], + + 'request_with_date_range_and_force_sync_enabled' => [ + 'request' => [ + ['store', null, null], + ['website', null, null], + ['from', null, '2021-01-01'], + ['to', null, '2021-01-31'], + ['force', null, '1'], + ], + 'observer' => [ + ['from', null, null], + ['to', null, null], + ['force', null, null], + ], + 'date_range' => [ + '2021-01-01 00:00:00', + '2021-01-31 23:59:59', + ], + ], + + 'observer_with_date_range_and_force_sync_enabled' => [ + 'request' => [ + ['store', null, null], + ['website', null, null], + ['from', null, null], + ['to', null, null], + ['force', null, null], + ], + 'observer' => [ + ['from', null, '2021-01-01'], + ['to', null, '2021-01-31'], + ['force', null, '1'], + ], + 'date_range' => [ + '2021-01-01 00:00:00', + '2021-01-31 23:59:59', + ], + ], + + 'request_with_website' => [ + 'request' => [ + ['store', null, null], + ['website', null, '1'], + ['from', null, null], + ['to', null, null], + ['force', null, null], + ], + 'observer' => [ + ['from', null, null], + ['to', null, null], + ['force', null, null], + ], + 'date_range' => $testDates, + ], + ]; + } + + /** + * @param bool $forceSync + * @param int $count + * @param string $updatedAt + * @param string $syncedAt + * @dataProvider orderDataProvider + */ + public function testGetOrdersMethod(bool $forceSync, int $count, string $updatedAt, string $syncedAt) + { + $orderMock = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->addMethods(['getTjSalestaxSyncDate']) + ->onlyMethods(['getUpdatedAt']) + ->getMock(); + + if (!$forceSync) { + $orderMock->expects($this->once())->method('getUpdatedAt')->willReturn($updatedAt); + $orderMock->expects($this->once())->method('getTjSalestaxSyncDate')->willReturn($syncedAt); + } + + $orderSearchResult = $this->createMock(OrderSearchResultInterface::class); + $orderSearchResult->expects($this->once())->method('getItems')->willReturn([$orderMock]); + $this->orderRepositoryMock->expects($this->once())->method('getList')->willReturn($orderSearchResult); + + $this->setExpectations(); + + $observerMock = $this->createMock(Observer::class); + $observerMock->expects($this->any())->method('getData')->willReturnMap([ + ['from', null, null], + ['to', null, null], + ['force', null, $forceSync], + ],); + $this->sut->observer = $observerMock; + + $criteriaMock = $this->getMockForAbstractClass(SearchCriteriaInterface::class); + + $this->assertCount($count, $this->sut->getOrders($criteriaMock)); + } + + public function orderDataProvider(): array + { + return [ + 'force_sync_enabled_for_unsyncable_order' => [ + 'force' => true, + 'count' => 1, + 'updated_at' => '2021-01-01', + 'sync_date' => '2021-01-01', + ], + 'force_sync_disabled_for_unsyncable_order' => [ + 'force' => false, + 'count' => 0, + 'updated_at' => '2021-01-01', + 'sync_date' => '2021-01-01', + ], + 'force_sync_enabled_for_syncable_order' => [ + 'force' => true, + 'count' => 1, + 'updated_at' => '2021-02-01', + 'sync_date' => '2021-01-01', + ], + 'force_sync_disabled_for_syncable_order' => [ + 'force' => false, + 'count' => 1, + 'updated_at' => '2021-02-01', + 'sync_date' => '2021-01-01', + ], + ]; + } + + public function testSyncTransactionMethodThrowsExceptionWhenScheduleBulkReturnsFalse() + { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage('Bulk management encountered an unknown error.'); + + $this->bulkManagementMock->expects($this->once())->method('scheduleBulk')->willReturn(false); + + $operationMock = $this->getMockForAbstractClass(OperationInterface::class); + $this->operationFactoryMock->expects($this->once())->method('create')->willReturn($operationMock); + + $this->setExpectations(); + + $orderMock = $this->createMock(Order::class); + $orderMock->expects($this->once())->method('getEntityId')->willReturn(1); + + $this->sut->syncTransactions([$orderMock]); + } + + /** + * @dataProvider syncTransactionDataProvider + */ + public function testSyncTransactionMethod($orders, $count, $force) + { + $this->bulkManagementMock->expects($this->once())->method('scheduleBulk')->willReturn(true); + + $operationMock = $this->getMockForAbstractClass(OperationInterface::class); + $this->operationFactoryMock->expects($this->exactly($count))->method('create')->willReturn($operationMock); + + $this->setExpectations(); + + $this->sut->syncTransactions($orders); + } + + public function syncTransactionDataProvider(): array + { + return [ + 'single_operation_without_force' => [ + 'orders' => array_map([$this, 'getOrderStub'], range(1, 100)), + 'count' => 1, + 'force' => false, + ], + 'single_operation_with_force' => [ + 'orders' => array_map([$this, 'getOrderStub'], range(1, 100)), + 'count' => 1, + 'force' => true, + ], + 'multiple_operations_without_force' => [ + 'orders' => array_map([$this, 'getOrderStub'], range(1, 500)), + 'count' => 5, + 'force' => false, + ], + 'multiple_operations_with_force' => [ + 'orders' => array_map([$this, 'getOrderStub'], range(1, 500)), + 'count' => 5, + 'force' => true, + ], + ]; + } + + public function testSuccessMethod() + { + [$startDate, $endDate] = $this->getTestDates(); + $expectedConfig = json_encode([ + 'date_start' => $startDate, + 'date_end' => $endDate, + 'force_sync' => false, + ]); + $expectedMessage = "No un-synced orders were found!"; + $expectedLogMessage = "$expectedMessage Detail: \"$expectedConfig\""; + + $this->loggerMock->expects($this->once()) + ->method('log') + ->with($expectedLogMessage) + ->willReturnSelf(); + + $this->setExpectations(); + + $this->sut->success(); + } + + public function testFailMethod() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('test-error'); + + $payload = json_encode([ + 'date_start' => '2021-01-01 00:00:00', + 'date_end' => '2021-01-31 23:59:59', + 'force_sync' => false, + ]); + + $logMessage = "Failed to schedule transaction sync! Message: \"test-error\" Detail: \"$payload\""; + + $this->loggerMock->expects($this->once())->method('log')->with($logMessage)->willReturnSelf(); + + $exception = new Exception('test-error'); + + $dataMap = [ + ['from', null, '2021-01-01'], + ['to', null, '2021-01-31'], + ['force', null, '0'], + ]; + + $observerMock = $this->getMockBuilder(Observer::class)->disableOriginalConstructor()->getMock(); + $observerMock->expects($this->any())->method('getData')->willReturnMap($dataMap); + + $this->setExpectations(); + $this->sut->observer = $observerMock; + $this->sut->fail($exception); + } + + protected function expectSearchCriteria(): void + { + $searchCriteriaMock = $this->getMockBuilder(SearchCriteriaInterface::class) + ->disableOriginalConstructor() + ->addMethods(['__toArray']) + ->getMockForAbstractClass(); + $searchCriteriaMock->expects($this->once())->method('__toArray')->willReturn((object)[]); + + $this->searchCriteriaBuilderMock->expects($this->exactly(3))->method('addFilter')->willReturnSelf(); + $this->searchCriteriaBuilderMock->expects($this->once())->method('create')->willReturn($searchCriteriaMock); + } + + /** + * @param $id + * @return Order|MockObject + */ + protected function getOrderStub($id) + { + $orderStub = $this->createMock(Order::class); + $orderStub->expects($this->any())->method('getEntityId')->willReturn($id); + return $orderStub; + } + + /** + * Used when StoreManager class is called while retrieving search criteria. + */ + protected function setStoreExpectations(): void + { + $storeListMock = []; + + if ($this->requestMock->getParam('store') && !$this->requestMock->getParam('website')) { + $storeMock = $this->getMockForAbstractClass(StoreInterface::class); + $storeMock->expects($this->once())->method('getWebsiteId')->willReturn(1); + $storeListMock[] = $storeMock; + } + + $this->storeManagerMock->expects($this->any())->method('getStores')->willReturn($storeListMock); + } + + /** + * Used to set the test subject. + * + * When called in the `setUp` method, this sets a new instance of the class under test. + * If any mock dependencies need to be configured within a test method, calling this method + * will "refresh" the configured dependencies by creating a new test subject with the updated + * mock dependencies. + */ + protected function setExpectations() + { + $this->sut = new BackfillTransactions( + $this->requestMock, + $this->loggerMock, + $this->orderRepositoryMock, + $this->storeManagerMock, + $this->searchCriteriaBuilderMock, + $this->bulkManagementMock, + $this->operationFactoryMock, + $this->identityServiceMock, + $this->serializerMock, + $this->userContextMock, + $this->taxjarConfigMock ); } + + /** + * This method is necessary to replicate the hard dependency of DateTime object + * @return array + */ + private function getTestDates(): array + { + $date = new \DateTimeImmutable(); + return [ + $date->sub(new \DateInterval('P1D'))->setTime(0, 0)->format('Y-m-d H:i:s'), + $date->setTime(23, 59, 59)->format('Y-m-d H:i:s'), + ]; + } } diff --git a/view/adminhtml/templates/transaction_sync.phtml b/view/adminhtml/templates/transaction_sync.phtml index 86220ada..7e22b739 100644 --- a/view/adminhtml/templates/transaction_sync.phtml +++ b/view/adminhtml/templates/transaction_sync.phtml @@ -74,8 +74,8 @@ url: 'escapeUrl($block->getStoreUrl('taxjar/transaction/backfill')) ?>', method: 'GET', data: { - from_date: fromDate, - to_date: toDate, + from: fromDate, + to: toDate, force: force, store: 'getRequest()->getParam('store') ?>', website: 'getRequest()->getParam('website') ?>',