Files

605 lines
23 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Copyright © All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Lts\Sales\Cron;
use JR\NetSuite\Lib\Restful\NetSuiteAPI;
use JR\NetSuite\Logger\Logger;
use Lts\Netterm\Model\Payment\Netterm;
use Lts\Sales\Netsuite\ShippingMethodMapping;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Customer\Api\Data\CustomerInterface;
use Magento\Framework\Api\SearchCriteriaBuilderFactory;
use Magento\Framework\Exception\InvalidArgumentException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Inventory\Model\Source\Command\GetList;
use Magento\InventoryApi\Api\Data\SourceItemInterface;
use Magento\InventoryApi\Api\SourceItemRepositoryInterface;
use Magento\InventoryDistanceBasedSourceSelection\Model\DistanceProvider\GetDistanceFromSourceToAddress;
use Magento\InventoryInStorePickupSales\Model\Order\GetPickupLocationCode;
use Magento\InventorySourceSelectionApi\Api\Data\AddressInterfaceFactory;
use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Quote\Api\Data\CartInterface;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\Data\OrderItemInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\Sales\Model\ResourceModel\Order\Collection;
use Magento\InventoryApi\Api\Data\SourceInterface;
use Psr\Log\LoggerInterface;
class OrderNetsuiteSync1
{
private Logger $logger;
private Collection $orderCollection;
private OrderInterface $order;
private CartRepositoryInterface $cartRepository;
private ?CartInterface $quote = null;
private GetPickupLocationCode $getPickupLocationCode;
/**
* @var SourceInterface[]
*/
private array $sources = [];
private GetList $getList;
private ProductRepositoryInterface $productRepository;
private NetSuiteAPI $netSuiteAPI;
const HUB_REPOSITORY_CODE = ['CA-LA', 'TX', 'NJ-MountLaurel'];
const NETSUITE_CREDIT_CARD_PAYMENT_ID = '12';
const NETSUITE_SALES_ORDER_SERVICE = 'record/v1/salesOrder';
const NETSUITE_CUSTOMER_DEPOSIT_SERVICE = 'record/v1/customerDeposit';
private OrderRepositoryInterface $orderRepository;
const DEFAULT_INVENTORY_SOURCE = 'default';
private SourceItemRepositoryInterface $sourceItemRepository;
private SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory;
private AddressInterfaceFactory $addressInterfaceFactory;
private GetDistanceFromSourceToAddress $getDistanceFromSourceToAddress;
private CustomerRepositoryInterface $customerRepository;
private ?CustomerInterface $customer = null;
private LoggerInterface $magentoLogger;
/**
* @param Logger $logger
* @param Collection $orderCollection
* @param CartRepositoryInterface $cartRepository
* @param GetPickupLocationCode $getPickupLocationCode
* @param GetList $getList
* @param ProductRepositoryInterface $productRepository
* @param NetSuiteAPI $netSuiteAPI
* @param OrderRepositoryInterface $orderRepository
* @param SourceItemRepositoryInterface $sourceItemRepository
* @param SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory
* @param GetDistanceFromSourceToAddress $getDistanceFromSourceToAddress
* @param AddressInterfaceFactory $addressInterfaceFactory
*/
public function __construct(
Logger $logger,
Collection $orderCollection,
CartRepositoryInterface $cartRepository,
GetPickupLocationCode $getPickupLocationCode,
GetList $getList,
ProductRepositoryInterface $productRepository,
NetSuiteAPI $netSuiteAPI,
OrderRepositoryInterface $orderRepository,
SourceItemRepositoryInterface $sourceItemRepository,
SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory,
GetDistanceFromSourceToAddress $getDistanceFromSourceToAddress,
AddressInterfaceFactory $addressInterfaceFactory,
CustomerRepositoryInterface $customerRepository,
LoggerInterface $magentoLogger
) {
$this->logger = $logger;
$this->orderCollection = $orderCollection;
$this->cartRepository = $cartRepository;
$this->getPickupLocationCode = $getPickupLocationCode;
$this->getList = $getList;
$this->productRepository = $productRepository;
$this->netSuiteAPI = $netSuiteAPI;
$this->orderRepository = $orderRepository;
$this->sourceItemRepository = $sourceItemRepository;
$this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory;
$this->addressInterfaceFactory = $addressInterfaceFactory;
$this->getDistanceFromSourceToAddress = $getDistanceFromSourceToAddress;
$this->customerRepository = $customerRepository;
$this->magentoLogger = $magentoLogger;
$this->getInventorySources();
}
/**
* @return array
*/
protected function getInventorySources()
{
if (empty($this->sources)) {
foreach ($this->getList->execute()->getItems() as $item) {
$this->sources[$item->getSourceCode()] = $item;
}
}
return $this->sources;
}
/**
* @throws LocalizedException
*/
protected function getCardInfo()
{
$payment = $this->order->getPayment();
$billingAddress = $this->order->getBillingAddress();
$paymentMethod = $payment->getMethodInstance();
if ($paymentMethod->isOffline()) {
return '';
}
return $payment->getCcType().': '.$payment->getCcLast4().', '.
$billingAddress->getFirstname().' '.$billingAddress->getLastname().': '.
implode(' ', $billingAddress->getStreet()).', '.
$billingAddress->getCity().', '.$billingAddress->getRegion() .', '.$billingAddress->getPostcode();
}
protected function getProductNetsuiteId(int $productId)
{
try {
$product = $this->productRepository->getById($productId);
return $product->getNetsuiteId();
} catch (NoSuchEntityException $e) {
}
return null;
}
/**
* @param string $pickupLocationCode
* @return array
*/
protected function getNetsuiteOrderItems(string $pickupLocationCode)
{
$data = [];
if (!empty($pickupLocationCode)) {
foreach ($this->order->getAllVisibleItems() as $item) {
/* @var $item OrderItemInterface */
$data[$item->getItemId()] = [
'item' => ['id' => $this->getProductNetsuiteId((int)$item->getProductId())],
'rate' => $this->numberFormat($item->getPrice()),
'sku' => $item->getSku(),
'quantity' => $item->getQtyOrdered()*1.0
];
}
}
return $data;
}
/**
* 拆单规则:
* 在checkout的时候,如果客户选择ship out,我们首先看哪个hub有相对应的库存,如果每一个hub都有货,那我们就根据shipping address的
* region来确定哪个hub出货。譬如旧金山下单了,三个hub都有货,那我们优先从西部的hub(洛杉矶)出货。假如说三个hub都没货,那我们就优先从
* 有货的warehouse出,如果有多个仓库有货的话还是按照region来。
*
* 如果没有一个仓库能满足整张单的库存量的话,那我们就会拆单。具体的选择仓库或者hub的逻辑还是按照上面的那个来,哪个能fulfill的多的就从哪
* 一个出,如果有多个的话就距离优先。如果可以的话,可以在M2就拆成两张单吗?税的话应该怎么拆都不会变,因为都是按照销售的物品来的。运费的话
* 我们是这样子想的,如果拆成两张单,我们只会收客户两张单里面运费高的那一份运费,剩下的运费我们自己承担。
*
* hub的话是指我们三个大仓库,分别是LA,TX,NJ,
*
* 如果客户选择pickup的话,如果他选择的preferred warehouse里面有库存的话那就直接可以process;如果没有的话,我们就会提示客户说当前物
* 品在你preferred warehouse没有库存,你可以选择配送或者把缺货商品从购物车里移除
*
* (1) 关于订单同步到netsuite时的拆单
* (1.1)根据上述规则拆单
* (1.2)运费可以放在其中一张单上
* (1.3)订单金额根据拆单后的产品的金额计算
* (1.4)税可以不用给
*
* @return array
*/
protected function getSyncOrders()
{
/* @var $info string | null */
$information = $this->order->getData('additional_information');
if ($information) {
$info = json_decode($information, true);
if (is_array($info) && isset($info['syncOrders'])) {
return $info['syncOrders'];
}
}
$pickupLocationCode = $this->getPickupLocationCode->execute($this->order);
if ($pickupLocationCode) {
return [$pickupLocationCode => $this->getNetsuiteOrderItems($pickupLocationCode)];
}
return $this->orderSplit();
}
/**
* @param string|float $number
* @return float
*/
protected function numberFormat(string | float $number)
{
return number_format($number*1.0, 2, '.', '')*1.0;
}
/**
* order split
* @return array
*/
protected function orderSplit()
{
$result = [];
foreach ($this->order->getAllVisibleItems() as $item) {
/* @var $item OrderItemInterface */
echo sprintf('order %s item id %d', $this->order->getIncrementId(), $item->getItemId());
foreach ($this->getInventorySourceFromProduct($item) as $sourceCode =>$splitItem) {
$result[$sourceCode][$item->getItemId()] = [
'item' => ['id' => $this->getProductNetsuiteId((int)$item->getProductId())],
'rate' => $this->numberFormat($item->getPrice()),
'sku' => $item->getSku(),
'quantity' => $splitItem[$item->getItemId()]*1.0
];
}
}
return $result;
}
/**
* @param OrderItemInterface $orderItem
* @return float[][]
*/
protected function getInventorySourceFromProduct(OrderItemInterface $orderItem)
{
$searchCriteria = $this->searchCriteriaBuilderFactory->create()
->addFilter(SourceItemInterface::SKU, $orderItem->getSku())
->addFilter(SourceItemInterface::SOURCE_CODE, self::DEFAULT_INVENTORY_SOURCE, 'neq')
->create();
$sourceItems = $this->sourceItemRepository->getList($searchCriteria)->getItems();
$availableItems = [];
$distances = [];
$oneSource = false;
foreach ($sourceItems as $sourceItem) {
$sourceAddress = $this->addressTransform($this->order->getShippingAddress());
$distances[$sourceItem->getSourceCode()] = $distance = $this->getDistanceFromSourceToAddress->execute(
$this->sources[$sourceItem->getSourceCode()],
$sourceAddress
);
if ($sourceItem->getQuantity() >= $orderItem->getQtyOrdered()) {
$oneSource = true;
$availableItems[$distance] = [
'sourceItem' => $sourceItem,
'isHub' => in_array($sourceItem->getSourceCode(), self::HUB_REPOSITORY_CODE),
];
}
}
//one warehouse inventory is not enough for this order product
if (empty($availableItems)) {
foreach ($sourceItems as $sourceItem) {
$availableItems[$distances[$sourceItem->getSourceCode()]] = [
'sourceItem' => $sourceItem,
'isHub' => in_array($sourceItem->getSourceCode(), self::HUB_REPOSITORY_CODE)
];
}
}
ksort($availableItems);
// $warehouseDistances = [];
// foreach ($availableItems as $distance => $items) {
// $warehouseDistances[] = [
// 'product'=>$orderItem->getSku(),
// 'warehouse code'=>$items['sourceItem']->getSourceCode(),
// 'warehouse_stock' => $items['sourceItem']->getQuantity(),
// 'distance'=>$distance,
// 'isHub' => $items['isHub']
// ];
// }
// $this->logger->debug('distance on warehouse: ', $warehouseDistances);
return $this->getPreferredInventorySource($availableItems, $orderItem, $oneSource);
}
/**
* choose preferred warehouse for ordered product
*/
protected function getPreferredInventorySource(array $availableItems, OrderItemInterface $orderItem, bool $oneSource)
{
$hub = [];
$other = [];
$qty = $orderItem->getQtyOrdered();
foreach ($availableItems as $availableItem) {
if ($availableItem['isHub']) {
$hub[] = $availableItem;
} else {
$other[] = $availableItem;
}
}
//if one warehouse stock is enough for this ordered product, then return
if ($oneSource === true) {
$source = !empty($hub)? current($hub):current($other);
// $source['qty'] = $qty;
return [$source['sourceItem']->getSourceCode() => [$orderItem->getItemId() => $qty]];
}
$aggregation = [];
$total = 0;
foreach ($availableItems as $item) {
if ($total < $qty) {
$aggregation[$item['sourceItem']->getSourceCode()] = $item['sourceItem']->getQuantity();
}
$total += $item['sourceItem']->getQuantity();
}
return $this->correctLastWarehouseQty($aggregation, $orderItem);
}
/**
* @param array $availableItems
* @param OrderItemInterface $orderItem
* @return array
*/
protected function correctLastWarehouseQty(array $availableItems, OrderItemInterface $orderItem)
{
$lastKey = array_key_last($availableItems);
$qty = $orderItem->getQtyOrdered();
$lastQty = $qty;
$data = [];
foreach ($availableItems as $sourceCode => $qty) {
if ($sourceCode != $lastKey) {
$lastQty -= $qty;
} else {
$availableItems[$lastKey] = $lastQty;
}
$data[$sourceCode][$orderItem->getItemId()] = $availableItems[$sourceCode];
}
return $data;
}
/**
* transform order address to the calculate distance address
* @param \Magento\Sales\Api\Data\OrderAddressInterface $address
* @return \Magento\InventorySourceSelectionApi\Api\Data\AddressInterface
*/
protected function addressTransform(\Magento\Sales\Api\Data\OrderAddressInterface $address) {
if ($address->getStreet()) {
$streets = implode(' ', $address->getStreet());
}
return $this->addressInterfaceFactory->create([
'country' => $address->getCountryId() ?? '',
'postcode' => $address->getPostcode() ?? '',
'street' => $streets ?? '',
'region' => $address->getRegion() ?? '',
'city' => $address->getCity() ?? ''
]);
}
protected function getShippingAddress()
{
$shippingAddress = $this->order->getShippingAddress();
$streets = ['', ''];
if ($shippingAddress->getStreet()) {
$streets = $shippingAddress->getStreet();
}
return [
'country' => ['id' => 'US'],
'addr1' => $streets[0],
'addr2' => $streets[1] ?? '',
'city' => $shippingAddress->getCity(),
'state' => $shippingAddress->getRegionCode(),
'zip' => $shippingAddress->getPostcode(),
'addressee' => $shippingAddress->getFirstname() . ' ' . $shippingAddress->getLastname(),
'attention' => null,
'addrPhone' => $shippingAddress->getTelephone(),
'override' => false
];
}
/**
* @return string
* @throws NoSuchEntityException
*/
protected function getTerms()
{
$payment = $this->order->getPayment();
if ($payment->getMethod() == NetTerm::PAYMENT_METHOD_NETTERM_CODE) {
return $this->getQuote()->getNetsuitePaymentId();
}
return self::NETSUITE_CREDIT_CARD_PAYMENT_ID;
}
protected function getCustomer() {
if ($this->customer) {
return $this->customer;
}
if ($this->order->getCustomer()) {
$this->customer = $this->order->getCustomer()->getDataModel();
}
$this->customer = $this->customerRepository->getById($this->order->getCustomerId());
return $this->customer;
}
protected function getPostData(string $locationId, array $items, int $index)
{
$dataCustomer = $this->getCustomer();
$shippingAddress = $this->order->getShippingAddress();
$shippingCost = 0;
if ($index == 1) {
$shippingCost = $this->numberFormat($this->order->getShippingAmount());
}
foreach ($items as &$item) {
unset($item['sku']);
}
return [
'externalId'=> 'Magento-'.$this->order->getIncrementId().'-'.$index,
'entity' => [
'id'=>$dataCustomer->getCustomAttribute('netsuite_id')->getValue(),
],
'terms' => ['id' => $this->getTerms()],
'otherRefNum' => $this->getQuote()->getOtherRefNum(),
'custbody_il_email' => $shippingAddress->getEmail(),
'custbody_il_phone' => $shippingAddress->getTelephone(),
'custbody_card_info' => $this->getCardInfo(),
'custbody_ecommerce_order_no' => 'Magento-'.$this->order->getIncrementId().'-'.$index,
'subsidiary' => ['id'=> 3],
'cseg_ncg_rep_office' => ['id'=> 13],
'location' => ['id'=> $locationId],
'custbody_location_filter' => 'RMA',
// 'discountItem' => ['id'=> -6],
// 'discountRate' => 6.17,
'shipMethod' => ['id'=> ShippingMethodMapping::getShippingMethodId($this->order->getShippingMethod())],
'shippingCost' => $shippingCost,
'shipAddressList' => ['id'=>-2],
'shipIsResidential' => false,
'shippingAddress' => $this->getShippingAddress(),
'item' => ['items'=>$items],
];
}
/**
* @return CartInterface
* @throws NoSuchEntityException
*/
protected function getQuote()
{
if (!$this->quote) {
$this->quote = $this->cartRepository->get($this->order->getQuoteId());
}
return $this->quote;
}
/**
* @return \Magento\Sales\Api\Data\OrderSearchResultInterface
*/
protected function getWaitSyncOrders()
{
$criteria = $this->searchCriteriaBuilderFactory->create()
->addFilter('is_netsuite_sync', 0)
->addFilter('entity_id', [17173], 'in')
->create();
return $this->orderRepository->getList($criteria);
}
protected function customerDepositBuild()
{
$dataCustomer = $this->getCustomer();
return [
'externalId'=> 'CD-'.$this->order->getIncrementId().'-1',
'customer' => ['id' => $dataCustomer->getCustomAttribute('netsuite_id')->getValue()],
'salesOrder' => ['externalId' => 'Magento-'.$this->order->getIncrementId().'-1'],
'payment' => $this->numberFormat($this->order->getGrandTotal())
];
}
/**
* @param OrderInterface $order
* @return $this
*/
public function setOrder(OrderInterface $order)
{
$this->order = $order;
return $this;
}
/**
* @return array
*/
public function customerDepositSync()
{
$res = [];
try {
$paymentMethod = $this->order->getPayment()->getMethodInstance();
if (!$paymentMethod->isOffline()) {
$postData = $this->customerDepositBuild();
$response = $this->netSuiteAPI->postData(self::NETSUITE_CUSTOMER_DEPOSIT_SERVICE, $postData);
if ($response['error'] == true){
$message = sprintf('customer deposit %s sync to Netsuite fail', $this->order->getIncrementId());
$this->logger->debug($message, ['response' => $response, 'postData' => $postData]);
}
$res['response'] = $response;
} else {
$res['message'] = 'this order payment method is offline';
}
} catch (\Exception $exception) {
$res['message'] = $message = sprintf('customer deposit %s sync to Netsuite exception: ', $this->order->getIncrementId(), $exception->getMessage());
$this->logger->debug($message, ['trace' => $exception->getTrace()]);
}
return $res;
}
/**
* Execute the cron
*
* @return void
*/
public function execute(): void
{
$waitSyncOrders = $this->getWaitSyncOrders()->getItems();
foreach ($waitSyncOrders as $order) {
$this->order = $order;
/* @var $order \Magento\Sales\Model\Order */
try {
$syncOrders = $this->getSyncOrders();
echo '$syncOrders: '.json_encode($syncOrders, JSON_PRETTY_PRINT)."\n";
$index = 1;
$orderSyncSuccess = true;
foreach ($syncOrders as $sourceCode => $syncOrder) {
$locationId = $this->sources[$sourceCode]->getNetsuiteId();
unset($syncOrder['increment_id']);
$postData = $this->getPostData($locationId, array_values($syncOrder), $index);
$syncOrders[$sourceCode]['increment_id'] = $postData['externalId'];
$index++;
try {
$response = $this->netSuiteAPI->postData(self::NETSUITE_SALES_ORDER_SERVICE, $postData);
$this->logger->debug('order sync to netsuite ', ['response' => $response, 'postData' => $postData]);
if ($response['error'] != true){
$this->order->setIsNetsuiteSync(1);
} else {
$orderSyncSuccess = false;
$message = sprintf('order %s sync to Netsuite fail', $postData['externalId']);
$this->logger->debug($message, ['response' => $response, 'postData' => $postData]);
}
} catch (InvalidArgumentException $e) {
$message = sprintf('order %s sync to Netsuite crash fail: %s', $postData['externalId'], $e->getMessage());
$this->logger->error($message, ['postData' => $postData, 'exception' => $e->getTrace()]);
}
}
$additionalInformation = ['syncOrders' => $syncOrders];
if ($orderSyncSuccess) {
$response = $this->customerDepositSync();
$additionalInformation['depositSync'] = $response;
}
$this->order->setData('additional_information', json_encode($additionalInformation));
$this->orderRepository->save($this->order);
} catch (\Exception $exception) {
$this->magentoLogger->error('magento sync to netsuite exception: '.$exception->getMessage(), $exception->getTrace());
}
}
}
}