Skip to content

Commit

Permalink
Generic product support (#114)
Browse files Browse the repository at this point in the history
* generic product support
* Fix bundle product options.
* Add virtual cart support.
* Add virtual cart support.
* generic product support

---------

Co-authored-by: MykolaMalovanets <mykola.malovanets@gmail.com>
  • Loading branch information
BoldCole and MykolaMalovanets authored Aug 21, 2023
1 parent 769e89e commit 47441f3
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 36 deletions.
6 changes: 1 addition & 5 deletions Model/Order/InitOrderFromQuote.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use Bold\Checkout\Api\Http\ClientInterface;
use Bold\Checkout\Model\Quote\GetCartLineItems;
use Bold\Checkout\Model\Quote\QuoteAction;
use Exception;
use Magento\Customer\Api\Data\AddressInterface;
use Magento\Directory\Model\Country;
use Magento\Directory\Model\ResourceModel\Country\CollectionFactory;
Expand All @@ -21,7 +20,6 @@
class InitOrderFromQuote
{
private const INIT_URL = '/checkout/orders/{{shopId}}/init';

private const FLOW_ID = 'Bold-Magento2';

/**
Expand Down Expand Up @@ -101,7 +99,7 @@ public function init(CartInterface $quote, string $flowId = self::FLOW_ID): arra
],
'note_attributes' => [
'quote_id' => $quote->getId(),
]
],
],
];

Expand All @@ -120,13 +118,11 @@ public function init(CartInterface $quote, string $flowId = self::FLOW_ID): arra
'saved_addresses' => $customerAddresses,
];
}

$orderData = $this->client->post($websiteId, self::INIT_URL, $body)->getBody();
$publicOrderId = $orderData['data']['public_order_id'] ?? null;
if (!$publicOrderId) {
throw new LocalizedException(__('Cannot initialize order for quote with id = "%s"', $quote->getId()));
}

if ($quote->getCustomer()->getId() && !isset($orderData['data']['application_state']['customer']['public_id'])) {
throw new LocalizedException(__('Cannot authenticate customer with id="%s"', $quote->getCustomerId()));
}
Expand Down
10 changes: 7 additions & 3 deletions Model/Order/PlaceOrder/CreateOrderFromQuote.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,17 @@ public function create(CartInterface $cart, OrderDataInterface $orderPayload): O
'create_order_from_quote_submit_before',
['orderPayload' => $orderPayload, 'orderData' => $orderData]
);
$cart->getShippingAddress()->setCollectShippingRates(true);
$this->cart->setQuote($cart);
if (!$cart->isVirtual()) {
$cart->getShippingAddress()->setCollectShippingRates(true);
}
$cart->setTotalsCollectedFlag(false);
$cart->collectTotals();
$this->cart->setQuote($cart);
$order = $this->cartManagement->submit($cart, $orderData->getData());
$this->setOrderTaxDetails($order);
$this->setShippingAssignments($order);
if (!$cart->getIsVirtual()) {
$this->setShippingAssignments($order);
}
$this->eventManager->dispatch(
'checkout_type_onepage_save_order_after',
['order' => $order, 'quote' => $cart]
Expand Down
203 changes: 183 additions & 20 deletions Model/Quote/GetCartLineItems.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@

namespace Bold\Checkout\Model\Quote;

use Magento\Bundle\Model\Product\Type;
use Magento\Catalog\Helper\Product\Configuration;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Bundle\Model\Product\Type as Bundle;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Product\Image\UrlBuilder;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Escaper;
use Magento\Framework\Exception\LocalizedException;
use Magento\Quote\Api\Data\CartInterface;
use Magento\Quote\Api\Data\CartItemInterface;
use Magento\Quote\Model\Quote\Item;
use Magento\Store\Model\ScopeInterface;
use Magento\Catalog\Model\Product\Type as Virtual;
use Magento\Downloadable\Model\Product\Type as Downloadable;

/**
* Cart line items builder.
Expand All @@ -30,16 +39,51 @@ class GetCartLineItems
*/
private $escaper;

/**
* @var ScopeConfigInterface
*/
private $scopeConfig;

/**
* @var ProductRepositoryInterface
*/
private $productRepository;

/**
* @var UrlBuilder
*/
private $productUrlBuilder;

/**
* @var Bundle
*/
private $bundleType;

/**
* @param Configuration $configuration
* @param Configurable $configurableType
* @param Escaper $escaper
* @param ScopeConfigInterface $scopeConfig
* @param ProductRepositoryInterface $productRepository
* @param UrlBuilder $productUrlBuilder
* @param Bundle $bundleType
*/
public function __construct(Configuration $configuration, Configurable $configurableType, Escaper $escaper)
{
public function __construct(
Configuration $configuration,
Configurable $configurableType,
Escaper $escaper,
ScopeConfigInterface $scopeConfig,
ProductRepositoryInterface $productRepository,
UrlBuilder $productUrlBuilder,
Type $bundleType
) {
$this->configuration = $configuration;
$this->configurableType = $configurableType;
$this->escaper = $escaper;
$this->scopeConfig = $scopeConfig;
$this->productRepository = $productRepository;
$this->productUrlBuilder = $productUrlBuilder;
$this->bundleType = $bundleType;
}

/**
Expand All @@ -52,18 +96,32 @@ public function __construct(Configuration $configuration, Configurable $configur
public function getItems(CartInterface $quote): array
{
$lineItems = [];
/** @var CartItemInterface $cartItem */
foreach ($quote->getAllItems() as $cartItem) {
if (!$cartItem->getChildren()) {
if (static::shouldAppearInCart($cartItem)) {
$lineItems[] = $this->getLineItem($cartItem);
}
}

if (!$lineItems) {
throw new LocalizedException(__('There are no cart items to checkout.'));
}

return $lineItems;
}

/**
* Determines if the cart item should appear in the cart sent to Bold
*
* @param Item $item
* @return boolean
*/
public static function shouldAppearInCart(CartItemInterface $item): bool
{
$parentItem = $item->getParentItem();
$parentIsBundle = $parentItem && $parentItem->getProductType() === Bundle::TYPE_CODE;
return (!$item->getChildren() && !$parentIsBundle) || $item->getProductType() === Bundle::TYPE_CODE;
}

/**
* Extract quote item entity data into array.
*
Expand All @@ -73,54 +131,112 @@ public function getItems(CartInterface $quote): array
private function getLineItem(CartItemInterface $item): array
{
$lineItem = [
'platform_id' => (string)$item->getProduct()->getId(),
'id' => (int)$item->getProduct()->getId(),
'quantity' => $this->extractLineItemQuantity($item),
'title' => $this->getLineItemName($item),
'product_title' => $this->getLineItemName($item),
'weight' => $this->getLineItemWeightInGrams($item),
'taxable' => true, // Doesn't matter since RSA will handle taxes
'image' => $this->getLineItemImage($item),
'requires_shipping' => $this->getRequiresShipping($item),
'line_item_key' => (string)$item->getId(),
'price_adjustment' => $this->getPriceAdjustment($item),
'price' => $this->getLineItemPrice($item),
];

$item = $item->getParentItem() ?: $item;
if ($item->getProductType() === Configurable::TYPE_CODE) {
$lineItem = $this->addConfigurableOptions($item, $lineItem);
}
if ($item->getProductType() === Bundle::TYPE_CODE) {
$lineItem = $this->addBundleOptions($item, $lineItem);
}
foreach ($this->configuration->getCustomOptions($item) as $customOption) {
$lineItem = $this->addCustomOptions($customOption, $lineItem);
}
return $lineItem;
}

/**
* Get quote item quantity considering product type.
* Gets the product's name from the line item
*
* @param CartItemInterface $item
* @return string
*/
private function getLineItemName(CartItemInterface $item): string
{
$item = $item->getParentItem() ?: $item;
return $item->getName();
}

/**
* Gets the price of a line item
*
* @param CartItemInterface $item
* @return int
*/
private function extractLineItemQuantity(CartItemInterface $item): int
private function getLineItemPrice(CartItemInterface $item)
{
$parentItem = $item->getParentItem();
if ($parentItem) {
$item = $parentItem;
$item = $item->getParentItem() ?: $item;
return $this->convertToCents((float)$item->getPrice());
}

/**
* Gets the weight of a line item in grams
*
* @param CartItemInterface $item
* @return float
*/
private function getLineItemWeightInGrams(CartItemInterface $item): float
{
$unit = strtolower(
$this->scopeConfig->getValue('general/locale/weight_unit', ScopeInterface::SCOPE_STORE)
);
$weight = $item->getWeight();
if ($unit === 'kgs') {
return round($weight * 1000, 2);
} elseif ($unit === 'lbs') {
return round($weight * 453.59237, 2);
}

return (int)$item->getQty();
return $weight;
}

/**
* Get quote item discount amount.
* Gets the line item's image. Falls back to the parent item (If available) if the direct
* item does not have an image
*
* @param CartItemInterface $item
* @return float
* @return string
*/
private function getLineItemImage(CartItemInterface $item): string
{
$product = $this->productRepository->getById($item->getProductId());
if ($product->getThumbnail() && $product->getThumbnail() !== 'no_selection') {
return $this->productUrlBuilder->getUrl($product->getThumbnail(), 'product_thumbnail_image');
}
// Attempting to get the parent product if there is one
if ($item->getParentItem()) {
$image = $this->productRepository->getById($item->getParentItem()->getProductId())->getThumbnail();
if ($image) {
return $this->productUrlBuilder->getUrl($image, 'product_thumbnail_image');
}
}
return $this->productUrlBuilder->getUrl('no_selection', 'product_thumbnail_image');
}

/**
* Get quote item quantity considering product type.
*
* @param CartItemInterface $item
* @return int
*/
private function getPriceAdjustment(CartItemInterface $item)
private function extractLineItemQuantity(CartItemInterface $item): int
{
$parentItem = $item->getParentItem();
$childProduct = $item->getProduct();
$baseProductPrice = $childProduct->getPrice();
if ($parentItem) {
$item = $parentItem;
}
$priceAdjustment = $item->getBasePrice() - $baseProductPrice;

return $priceAdjustment * 100;
return (int)$item->getQty();
}

/**
Expand Down Expand Up @@ -159,4 +275,51 @@ private function addCustomOptions(array $customOption, array $lineItem): array
$lineItem['line_item_properties'][$label] = $value['value'] ?? '';
return $lineItem;
}

/**
* Takes in a bundle product line item and adds the items in the bundle to the line
* item as line item properties
*
* @param CartItemInterface $item
* @param array $lineItem
* @return array
*/
private function addBundleOptions(CartItemInterface $item, array $lineItem): array
{
$options = $this->bundleType->getOptionsCollection($item->getProduct());
$children = $item->getChildren();
$lineItem['line_item_properties'] = [];
foreach (array_values($options->getItems()) as $i => $option) {
$childItem = $children[$i] ?? null;
if (!$childItem) {
continue;
}
$qty = (int)$childItem->getQty();
$name = $childItem->getName();
$lineItem['line_item_properties'][$option['title']] = "$qty x $name";
}
return $lineItem;
}

/**
* Get requires shipping considering product type
*
* @param CartItemInterface $item
* @return bool
*/
private function getRequiresShipping(CartItemInterface $item): bool
{
$type = $item->getProductType();
return $type !== Virtual::TYPE_VIRTUAL && $type !== Downloadable::TYPE_DOWNLOADABLE;
}

/**
* Converts a dollar amount to cents
*
* @param string|float $dollars
* @return integer
*/
private function convertToCents($dollars): int {
return (int) round(floatval($dollars) * 100);
}
}
14 changes: 13 additions & 1 deletion Model/Quote/GetQuoteInventoryData.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Bold\Checkout\Api\Data\Quote\Inventory\Result\InventoryDataInterfaceFactory;
use Bold\Checkout\Api\Data\Quote\Inventory\ResultInterface;
use Bold\Checkout\Api\Data\Quote\Inventory\ResultInterfaceFactory;
use Magento\Bundle\Model\Product\Type as Bundle;
use Bold\Checkout\Api\Quote\GetQuoteInventoryDataInterface;
use Bold\Checkout\Model\Http\Client\Request\Validator\ShopIdValidator;
use Exception;
Expand Down Expand Up @@ -97,7 +98,7 @@ public function getInventory(string $shopId, int $cartId): ResultInterface
}
$inventoryResult = [];
foreach ($quote->getAllItems() as $item) {
if ($item->getChildren()) {
if (!GetCartLineItems::shouldAppearInCart($item)) {
continue;
}
$inventoryResult[] = $this->inventoryDataFactory->create(
Expand Down Expand Up @@ -145,6 +146,17 @@ private function buildErrorResponse(string $error): ResultInterface
*/
private function isProductSalable(CartItemInterface $item): bool
{
// If the product is a bundle type, get the salabilty of all it's children instead
if ($item->getProductType() === Bundle::TYPE_CODE) {
foreach ($item->getChildren() as $childItem) {
if (!$this->isProductSalable($childItem)) {
return false;
}
}

return true;
}

try {
$stockResolver = $this->getStockResolverService();
$productSalableForRequestedQtyService = $this->getProductSalableForRequestedQtyService();
Expand Down
3 changes: 0 additions & 3 deletions Model/Quote/IsBoldCheckoutAllowedForCart.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,6 @@ public function isAllowed(CartInterface $quote): bool
if ($item->getIsQtyDecimal()) {
return false;
}
if ($item->getProductType() === Type::TYPE_BUNDLE) {
return false;
}
}
return $this->scopeConfig->isSetFlag(Config::CONFIG_XML_PATH_APPLY_AFTER_DISCOUNT);
}
Expand Down
Loading

0 comments on commit 47441f3

Please sign in to comment.