addressFactory = $addressFactory; $this->quoteFactory = $quoteFactory; $this->quoteManagement = $quoteManagement; $this->cartRepository = $cartRepository; $this->cartManagement = $cartManagement; $this->storeManager = $storeManager; $this->stockRegistry = $stockRegistry; $this->getStockIdForCurrentWebsite = $getStockIdForCurrentWebsite; $this->getReservationsQuantity = $getReservationsQuantity; $this->customersSyncService = $customersSyncService; $this->orderMapper = $orderMapper; $this->sourceRepository = $sourceRepository; $this->productRepository = $productRepository; $this->orderRepository = $orderRepository; $this->orderFactory = $orderFactory; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->addressExtensionFactory = $addressExtensionFactory; $this->shippingRate = $shippingRate; $this->productsSync = $productsSync; $this->helper = $helper; $this->logger = $logger; $this->defaultStoreId = $this->helper->getDefaultStoreId(); } /** * Create or Skip a order based on the provided data * * @param array $data * @return array * @throws LocalizedException */ public function createOrSkip(array $data) { $order = $this->orderMapper->getOrderBy('netsuite_id', $data['netsuite_order_id']); if ($order) { return [ 'data' => $order, 'is_new' => false ]; } $order = $this->create($data); return [ 'data' => $order, 'is_new' => true ]; } /** * Create or Update a order based on the provided data. Only update status for now. TODO: Update other order attributes including item, price etc. * * @param array $data * @return array * @throws LocalizedException */ public function createOrUpdate(array $data) { // $this->logger->info("[OrdersSyncService] createOrUpdate: " . json_encode($data)); $order = $this->orderMapper->getOrderBy('netsuite_id', $data['netsuite_order_id']); if ($order) { $this->updateOrderStatus($order, $data['status']); return [ 'data' => $order, 'is_new' => false ]; } $order = $this->create($data); return [ 'data' => $order, 'is_new' => true ]; } private function updateOrderStatus($order, $newStatus) { $currentStatus = $order->getStatus(); if ($currentStatus === $newStatus) { // $this->logger->info("[OrdersSyncService] updateOrderStatus: Order #" . $order->getIncrementId() . " is already in status '$currentStatus'. No change needed."); return false; } // Map status ? state $statusStateMap = [ 'pending' => \Magento\Sales\Model\Order::STATE_NEW, 'processing' => \Magento\Sales\Model\Order::STATE_PROCESSING, 'complete' => \Magento\Sales\Model\Order::STATE_COMPLETE, 'canceled' => \Magento\Sales\Model\Order::STATE_CANCELED, 'closed' => \Magento\Sales\Model\Order::STATE_CLOSED, 'holded' => \Magento\Sales\Model\Order::STATE_HOLDED, 'payment_review' => \Magento\Sales\Model\Order::STATE_PAYMENT_REVIEW, ]; $newState = $statusStateMap[$newStatus] ?? null; if (!$newState) { $this->logger->error("[OrdersSyncService] updateOrderStatus: Unknown status '$newStatus'."); return false; } try { $order->setStatus($newStatus); $order->setState($newState); $this->orderRepository->save($order); // $this->logger->info("[OrdersSyncService] updateOrderStatus: Order #" . $order->getIncrementId() . " updated from status '$currentStatus' to '$newStatus' (state: '$newState')."); return true; } catch (\Exception $e) { $this->logger->error("[OrdersSyncService] updateOrderStatus: Error updating order -> " . $e->getMessage()); return false; } } /** * Create or Update a order based on the provided data * * @param array $data * @return OrderInterface * @throws LocalizedException */ public function create(array $data) { $quote = $this->buildQuote($data); $order = $this->quoteManagement->submit($quote); $order = $this->orderRepository->get($order->getId()); $order->setStatus($data['status']) ->setState($data['status']) ->setCreatedAt($data['purchase_date']) ->setEmailSent(0) ->setNetsuiteId($data['netsuite_order_id']) ->setNetsuiteNumber($data['netsuite_order_number']) ->setIsNetsuiteSync(!empty($data['netsuite_order_number'])); $this->addDiscount($order, $data['items']); $this->addTax($order, $data['items']); $this->addShipping($order, $data['items']); $this->orderRepository->save($order); $this->afterOrderSave(); return $order; } /** * @param array $data * @return Quote * @throws LocalizedException */ private function buildQuote(array $data): Quote { $quote = $this->initQuote($data); $quote = $this->addItems($quote, $data['items']); $quote = $this->setAddresses($quote, $data); $quote = $this->setPayment($quote, $data['payment']['method'] ?? 'checkmo'); $quote->setInventoryProcessed(false); $quote->setTotalsCollectedFlag(false); $quote->collectTotals(); // $this->addTax($quote, $data['items']); // $quote->setTaxAmount($data['tax']); // $quote->setBaseTaxAmount($data['tax']); $this->cartRepository->save($quote); return $quote; } /** * @param array $data * @return Quote * @throws LocalizedException */ private function initQuote(array $data): Quote { $store = $this->storeManager->getStore($this->defaultStoreId); $websiteId = $store->getWebsiteId(); // get the customer by email $customer = $this->getOrCreateCustomer($data, $websiteId); $cartId = $this->cartManagement->createEmptyCart(); $quote = $this->cartRepository->get($cartId); $quote->setStore($store); $quote->setCurrency(); $quote->assignCustomer($customer); if ($quote->getCustomer()->getId()) { $quote->setCustomerIsGuest(0); } // Write PO Number $quote->setOtherRefNum($data['other_ref_num']); // Set custom data $quote->setData('netsuite_order_id', $data['netsuite_order_id']); return $quote; } /** * @param array $data * @param int $websiteId * @return \Magento\Customer\Api\Data\CustomerInterface * @throws LocalizedException */ private function getOrCreateCustomer(array $data, int $websiteId) { if (empty($data['firstname'])) $data['firstname'] = $data['shipping_address']['firstname'] ?? $data['billing_address']['firstname'] ?? ''; if (empty($data['lastname'])) $data['lastname'] = $data['shipping_address']['lastname'] ?? $data['billing_address']['lastname'] ?? ''; $customerData = $data; if (isset($data['customer'])) { if (count($data['customer']) == 1) $data['customer'] = reset($data['customer']); if (($data['customer']['email'] ?? '') == '') $data['customer']['email'] = $data['email']; $customerData = $data['customer']; } if (empty($data['firstname'])) $data['firstname'] = $data['shipping_address']['firstname'] ?? $data['billing_address']['firstname'] ?? $customerData['firstname'] ?? ''; if (empty($data['lastname'])) $data['lastname'] = $data['shipping_address']['lastname'] ?? $data['billing_address']['lastname'] ?? $customerData['lastname'] ?? ''; $result = $this->customersSyncService->createOrSkip($customerData); return $result['data']; // try { // $customer = $this->customerRepository->get($data['email'], $websiteId); // } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { // $customer = $this->customerFactory->create(); // } // if (!$customer->getId()) { // $customer->setWebsiteId($websiteId) // ->setFirstname($data['shipping_address']['firstname']) // ->setLastname($data['shipping_address']['lastname']) // ->setEmail($data['email']); // $customer = $this->customerRepository->save($customer); // } // return $customer; } /** * @param Quote $quote * @param Quote $quote * @return Quote * @throws LocalizedException */ private function addItems(Quote $quote, array $items = []): Quote { $productNameError = ''; foreach ($items as $item) { if ($item['type'] != self::INVT_PART) continue; $product = $this->productRepository->get($item['sku']); // need to be changed if multi location // Product stock is being synced by cronjob, ProductSyncWorker::processQueueItems. There is no need to touch stock during order sync /* $stockDetail = $this->productsSync->updateStockForProduct($item['sku'], $item, $product->getStore()->getWebsiteId()); $this->productStocks[$item['sku']] = $stockDetail; */ $stockItem = $this->stockRegistry->getStockItem($product->getId()); $qty = (int) $stockItem->getQty() + $item['qty']; $stockId = $this->getStockIdForCurrentWebsite->execute(); $qty -= $this->getReservationsQuantity->execute($item['sku'], $stockId); $stockItem->setData('is_in_stock', 1); $stockItem->setData('qty', $qty); $stockItem->setData('manage_stock', 1); $stockItem->setData('use_config_notify_stock_qty', 1); $stockItem->save(); // $status = $product->getStatus(); // $salable = $product->getData('salable'); $product->setOrigData('status', 1); $product->setStatus(1); $product->setData('salable', true); $product->setPrice($item['price']); $discountAmount = ((float)$item['amount'] - (float)$item['price'] * $item['qty']) / $item['qty']; // $product->setSpecialPrice($item['price'] + $discountAmount); $productNameError = $productNameError . ' ' . $product->getName() . '( SKU : ' . $product->getSku() . ')'; // $this->logger->info($product->getSku() . ' ' . $item['qty']); $quoteItem = $quote->addProduct($product, (int) $item['qty']); // $product->setStatus($status); // $product->setData('salable', $salable); // $product->save(); // $quoteItem->setTaxPercent($quote->getTaxPercent()); // $quoteItem->setTaxAmount($item['amount'] * $quote->getTaxPercent() / 100); // $quoteItem->setBaseTaxAmount($item['amount'] * $quote->getTaxPercent() / 100); // $quoteItem->setDiscountAmount($discountAmount); // $quoteItem->setBaseDiscountAmount($discountAmount); // $quoteItem->setSubtotal($item['price'] * $item['qty']); // $quoteItem->setBaseSubtotal($item['price'] * $item['qty']); // $quoteItem->setRowTotal($item['amount']); // $quoteItem->setBaseRowTotal($item['amount']); $quoteItem->setCustomPrice($item['price']); $quoteItem->setOriginalCustomPrice($item['price']); $quoteItem->getProduct()->setIsSuperMode(true); } return $quote; } /** * @param OrderInterface $order * @param array $items */ private function addDiscount(OrderInterface $order, array $items): void { $discount = 0; $discountDescription = ''; $itemMap = []; foreach ($items as $item) { if ($item['type'] == self::DISCOUNT) { $discount += $item['amount']; $discountDescription = $item['sku']; } else { $itemMap[$item['sku']] = $item; } } if ($discount == 0) { return; } $order->setDiscountAmount($discount); $order->setBaseDiscountAmount($discount); $order->setDiscountDescription($discountDescription); $order->setGrandTotal($order->getGrandTotal() + $discount); $order->setBaseGrandTotal($order->getBaseGrandTotal() + $discount); foreach ($order->getAllItems() as $item) { if ($item) { $itemData = $itemMap[$item->getSku()]; $discountAmount = $itemData['price'] * $itemData['qty'] - $itemData['amount']; $discountRate = $itemData['price'] == 0 ? 0 : $discountAmount / ($itemData['price'] * $itemData['qty']) * 100; $item->setDiscountPercent($discountRate); $item->setDiscountAmount($discountAmount); $item->setBaseDiscountAmount($discountAmount); $item->save(); } } } /** * @param OrderInterface $order * @param array $items */ private function addTax(OrderInterface $order, array $items): void { $tax = 0; $taxRate = 0; foreach ($items as $item) { if ($item['type'] == self::TAX_GROUP) { $tax += $item['amount']; $taxRate += $item['price']; } } if ($tax <= 0) { return; } // $order->setTaxPercent($taxRate); $order->setTaxAmount($tax); $order->setBaseTaxAmount($tax); $order->setGrandTotal($order->getGrandTotal() + $tax); $order->setBaseGrandTotal($order->getBaseGrandTotal() + $tax); foreach ($order->getAllItems() as $item) { if ($item) { $item->setTaxPercent($taxRate); $taxAmount = $item->getPrice() * $item->getQtyOrdered() * $taxRate / 100; $item->setTaxAmount($taxAmount); $item->setBaseTaxAmount($taxAmount); $priceInclTax = $item->getPrice() + $item->getPrice() * $taxRate / 100; $item->setPriceInclTax($priceInclTax); $item->setBasePriceInclTax($priceInclTax); $item->setRowTotalInclTax($item->getRowTotal() + $item->getTaxAmount()); $item->setBaseRowTotalInclTax($item->getBaseRowTotal() + $item->getBaseTaxAmount()); $item->save(); } } } /** * @param Quote $quote * @param array $data * @return Quote */ private function setAddresses(Quote $quote, array $data): Quote { if (!isset($data['shipping_address']) || empty($data['shipping_address'])) { if (isset($data['customer']) && ($data['customer']['isdefaultshipping'] ?? false)) { $data['shipping_address'] = $data['customer']['shipping_address']; } else if ($shippingAddressId = $quote->getCustomer()->getDefaultShipping()) { $shippingAddress = $this->addressFactory->create()->load($shippingAddressId); $data['shipping_address'] = $shippingAddress->getData(); } else if (isset($data['customer'][0]['shipping_address'])) { $data['shipping_address'] = $data['customer'][0]['shipping_address']; } } else { if ($data['shipping_address']['firstname'] == '') { if (isset($data['customer'])) { $data['shipping_address']['firstname'] = $data['customer']['firstname']; } else { $data['shipping_address']['firstname'] = $quote->getCustomer()->getFirstname(); } } if ($data['shipping_address']['lastname'] == '') { if (isset($data['customer'])) { $data['shipping_address']['lastname'] = $data['customer']['lastname']; } else { $data['shipping_address']['lastname'] = $quote->getCustomer()->getLastname(); } } } if (!isset($data['billing_address']) || empty($data['billing_address'])) { if (isset($data['customer']) && ($data['customer']['isdefaultbilling'] ?? false)) { $data['billing_address'] = $data['customer']['billing_address']; } else if ($billingAddressId = $quote->getCustomer()->getDefaultBilling()) { $billingAddress = $this->addressFactory->create()->load($billingAddressId); $data['billing_address'] = $billingAddress->getData(); unset($data['billing_address']['entity_id']); } else if (isset($data['customer'][0]['billing_address'])) { $data['billing_address'] = $data['customer'][0]['billing_address']; } } if (isset($data['shipping_address']) && !$data['shipping_address']['company']) $data['shipping_address']['company'] = $data['company']; if (isset($data['billing_address']) && !$data['billing_address']['company']) $data['billing_address']['company'] = $data['company']; $quote->getBillingAddress()->addData($data['billing_address'] ?? $data['shipping_address'] ?? []); $shippingAddress = $quote->getShippingAddress() ->addData($data['shipping_address'] ?? $data['billing_address'] ?? []); // $shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()); $shippingItem = $this->getShippingItem($data['items']); $shippingItem['sku'] = $shippingItem['sku'] ?? ''; $this->shippingRate->setCode($shippingItem['sku']) ->setPrice($shippingItem['amount'] ?? 0); if (strpos(strtolower($shippingItem['sku']), 'ups') !== false) { $shippingAddress->setShippingMethod('ups'); } else if (strpos(strtolower($shippingItem['sku']), 'fedex') !== false) { $shippingAddress->setShippingMethod('fedex'); } else { $shippingAddress->setShippingMethod('flatrate_flatrate'); } $m2ShipMethod = $this->orderMapper->parseNetSuiteShippingMethod($shippingItem['netsuite_order_item_id'] ?? ''); $this->logger->info("Shipping Method: " . $shippingItem['netsuite_order_item_id'] . " -> " . $m2ShipMethod); if ($m2ShipMethod == 'instore_pickup') { $pickupLocationCode = $this->getPickupLocationSourceCode($data['netsuite_order_location_id']); $pickupSource = $this->sourceRepository->get($pickupLocationCode); $this->logger->info("Pickup Location: {$pickupLocationCode}"); $street = $pickupSource->getStreet(); if (!is_array($street)) { $street = [$street]; } $shippingAddress->setShippingMethod($m2ShipMethod) ->setFirstname($data['shipping_address']['firstname']) ->setLastname($data['shipping_address']['lastname']) ->setSameAsBilling(false) // *** ->setStreet($street) // *** ->setCity($pickupSource->getCity()) // *** ->setRegionId($pickupSource->getRegionId()) // *** ->setRegion($pickupSource->getRegion()) // *** ->setPostcode($pickupSource->getPostcode()) // *** ->setCountryId($pickupSource->getCountryId()) // *** ->setTelephone($data['shipping_address']['telephone'] ?? '000-000-0000') ->setShippingDescription("In-Store Pickup Delivery - {$pickupSource->getName()}"); $extensionAttributes = $shippingAddress->getExtensionAttributes(); if (!$extensionAttributes) { $extensionAttributes = $this->addressExtensionFactory->create(); } $extensionAttributes->setPickupLocationCode($pickupLocationCode); $shippingAddress->setExtensionAttributes($extensionAttributes); } else { $shippingAddress->setCollectShippingRates(true) ->setShippingMethod($m2ShipMethod) ->collectShippingRates(); $shippingAmount = (float) $shippingItem['amount']; $quote->getShippingAddress() ->setShippingAmount($shippingAmount) ->setBaseShippingAmount((float) $shippingItem['amount']) ->setShippingDescription($shippingItem['sku']); $quote->getShippingAddress()->addShippingRate($this->shippingRate); } // $shippingAddress->setCollectShippingRates(true) // ->setShippingMethod('ups_03') // ->collectShippingRates(); // $shippingAmount = (float) $shippingItem['amount']; // $quote->getShippingAddress() // ->setShippingAmount($shippingAmount) // ->setBaseShippingAmount((float) $shippingItem['amount']) // ->setShippingDescription($shippingItem['sku']); // $quote->getShippingAddress()->addShippingRate($this->shippingRate); $quote->getBillingAddress()->setCustomerAddressId(null); $quote->getShippingAddress()->setCustomerAddressId(null); return $quote; } /** * get shipping item * * @param array $items * @return array */ private function getShippingItem(array $items): array { foreach ($items as $item) { if ($item['type'] === self::SHIP_ITEM && $item['price']) { return $item; } } return []; } /** * set shipping to order * * @param OrderInterface $order * @param array $items */ private function addShipping(OrderInterface $order, array $items) { $shippingItem = $this->getShippingItem($items); $originShippingAmount = $order->getShippingAmount(); $order->setShippingAmount($shippingItem['amount']); $order->setBaseShippingAmount($shippingItem['amount']); $order->setShippingDescription($shippingItem['sku']); $grandTotal = $order->getGrandTotal() - $originShippingAmount + $shippingItem['amount']; $order->setGrandTotal($grandTotal); $order->setBaseGrandTotal($grandTotal); } /** * @param Quote $quote * @param string $paymentMethod * @return Quote */ private function setPayment(Quote $quote, string $paymentMethod): Quote { if (!$paymentMethod || $paymentMethod == '') { $paymentMethod = 'checkmo'; } $quote->setPaymentMethod($paymentMethod); $quote->getPayment()->importData(['method' => $paymentMethod]); return $quote; } public function afterOrderSave() { // Product stock is being synced by cronjob, ProductSyncWorker::processQueueItems. There is no need to touch stock during order sync // If for some reason need to uncomment the below, also need to uncomment the part inside addItems() /* foreach ($this->productStocks as $sku => $stockDetail) { $stockDetail['qty'] = $stockDetail['from']; $this->productsSync->updateStockForProduct($sku, $stockDetail); } $this->productStocks = []; */ } /** * Save NetSuite Order ID to Magento Order * * @param OrderInterface $order * @param string $netsuiteOrderId * @return void */ public function saveNetsuiteId(OrderInterface $order, string $netsuiteOrderId) { $order->setNetsuiteId($netsuiteOrderId); $this->orderRepository->save($order); } /** * Search inventory source code by NetSuite location id. * * @param string $nsLocationId * @return string */ private function getPickupLocationSourceCode($nsLocationId) { $searchCriteria = $this->searchCriteriaBuilder ->addFilter('netsuite_id', $nsLocationId) ->create(); $sources = $this->sourceRepository->getList($searchCriteria)->getItems(); if (count($sources) > 0) { $source = reset($sources); return $source->getSourceCode() ?? ''; } return ''; } }