diff --git a/src/Service/CartLine/CartItemDiscountService.php b/src/Service/CartLine/CartItemDiscountService.php new file mode 100644 index 000000000..fd77d8b62 --- /dev/null +++ b/src/Service/CartLine/CartItemDiscountService.php @@ -0,0 +1,59 @@ + + * @copyright Mollie B.V. + * @license https://github.com/mollie/PrestaShop/blob/master/LICENSE.md + * + * @see https://github.com/mollie/PrestaShop + * @codingStandardsIgnoreStart + */ + +namespace Mollie\Service\CartLine; + +use Mollie\Config\Config; +use mollie\src\Utility\RoundingUtility; +use Mollie\Utility\NumberUtility; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class CartItemDiscountService +{ + /* @var RoundingUtility $roundingUtility */ + private $roundingUtility; + + public function __construct(RoundingUtility $roundingUtility) + { + $this->roundingUtility = $roundingUtility; + } + + /** + * @param float $totalDiscounts + * @param array $orderLines + * @param float $remaining + * + * @return array + */ + public function addDiscountsToProductLines(float $totalDiscounts, array $orderLines, float $remaining): array + { + if ($totalDiscounts >= 0.01) { + $orderLines['discount'] = [ + [ + 'name' => 'Discount', + 'type' => 'discount', + 'quantity' => 1, + 'unitPrice' => -$this->roundingUtility->round($totalDiscounts, Config::API_ROUNDING_PRECISION), + 'totalAmount' => -$this->roundingUtility->round($totalDiscounts, Config::API_ROUNDING_PRECISION), + 'targetVat' => 0, + 'category' => '', + ], + ]; + $remaining = NumberUtility::plus($remaining, $totalDiscounts); + } + + return [$orderLines, $remaining]; + } +} diff --git a/src/Service/CartLine/CartItemPaymentFeeService.php b/src/Service/CartLine/CartItemPaymentFeeService.php new file mode 100644 index 000000000..3b34216e2 --- /dev/null +++ b/src/Service/CartLine/CartItemPaymentFeeService.php @@ -0,0 +1,65 @@ + + * @copyright Mollie B.V. + * @license https://github.com/mollie/PrestaShop/blob/master/LICENSE.md + * + * @see https://github.com/mollie/PrestaShop + * @codingStandardsIgnoreStart + */ + +namespace mollie\src\Service\CartLine; + +use Mollie\Config\Config; +use Mollie\DTO\PaymentFeeData; +use Mollie\Service\LanguageService; +use mollie\src\Utility\RoundingUtility; +use Mollie\Utility\NumberUtility; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class CartItemPaymentFeeService +{ + /* @var LanguageService */ + private $languageService; + + /* @var RoundingUtility */ + private $roundingUtility; + + public function __construct(LanguageService $languageService, RoundingUtility $roundingUtility) + { + $this->languageService = $languageService; + $this->roundingUtility = $roundingUtility; + } + + /** + * @param PaymentFeeData $paymentFeeData + * @param array $orderLines + * + * @return array + */ + public function addPaymentFeeLine(PaymentFeeData $paymentFeeData, array $orderLines): array + { + if (!$paymentFeeData->isActive()) { + return $orderLines; + } + + $orderLines['surcharge'] = [ + [ + 'name' => $this->languageService->lang('Payment fee'), + 'sku' => Config::PAYMENT_FEE_SKU, + 'quantity' => 1, + 'unitPrice' => $this->roundingUtility->round($paymentFeeData->getPaymentFeeTaxIncl(), CONFIG::API_ROUNDING_PRECISION), + 'totalAmount' => $this->roundingUtility->round($paymentFeeData->getPaymentFeeTaxIncl(), CONFIG::API_ROUNDING_PRECISION), + 'vatAmount' => NumberUtility::minus($paymentFeeData->getPaymentFeeTaxIncl(), $paymentFeeData->getPaymentFeeTaxExcl()), + 'vatRate' => $paymentFeeData->getTaxRate(), + ], + ]; + + return $orderLines; + } +} diff --git a/src/Service/CartLine/CartItemProductLinesService.php b/src/Service/CartLine/CartItemProductLinesService.php new file mode 100644 index 000000000..f52e311ba --- /dev/null +++ b/src/Service/CartLine/CartItemProductLinesService.php @@ -0,0 +1,93 @@ + + * @copyright Mollie B.V. + * @license https://github.com/mollie/PrestaShop/blob/master/LICENSE.md + * + * @see https://github.com/mollie/PrestaShop + * @codingStandardsIgnoreStart + */ + +namespace Mollie\Service\CartLine; + +use Mollie\Config\Config; +use mollie\src\Utility\RoundingUtility; +use Mollie\Utility\CalculationUtility; +use Mollie\Utility\NumberUtility; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class CartItemProductLinesService +{ + /* @var RoundingUtility $roundingUtility */ + private $roundingUtility; + + public function __construct(RoundingUtility $roundingUtility) + { + $this->roundingUtility = $roundingUtility; + } + + /** + * @param int $vatRatePrecision + * + * @return array + * + * @throws \PrestaShop\Decimal\Exception\DivisionByZeroException + */ + public function fillProductLinesWithRemainingData(array $orderLines, int $vatRatePrecision): array + { + $roundingPrecision = CONFIG::API_ROUNDING_PRECISION; + + foreach ($orderLines as $productHash => $aItem) { + $orderLines[$productHash] = array_map(function ($line) use ($roundingPrecision, $vatRatePrecision) { + $quantity = (int) $line['quantity']; + $targetVat = $line['targetVat']; + $unitPrice = $line['unitPrice']; + $unitPriceNoTax = $this->roundingUtility->round(CalculationUtility::getUnitPriceNoTax( + $line['unitPrice'], + $targetVat + ), + $roundingPrecision + ); + + // Calculate VAT + $totalAmount = $line['totalAmount']; + $actualVatRate = 0; + if ($unitPriceNoTax > 0) { + $actualVatRate = $this->roundingUtility->round( + $vatAmount = CalculationUtility::getActualVatRate($unitPrice, $unitPriceNoTax, $quantity), + $vatRatePrecision + ); + } + $vatRateWithPercentages = NumberUtility::plus($actualVatRate, 100); + $vatAmount = NumberUtility::times( + $totalAmount, + NumberUtility::divide($actualVatRate, $vatRateWithPercentages) + ); + + $newItem = [ + 'name' => $line['name'], + 'category' => $line['category'], + 'quantity' => (int) $quantity, + 'unitPrice' => $this->roundingUtility->round($unitPrice, $roundingPrecision), + 'totalAmount' => $this->roundingUtility->round($totalAmount, $roundingPrecision), + 'vatRate' => $this->roundingUtility->round($actualVatRate, $roundingPrecision), + 'vatAmount' => $this->roundingUtility->round($vatAmount, $roundingPrecision), + 'product_url' => $line['product_url'] ?? null, + 'image_url' => $line['image_url'] ?? null, + ]; + if (isset($line['sku'])) { + $newItem['sku'] = $line['sku']; + } + + return $newItem; + }, $aItem); + } + + return $orderLines; + } +} diff --git a/src/Service/CartLine/CartItemShippingLineService.php b/src/Service/CartLine/CartItemShippingLineService.php new file mode 100644 index 000000000..ed92d78ac --- /dev/null +++ b/src/Service/CartLine/CartItemShippingLineService.php @@ -0,0 +1,63 @@ + + * @copyright Mollie B.V. + * @license https://github.com/mollie/PrestaShop/blob/master/LICENSE.md + * + * @see https://github.com/mollie/PrestaShop + * @codingStandardsIgnoreStart + */ + +namespace Mollie\Service\CartLine; + +use Mollie\Config\Config; +use Mollie\Service\LanguageService; +use mollie\src\Utility\RoundingUtility; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class CartItemShippingLineService +{ + /* @var LanguageService */ + private $languageService; + + /* @var RoundingUtility */ + private $roundingUtility; + + public function __construct(LanguageService $languageService, RoundingUtility $roundingUtility) + { + $this->languageService = $languageService; + $this->roundingUtility = $roundingUtility; + } + + /** + * @param float $roundedShippingCost + * @param array $cartSummary + * @param array $orderLines + * + * @return array + */ + public function addShippingLine(float $roundedShippingCost, array $cartSummary, array $orderLines): array + { + if ($this->roundingUtility->round($roundedShippingCost, 2) > 0) { + $shippingVatRate = $this->roundingUtility->round(($cartSummary['total_shipping'] - $cartSummary['total_shipping_tax_exc']) / $cartSummary['total_shipping_tax_exc'] * 100, Config::API_ROUNDING_PRECISION); + + $orderLines['shipping'] = [ + [ + 'name' => $this->languageService->lang('Shipping'), + 'quantity' => 1, + 'unitPrice' => $this->roundingUtility->round($roundedShippingCost, Config::API_ROUNDING_PRECISION), + 'totalAmount' => $this->roundingUtility->round($roundedShippingCost, Config::API_ROUNDING_PRECISION), + 'vatAmount' => $this->roundingUtility->round($roundedShippingCost * $shippingVatRate / ($shippingVatRate + 100), Config::API_ROUNDING_PRECISION), + 'vatRate' => $shippingVatRate, + ], + ]; + } + + return $orderLines; + } +} diff --git a/src/Service/CartLine/CartItemWrappingService.php b/src/Service/CartLine/CartItemWrappingService.php new file mode 100644 index 000000000..604157001 --- /dev/null +++ b/src/Service/CartLine/CartItemWrappingService.php @@ -0,0 +1,73 @@ + + * @copyright Mollie B.V. + * @license https://github.com/mollie/PrestaShop/blob/master/LICENSE.md + * + * @see https://github.com/mollie/PrestaShop + * @codingStandardsIgnoreStart + */ + +namespace Mollie\Service\CartLine; + +use Mollie\Config\Config; +use Mollie\Service\LanguageService; +use mollie\src\Utility\RoundingUtility; +use Mollie\Utility\CalculationUtility; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class CartItemWrappingService +{ + /** + * @var LanguageService + */ + private $languageService; + + /* @var RoundingUtility */ + private $roundingUtility; + + public function __construct(LanguageService $languageService, RoundingUtility $roundingUtility) + { + $this->languageService = $languageService; + $this->roundingUtility = $roundingUtility; + } + + /** + * @param float $wrappingPrice + * @param array $cartSummary + * @param int $vatRatePrecision + * @param array $orderLines + * + * @return array + */ + public function addWrappingLine(float $wrappingPrice, array $cartSummary, int $vatRatePrecision, array $orderLines): array + { + if (round($wrappingPrice, 2) > 0) { + $wrappingVatRate = $this->roundingUtility->round( + CalculationUtility::getActualVatRate( + $cartSummary['total_wrapping'], + $cartSummary['total_wrapping_tax_exc'] + ), + $vatRatePrecision + ); + + $orderLines['wrapping'] = [ + [ + 'name' => $this->languageService->lang('Gift wrapping'), + 'quantity' => 1, + 'unitPrice' => $this->roundingUtility->round($wrappingPrice, Config::API_ROUNDING_PRECISION), + 'totalAmount' => $this->roundingUtility->round($wrappingPrice, Config::API_ROUNDING_PRECISION), + 'vatAmount' => $this->roundingUtility->round($wrappingPrice * $wrappingVatRate / ($wrappingVatRate + 100), Config::API_ROUNDING_PRECISION), + 'vatRate' => $wrappingVatRate, + ], + ]; + } + + return $orderLines; + } +} diff --git a/src/Service/CartLine/CartItemsService.php b/src/Service/CartLine/CartItemsService.php new file mode 100644 index 000000000..f65a623f7 --- /dev/null +++ b/src/Service/CartLine/CartItemsService.php @@ -0,0 +1,145 @@ + + * @copyright Mollie B.V. + * @license https://github.com/mollie/PrestaShop/blob/master/LICENSE.md + * + * @see https://github.com/mollie/PrestaShop + * @codingStandardsIgnoreStart + */ + +namespace Mollie\Service\CartLine; + +use Mollie\Adapter\Context; +use Mollie\Config\Config; +use Mollie\Service\VoucherService; +use Mollie\Utility\CartPriceUtility; +use Mollie\Utility\NumberUtility; +use Mollie\Utility\TextFormatUtility; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class CartItemsService +{ + /** + * @var Context + */ + private $context; + /** + * @var VoucherService + */ + private $voucherService; + + public function __construct(Context $context, VoucherService $voucherService) + { + $this->context = $context; + $this->voucherService = $voucherService; + } + + /** + * @param array $giftProducts + * @param string $selectedVoucherCategory + * @param float $remaining + * + * @return array + */ + public function createProductLines(array $cartItems, array $giftProducts, array $orderLines, string $selectedVoucherCategory, float $remaining): array + { + foreach ($cartItems as $cartItem) { + // Get the rounded total w/ tax + $roundedTotalWithTax = round($cartItem['total_wt'], Config::API_ROUNDING_PRECISION); + + // Skip if no qty + $quantity = (int) $cartItem['cart_quantity']; + if ($quantity <= 0 || $cartItem['price_wt'] <= 0) { + continue; + } + + // Generate the product hash + $idProduct = TextFormatUtility::formatNumber($cartItem['id_product'], 0); + $idProductAttribute = TextFormatUtility::formatNumber($cartItem['id_product_attribute'], 0); + $idCustomization = TextFormatUtility::formatNumber($cartItem['id_customization'], 0); + + $productHash = "{$idProduct}¤{$idProductAttribute}¤{$idCustomization}"; + + foreach ($giftProducts as $gift_product) { + if ($gift_product['id_product'] === $cartItem['id_product']) { + $quantity = NumberUtility::minus($quantity, $gift_product['cart_quantity']); + + $productHashGift = "{$idProduct}¤{$idProductAttribute}¤{$idCustomization}gift"; + $orderLines[$productHashGift][] = [ + 'name' => $cartItem['name'], + 'sku' => $productHashGift, + 'targetVat' => (float) $cartItem['rate'], + 'quantity' => $gift_product['cart_quantity'], + 'unitPrice' => 0, + 'totalAmount' => 0, + 'category' => '', + 'product_url' => $this->context->getProductLink($cartItem['id_product']), + 'image_url' => $this->context->getImageLink($cartItem['link_rewrite'], $cartItem['id_image']), + ]; + continue; + } + } + + if ((int) $quantity <= 0) { + continue; + } + + // Try to spread this product evenly and account for rounding differences on the order line + $orderLines[$productHash][] = [ + 'name' => $cartItem['name'], + 'sku' => $productHash, + 'targetVat' => (float) $cartItem['rate'], + 'quantity' => $quantity, + 'unitPrice' => round($cartItem['price_wt'], Config::API_ROUNDING_PRECISION), + 'totalAmount' => (float) $roundedTotalWithTax, + 'category' => $this->voucherService->getVoucherCategory($cartItem, $selectedVoucherCategory), + 'product_url' => $this->context->getProductLink($cartItem['id_product']), + 'image_url' => $this->context->getImageLink($cartItem['link_rewrite'], $cartItem['id_image']), + ]; + $remaining -= $roundedTotalWithTax; + } + + return [$orderLines, $remaining]; + } + + /** + * Spread the cart line amount evenly. + * + * Optionally split into multiple lines in case of rounding inaccuracies + * + * @param array[] $cartLineGroup Cart Line Group WITHOUT VAT details (except target VAT rate) + * @param float $newTotal + * + * @return array[] + * + * @since 3.2.2 + * @since 3.3.3 Omits VAT details + */ + public static function spreadCartLineGroup($cartLineGroup, $newTotal) + { + $newTotal = round($newTotal, Config::API_ROUNDING_PRECISION); + $quantity = array_sum(array_column($cartLineGroup, 'quantity')); + $newCartLineGroup = []; + $spread = CartPriceUtility::spreadAmountEvenly($newTotal, $quantity); + + foreach ($spread as $unitPrice => $qty) { + $newCartLineGroup[] = [ + 'name' => $cartLineGroup[0]['name'], + 'quantity' => $qty, + 'unitPrice' => (float) $unitPrice, + 'totalAmount' => (float) $unitPrice * $qty, + 'sku' => $cartLineGroup[0]['sku'] ?? '', + 'targetVat' => $cartLineGroup[0]['targetVat'], + 'category' => $cartLineGroup[0]['category'], + ]; + } + + return $newCartLineGroup; + } +} diff --git a/src/Service/CartLinesService.php b/src/Service/CartLinesService.php index 0312ed108..7054fc578 100644 --- a/src/Service/CartLinesService.php +++ b/src/Service/CartLinesService.php @@ -12,17 +12,18 @@ namespace Mollie\Service; -use Cart; -use Mollie\Adapter\Context; -use Mollie\Adapter\ToolsAdapter; use Mollie\Config\Config; -use Mollie\DTO\Line; -use Mollie\DTO\Object\Amount; use Mollie\DTO\PaymentFeeData; +use Mollie\Service\CartLine\CartItemDiscountService; +use Mollie\Service\CartLine\CartItemProductLinesService; +use Mollie\Service\CartLine\CartItemShippingLineService; +use Mollie\Service\CartLine\CartItemsService; +use Mollie\Service\CartLine\CartItemWrappingService; +use mollie\src\Service\CartLine\CartItemPaymentFeeService; +use mollie\src\Utility\LineUtility; +use mollie\src\Utility\RoundingUtility; +use Mollie\Utility\ArrayUtility; use Mollie\Utility\CalculationUtility; -use Mollie\Utility\CartPriceUtility; -use Mollie\Utility\NumberUtility; -use Mollie\Utility\TextFormatUtility; if (!defined('_PS_VERSION_')) { exit; @@ -30,28 +31,53 @@ class CartLinesService { - /** - * @var VoucherService - */ - private $voucherService; + /* @var CartItemsService */ + private $cartItemsService; - /** - * @var LanguageService - */ - private $languageService; + /* @var CartItemDiscountService */ + private $cartItemDiscountService; - /** - * @var ToolsAdapter - */ - private $tools; - private $context; + /* @var CartItemShippingLineService */ + private $cartItemShippingLineService; + + /* @var CartItemWrappingService */ + private $cartItemWrappingService; + + /* @var CartItemProductLinesService */ + private $cartItemProductLinesService; + + /* @var CartItemPaymentFeeService */ + private $cartItemPaymentFeeService; - public function __construct(LanguageService $languageService, VoucherService $voucherService, ToolsAdapter $tools, Context $context) - { - $this->voucherService = $voucherService; - $this->languageService = $languageService; - $this->tools = $tools; - $this->context = $context; + /* @var LineUtility */ + private $lineUtility; + + /* @var RoundingUtility */ + private $roundingUtility; + + /* @var ArrayUtility */ + private $arrayUtility; + + public function __construct( + CartItemsService $cartItemsService, + CartItemDiscountService $cartItemDiscountService, + CartItemShippingLineService $cartItemShippingLineService, + CartItemWrappingService $cartItemWrappingService, + CartItemProductLinesService $cartItemProductLinesService, + CartItemPaymentFeeService $cartItemPaymentFeeService, + LineUtility $lineUtility, + RoundingUtility $roundingUtility, + ArrayUtility $arrayUtility + ) { + $this->cartItemsService = $cartItemsService; + $this->cartItemDiscountService = $cartItemDiscountService; + $this->cartItemShippingLineService = $cartItemShippingLineService; + $this->cartItemWrappingService = $cartItemWrappingService; + $this->cartItemProductLinesService = $cartItemProductLinesService; + $this->cartItemPaymentFeeService = $cartItemPaymentFeeService; + $this->lineUtility = $lineUtility; + $this->roundingUtility = $roundingUtility; + $this->arrayUtility = $arrayUtility; } /** @@ -69,448 +95,57 @@ public function __construct(LanguageService $languageService, VoucherService $vo * @throws \PrestaShop\Decimal\Exception\DivisionByZeroException */ public function getCartLines( - $amount, - $paymentFeeData, - $currencyIsoCode, - $cartSummary, - $shippingCost, - $cartItems, - $psGiftWrapping, - $selectedVoucherCategory - ) { - // TODO refactor whole service, split order line append into separate services and test them individually at least!!! - - $apiRoundingPrecision = Config::API_ROUNDING_PRECISION; - $vatRatePrecision = Config::VAT_RATE_ROUNDING_PRECISION; + float $amount, + PaymentFeeData $paymentFeeData, + string $currencyIsoCode, + array $cartSummary, + float $shippingCost, + array $cartItems, + bool $psGiftWrapping, + string $selectedVoucherCategory + ): array { + $totalPrice = $this->roundingUtility->round($amount, Config::API_ROUNDING_PRECISION); + $roundedShippingCost = $this->roundingUtility->round($shippingCost, Config::API_ROUNDING_PRECISION); - $totalPrice = round($amount, $apiRoundingPrecision); - $roundedShippingCost = round($shippingCost, $apiRoundingPrecision); foreach ($cartSummary['discounts'] as $discount) { if ($discount['free_shipping']) { $roundedShippingCost = 0; } } - $wrappingPrice = $psGiftWrapping ? round($cartSummary['total_wrapping'], $apiRoundingPrecision) : 0; - $totalDiscounts = isset($cartSummary['total_discounts']) ? $cartSummary['total_discounts'] : 0; - $remaining = round( + $wrappingPrice = $psGiftWrapping ? $this->roundingUtility->round($cartSummary['total_wrapping'], Config::API_ROUNDING_PRECISION) : 0; + + $remainingAmount = $this->roundingUtility->round( CalculationUtility::getCartRemainingPrice((float) $totalPrice, (float) $roundedShippingCost, (float) $wrappingPrice), - $apiRoundingPrecision + Config::API_ROUNDING_PRECISION ); $orderLines = []; - /* Item */ - list($orderLines, $remaining) = $this->createProductLines($cartItems, $apiRoundingPrecision, $cartSummary['gift_products'], $orderLines, $selectedVoucherCategory, $remaining); - // Add discount if applicable - list($orderLines, $remaining) = $this->addDiscountsToProductLines($totalDiscounts, $apiRoundingPrecision, $orderLines, $remaining); + // Item + list($orderLines, $remainingAmount) = $this->cartItemsService->createProductLines($cartItems, $cartSummary['gift_products'], $orderLines, $selectedVoucherCategory, $remainingAmount); + + // Add discounts to the order lines + $totalDiscounts = $cartSummary['total_discounts'] ?? 0; + list($orderLines, $remainingAmount) = $this->cartItemDiscountService->addDiscountsToProductLines($totalDiscounts, $orderLines, $remainingAmount); // Compensate for order total rounding inaccuracies - $orderLines = $this->compositeRoundingInaccuracies($remaining, $apiRoundingPrecision, $orderLines); + $orderLines = $this->roundingUtility->compositeRoundingInaccuracies($remainingAmount, $orderLines); // Fill the order lines with the rest of the data (tax, total amount, etc.) - $orderLines = $this->fillProductLinesWithRemainingData($orderLines, $apiRoundingPrecision, $vatRatePrecision); - - // Add shipping - $orderLines = $this->addShippingLine($roundedShippingCost, $cartSummary, $apiRoundingPrecision, $orderLines); - - // Add wrapping - $orderLines = $this->addWrappingLine($wrappingPrice, $cartSummary, $vatRatePrecision, $apiRoundingPrecision, $orderLines); - - // Add fee - $orderLines = $this->addPaymentFeeLine($paymentFeeData, $apiRoundingPrecision, $orderLines); - - // Ungroup all the cart lines, just one level - $newItems = $this->ungroupLines($orderLines); - - // Convert floats to strings for the Mollie API and add additional info - return $this->convertToLineArray($newItems, $currencyIsoCode, $apiRoundingPrecision); - } - - /** - * Spread the cart line amount evenly. - * - * Optionally split into multiple lines in case of rounding inaccuracies - * - * @param array[] $cartLineGroup Cart Line Group WITHOUT VAT details (except target VAT rate) - * @param float $newTotal - * - * @return array[] - * - * @since 3.2.2 - * @since 3.3.3 Omits VAT details - */ - public static function spreadCartLineGroup($cartLineGroup, $newTotal) - { - $apiRoundingPrecision = Config::API_ROUNDING_PRECISION; - $newTotal = round($newTotal, $apiRoundingPrecision); - $quantity = array_sum(array_column($cartLineGroup, 'quantity')); - $newCartLineGroup = []; - $spread = CartPriceUtility::spreadAmountEvenly($newTotal, $quantity); - foreach ($spread as $unitPrice => $qty) { - $newCartLineGroup[] = [ - 'name' => $cartLineGroup[0]['name'], - 'quantity' => $qty, - 'unitPrice' => (float) $unitPrice, - 'totalAmount' => (float) $unitPrice * $qty, - 'sku' => isset($cartLineGroup[0]['sku']) ? $cartLineGroup[0]['sku'] : '', - 'targetVat' => $cartLineGroup[0]['targetVat'], - 'category' => $cartLineGroup[0]['category'], - ]; - } - - return $newCartLineGroup; - } - - /** - * @param int $apiRoundingPrecision - * @param array $giftProducts - * @param string $selectedVoucherCategory - * @param float $remaining - * - * @return array - */ - private function createProductLines(array $cartItems, $apiRoundingPrecision, $giftProducts, array $orderLines, $selectedVoucherCategory, $remaining) - { - foreach ($cartItems as $cartItem) { - // Get the rounded total w/ tax - $roundedTotalWithTax = round($cartItem['total_wt'], $apiRoundingPrecision); - - // Skip if no qty - $quantity = (int) $cartItem['cart_quantity']; - if ($quantity <= 0 || $cartItem['price_wt'] <= 0) { - continue; - } - - // Generate the product hash - $idProduct = TextFormatUtility::formatNumber($cartItem['id_product'], 0); - $idProductAttribute = TextFormatUtility::formatNumber($cartItem['id_product_attribute'], 0); - $idCustomization = TextFormatUtility::formatNumber($cartItem['id_customization'], 0); - - $productHash = "{$idProduct}¤{$idProductAttribute}¤{$idCustomization}"; - - foreach ($giftProducts as $gift_product) { - if ($gift_product['id_product'] === $cartItem['id_product']) { - $quantity = NumberUtility::minus($quantity, $gift_product['cart_quantity']); - - $productHashGift = "{$idProduct}¤{$idProductAttribute}¤{$idCustomization}gift"; - $orderLines[$productHashGift][] = [ - 'name' => $cartItem['name'], - 'sku' => $productHashGift, - 'targetVat' => (float) $cartItem['rate'], - 'quantity' => $gift_product['cart_quantity'], - 'unitPrice' => 0, - 'totalAmount' => 0, - 'category' => '', - 'product_url' => $this->context->getProductLink($cartItem['id_product']), - 'image_url' => $this->context->getImageLink($cartItem['link_rewrite'], $cartItem['id_image']), - ]; - continue; - } - } - - if ((int) $quantity <= 0) { - continue; - } - - // Try to spread this product evenly and account for rounding differences on the order line - $orderLines[$productHash][] = [ - 'name' => $cartItem['name'], - 'sku' => $productHash, - 'targetVat' => (float) $cartItem['rate'], - 'quantity' => $quantity, - 'unitPrice' => round($cartItem['price_wt'], $apiRoundingPrecision), - 'totalAmount' => (float) $roundedTotalWithTax, - 'category' => $this->voucherService->getVoucherCategory($cartItem, $selectedVoucherCategory), - 'product_url' => $this->context->getProductLink($cartItem['id_product']), - 'image_url' => $this->context->getImageLink($cartItem['link_rewrite'], $cartItem['id_image']), - ]; - $remaining -= $roundedTotalWithTax; - } - - return [$orderLines, $remaining]; - } + $orderLines = $this->cartItemProductLinesService->fillProductLinesWithRemainingData($orderLines, Config::VAT_RATE_ROUNDING_PRECISION); - /** - * @param float $totalDiscounts - * @param int $apiRoundingPrecision - * @param array $orderLines - * @param float $remaining - * - * @return array - */ - private function addDiscountsToProductLines($totalDiscounts, $apiRoundingPrecision, $orderLines, $remaining) - { - if ($totalDiscounts >= 0.01) { - $orderLines['discount'] = [ - [ - 'name' => 'Discount', - 'type' => 'discount', - 'quantity' => 1, - 'unitPrice' => -round($totalDiscounts, $apiRoundingPrecision), - 'totalAmount' => -round($totalDiscounts, $apiRoundingPrecision), - 'targetVat' => 0, - 'category' => '', - ], - ]; - $remaining = NumberUtility::plus($remaining, $totalDiscounts); - } + // Add shipping costs to the order lines + $orderLines = $this->cartItemShippingLineService->addShippingLine($roundedShippingCost, $cartSummary, $orderLines); - return [$orderLines, $remaining]; - } + // Add wrapping costs to the order lines + $orderLines = $this->cartItemWrappingService->addWrappingLine($wrappingPrice, $cartSummary, Config::VAT_RATE_ROUNDING_PRECISION, $orderLines); - /** - * @param float $remaining - * @param int $apiRoundingPrecision - * @param array $orderLines - * - * @return array - */ - private function compositeRoundingInaccuracies($remaining, $apiRoundingPrecision, $orderLines) - { - $remaining = round($remaining, $apiRoundingPrecision); - if ($remaining < 0) { - foreach (array_reverse($orderLines) as $hash => $items) { - // Grab the line group's total amount - $totalAmount = array_sum(array_column($items, 'totalAmount')); - - // Remove when total is lower than remaining - if ($totalAmount <= $remaining) { - // The line total is less than remaining, we should remove this line group and continue - $remaining = $remaining - $totalAmount; - unset($items); - continue; - } - - // Otherwise spread the cart line again with the updated total - //TODO: check why remaining comes -100 when testing and new total becomes different - $orderLines[$hash] = static::spreadCartLineGroup($items, $totalAmount + $remaining); - break; - } - } elseif ($remaining > 0) { - foreach (array_reverse($orderLines) as $hash => $items) { - // Grab the line group's total amount - $totalAmount = array_sum(array_column($items, 'totalAmount')); - // Otherwise spread the cart line again with the updated total - $orderLines[$hash] = static::spreadCartLineGroup($items, $totalAmount + $remaining); - break; - } - } + // Add payment fees to the order lines + $orderLines = $this->cartItemPaymentFeeService->addPaymentFeeLine($paymentFeeData, $orderLines); - return $orderLines; - } - - /** - * @param int $apiRoundingPrecision - * @param int $vatRatePrecision - * - * @return array - * - * @throws \PrestaShop\Decimal\Exception\DivisionByZeroException - */ - private function fillProductLinesWithRemainingData(array $orderLines, $apiRoundingPrecision, $vatRatePrecision) - { - foreach ($orderLines as $productHash => $aItem) { - $orderLines[$productHash] = array_map(function ($line) use ($apiRoundingPrecision, $vatRatePrecision) { - $quantity = (int) $line['quantity']; - $targetVat = $line['targetVat']; - $unitPrice = $line['unitPrice']; - $unitPriceNoTax = round(CalculationUtility::getUnitPriceNoTax( - $line['unitPrice'], - $targetVat - ), - $apiRoundingPrecision - ); - - // Calculate VAT - $totalAmount = $line['totalAmount']; - $actualVatRate = 0; - if ($unitPriceNoTax > 0) { - $actualVatRate = round( - $vatAmount = CalculationUtility::getActualVatRate($unitPrice, $unitPriceNoTax, $quantity), - $vatRatePrecision - ); - } - $vatRateWithPercentages = NumberUtility::plus($actualVatRate, 100); - $vatAmount = NumberUtility::times( - $totalAmount, - NumberUtility::divide($actualVatRate, $vatRateWithPercentages) - ); - - $newItem = [ - 'name' => $line['name'], - 'category' => $line['category'], - 'quantity' => (int) $quantity, - 'unitPrice' => round($unitPrice, $apiRoundingPrecision), - 'totalAmount' => round($totalAmount, $apiRoundingPrecision), - 'vatRate' => round($actualVatRate, $apiRoundingPrecision), - 'vatAmount' => round($vatAmount, $apiRoundingPrecision), - 'product_url' => $line['product_url'] ?? null, - 'image_url' => $line['image_url'] ?? null, - ]; - if (isset($line['sku'])) { - $newItem['sku'] = $line['sku']; - } - - return $newItem; - }, $aItem); - } - - return $orderLines; - } - - /** - * @param float $roundedShippingCost - * @param array $cartSummary - * @param int $apiRoundingPrecision - * - * @return array - */ - private function addShippingLine($roundedShippingCost, $cartSummary, $apiRoundingPrecision, array $orderLines) - { - if (round($roundedShippingCost, 2) > 0) { - $shippingVatRate = round(($cartSummary['total_shipping'] - $cartSummary['total_shipping_tax_exc']) / $cartSummary['total_shipping_tax_exc'] * 100, $apiRoundingPrecision); - - $orderLines['shipping'] = [ - [ - 'name' => $this->languageService->lang('Shipping'), - 'quantity' => 1, - 'unitPrice' => round($roundedShippingCost, $apiRoundingPrecision), - 'totalAmount' => round($roundedShippingCost, $apiRoundingPrecision), - 'vatAmount' => round($roundedShippingCost * $shippingVatRate / ($shippingVatRate + 100), $apiRoundingPrecision), - 'vatRate' => $shippingVatRate, - ], - ]; - } - - return $orderLines; - } - - /** - * @param float $wrappingPrice - * @param int $vatRatePrecision - * @param int $apiRoundingPrecision - * - * @return array - */ - private function addWrappingLine($wrappingPrice, array $cartSummary, $vatRatePrecision, $apiRoundingPrecision, array $orderLines) - { - if (round($wrappingPrice, 2) > 0) { - $wrappingVatRate = round( - CalculationUtility::getActualVatRate( - $cartSummary['total_wrapping'], - $cartSummary['total_wrapping_tax_exc'] - ), - $vatRatePrecision - ); - - $orderLines['wrapping'] = [ - [ - 'name' => $this->languageService->lang('Gift wrapping'), - 'quantity' => 1, - 'unitPrice' => round($wrappingPrice, $apiRoundingPrecision), - 'totalAmount' => round($wrappingPrice, $apiRoundingPrecision), - 'vatAmount' => round($wrappingPrice * $wrappingVatRate / ($wrappingVatRate + 100), $apiRoundingPrecision), - 'vatRate' => $wrappingVatRate, - ], - ]; - } - - return $orderLines; - } - - /** - * @param PaymentFeeData $paymentFeeData - * @param int $apiRoundingPrecision - * - * @return array - */ - private function addPaymentFeeLine($paymentFeeData, $apiRoundingPrecision, array $orderLines) - { - if (!$paymentFeeData->isActive()) { - return $orderLines; - } - - $orderLines['surcharge'] = [ - [ - 'name' => $this->languageService->lang('Payment fee'), - 'sku' => Config::PAYMENT_FEE_SKU, - 'quantity' => 1, - 'unitPrice' => round($paymentFeeData->getPaymentFeeTaxIncl(), $apiRoundingPrecision), - 'totalAmount' => round($paymentFeeData->getPaymentFeeTaxIncl(), $apiRoundingPrecision), - 'vatAmount' => NumberUtility::minus($paymentFeeData->getPaymentFeeTaxIncl(), $paymentFeeData->getPaymentFeeTaxExcl()), - 'vatRate' => $paymentFeeData->getTaxRate(), - ], - ]; - - return $orderLines; - } - - /** - * @return array - */ - private function ungroupLines(array $orderLines) - { - $newItems = []; - foreach ($orderLines as &$items) { - foreach ($items as &$item) { - $newItems[] = $item; - } - } - - return $newItems; - } - - /** - * @param string $currencyIsoCode - * @param int $apiRoundingPrecision - * - * @return array - */ - private function convertToLineArray(array $newItems, $currencyIsoCode, $apiRoundingPrecision) - { - foreach ($newItems as $index => $item) { - $line = new Line(); - $line->setName($item['name'] ?: $item['sku']); - $line->setQuantity((int) $item['quantity']); - $line->setSku(isset($item['sku']) ? $item['sku'] : ''); - - $currency = strtoupper(strtolower($currencyIsoCode)); - - if (isset($item['discount'])) { - $line->setDiscountAmount(new Amount( - $currency, - TextFormatUtility::formatNumber($item['discount'], $apiRoundingPrecision, '.', '') - ) - ); - } - - $line->setUnitPrice(new Amount( - $currency, - TextFormatUtility::formatNumber($item['unitPrice'], $apiRoundingPrecision, '.', '') - )); - - $line->setTotalPrice(new Amount( - $currency, - TextFormatUtility::formatNumber($item['totalAmount'], $apiRoundingPrecision, '.', '') - )); - - $line->setVatAmount(new Amount( - $currency, - TextFormatUtility::formatNumber($item['vatAmount'], $apiRoundingPrecision, '.', '') - )); - - if (isset($item['category'])) { - $line->setCategory($item['category']); - } - - $line->setVatRate(TextFormatUtility::formatNumber($item['vatRate'], $apiRoundingPrecision, '.', '')); - $line->setProductUrl($item['product_url'] ?? null); - $line->setImageUrl($item['image_url'] ?? null); - - $newItems[$index] = $line; - } + $newItems = $this->arrayUtility->ungroupLines($orderLines); - return $newItems; + return $this->lineUtility->convertToLineArray($newItems, $currencyIsoCode); } } diff --git a/src/Utility/ArrayUtility.php b/src/Utility/ArrayUtility.php index e769dd2ba..f96b48308 100644 --- a/src/Utility/ArrayUtility.php +++ b/src/Utility/ArrayUtility.php @@ -22,4 +22,21 @@ public static function getLastElement($array) { return end($array); } + + /** + * @param array $lines + * + * @return array + */ + public function ungroupLines(array $lines): array + { + $newItems = []; + foreach ($lines as &$items) { + foreach ($items as &$item) { + $newItems[] = $item; + } + } + + return $newItems; + } } diff --git a/src/Utility/LineUtility.php b/src/Utility/LineUtility.php new file mode 100644 index 000000000..6ec3b732d --- /dev/null +++ b/src/Utility/LineUtility.php @@ -0,0 +1,79 @@ + + * @copyright Mollie B.V. + * @license https://github.com/mollie/PrestaShop/blob/master/LICENSE.md + * + * @see https://github.com/mollie/PrestaShop + * @codingStandardsIgnoreStart + */ + +namespace mollie\src\Utility; + +use Mollie\Config\Config; +use Mollie\DTO\Line; +use Mollie\DTO\Object\Amount; +use Mollie\Utility\TextFormatUtility; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class LineUtility +{ + /** + * @param string $currencyIsoCode + * @param string $currencyIsoCode + * + * @return array + */ + public function convertToLineArray(array $newItems, $currencyIsoCode): array + { + $roundingPrecision = CONFIG::API_ROUNDING_PRECISION; + foreach ($newItems as $index => $item) { + $line = new Line(); + $line->setName($item['name'] ?: $item['sku']); + $line->setQuantity((int) $item['quantity']); + $line->setSku(isset($item['sku']) ? $item['sku'] : ''); + + $currency = strtoupper(strtolower($currencyIsoCode)); + + if (isset($item['discount'])) { + $line->setDiscountAmount(new Amount( + $currency, + TextFormatUtility::formatNumber($item['discount'], $roundingPrecision, '.', '') + ) + ); + } + + $line->setUnitPrice(new Amount( + $currency, + TextFormatUtility::formatNumber($item['unitPrice'], $roundingPrecision, '.', '') + )); + + $line->setTotalPrice(new Amount( + $currency, + TextFormatUtility::formatNumber($item['totalAmount'], $roundingPrecision, '.', '') + )); + + $line->setVatAmount(new Amount( + $currency, + TextFormatUtility::formatNumber($item['vatAmount'], $roundingPrecision, '.', '') + )); + + if (isset($item['category'])) { + $line->setCategory($item['category']); + } + + $line->setVatRate(TextFormatUtility::formatNumber($item['vatRate'], $roundingPrecision, '.', '')); + $line->setProductUrl($item['product_url'] ?? null); + $line->setImageUrl($item['image_url'] ?? null); + + $newItems[$index] = $line; + } + + return $newItems; + } +} diff --git a/src/Utility/RoundingUtility.php b/src/Utility/RoundingUtility.php new file mode 100644 index 000000000..14f187717 --- /dev/null +++ b/src/Utility/RoundingUtility.php @@ -0,0 +1,84 @@ + + * @copyright Mollie B.V. + * @license https://github.com/mollie/PrestaShop/blob/master/LICENSE.md + * + * @see https://github.com/mollie/PrestaShop + * @codingStandardsIgnoreStart + */ + +namespace mollie\src\Utility; + +use Mollie\Config\Config; +use Mollie\Service\CartLine\CartItemsService; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class RoundingUtility +{ + /** + * @var CartItemsService + */ + private $cartItemsService; + + public function __construct(CartItemsService $cartItemsService) + { + $this->cartItemsService = $cartItemsService; + } + + /** + * @param float $amount + * @param int $precision + * + * @return float + */ + public function round(float $amount, int $precision): float + { + return round($amount, $precision); + } + + /** + * @param float $remaining + * @param array $orderLines + * + * @return array + */ + public function compositeRoundingInaccuracies($remaining, $orderLines): array + { + $remaining = $this->round($remaining, CONFIG::API_ROUNDING_PRECISION); + if ($remaining < 0) { + foreach (array_reverse($orderLines) as $hash => $items) { + // Grab the line group's total amount + $totalAmount = array_sum(array_column($items, 'totalAmount')); + + // Remove when total is lower than remaining + if ($totalAmount <= $remaining) { + // The line total is less than remaining, we should remove this line group and continue + $remaining = $remaining - $totalAmount; + unset($items); + continue; + } + + // Otherwise spread the cart line again with the updated total + //TODO: check why remaining comes -100 when testing and new total becomes different + $orderLines[$hash] = $this->cartItemsService->spreadCartLineGroup($items, $totalAmount + $remaining); + break; + } + } elseif ($remaining > 0) { + foreach (array_reverse($orderLines) as $hash => $items) { + // Grab the line group's total amount + $totalAmount = array_sum(array_column($items, 'totalAmount')); + // Otherwise spread the cart line again with the updated total + $orderLines[$hash] = $this->cartItemsService->spreadCartLineGroup($items, $totalAmount + $remaining); + break; + } + } + + return $orderLines; + } +}