Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic product support #114

Merged
merged 5 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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') {
sabelard-bold marked this conversation as resolved.
Show resolved Hide resolved
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) {
BoldCole marked this conversation as resolved.
Show resolved Hide resolved
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();
sabelard-bold marked this conversation as resolved.
Show resolved Hide resolved
}

/**
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) {
sabelard-bold marked this conversation as resolved.
Show resolved Hide resolved
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();
BoldCole marked this conversation as resolved.
Show resolved Hide resolved
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