Skip to content

Commit

Permalink
feat(checkout): calculate shipping costs per carrier
Browse files Browse the repository at this point in the history
  • Loading branch information
EdieLemoine committed Nov 9, 2023
1 parent 3d3c223 commit e03a380
Show file tree
Hide file tree
Showing 55 changed files with 543 additions and 263 deletions.
1 change: 0 additions & 1 deletion src/Hooks/HasPdkCheckoutDeliveryOptionsHooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ private function renderDeliveryOptions(): string
'content' => Frontend::renderDeliveryOptions($cartRepository->get($cart)),
'shippingAddress' => $this->encodeAddress($addressAdapter->fromAddress($cart->id_address_delivery)),
'billingAddress' => $this->encodeAddress($addressAdapter->fromAddress($cart->id_address_invoice)),

]);

return $this->display($this->name, 'views/templates/hook/carrier_delivery_options.tpl');
Expand Down
24 changes: 6 additions & 18 deletions src/Hooks/HasPdkScriptHooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,16 @@ public function hookDisplayHeader(): void
/** @var \MyParcelNL\Pdk\Frontend\Contract\ViewServiceInterface $viewService */
$viewService = Pdk::get(ViewServiceInterface::class);

if (! $viewService->isCheckoutPage()) {
return;
}
$deliveryOptionsEnabled = Settings::get(CheckoutSettings::ENABLE_DELIVERY_OPTIONS, CheckoutSettings::ID);

$this->loadCoreScripts();

if (! Settings::get(CheckoutSettings::ENABLE_DELIVERY_OPTIONS, CheckoutSettings::ID)) {
if (! $deliveryOptionsEnabled || ! $viewService->isCheckoutPage()) {
return;
}

$this->loadDeliveryOptionsScripts();
$this->loadCheckoutScripts();
}

private function loadCoreScripts(): void
{
$this->context->controller->addJS("{$this->_path}views/js/frontend/checkout-core/dist/index.iife.js");
$this->context->controller->addCSS("{$this->_path}views/js/frontend/checkout-core/dist/style.css");
}

private function loadDeliveryOptionsScripts(): void
private function loadCheckoutScripts(): void
{
$this->context->controller->registerJavascript(
'myparcelnl-delivery-options',
Expand All @@ -73,9 +63,7 @@ private function loadDeliveryOptionsScripts(): void
['server' => 'remote', 'position' => 'head', 'priority' => 1]
);

$this->context->controller->addJS(
"{$this->_path}views/js/frontend/checkout-delivery-options/dist/index.iife.js"
);
$this->context->controller->addCSS("{$this->_path}views/js/frontend/checkout-delivery-options/dist/style.css");
$this->context->controller->addJS("{$this->_path}views/js/frontend/checkout/dist/index.iife.js");
$this->context->controller->addCSS("{$this->_path}views/js/frontend/checkout/dist/style.css");
}
}
85 changes: 81 additions & 4 deletions src/Hooks/HasPsShippingCostHooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,58 @@

namespace MyParcelNL\PrestaShop\Hooks;

use MyParcelNL\Pdk\App\Cart\Model\PdkCartFee;
use MyParcelNL\Pdk\App\DeliveryOptions\Contract\DeliveryOptionsFeesServiceInterface;
use MyParcelNL\Pdk\Carrier\Model\Carrier;
use MyParcelNL\Pdk\Facade\Pdk;
use MyParcelNL\Pdk\Shipment\Model\DeliveryOptions;
use MyParcelNL\PrestaShop\Contract\PsCarrierServiceInterface;
use Tools;

trait HasPsShippingCostHooks
{
/**
* @param $cart
* @param $shippingCost
* Will be filled automatically during the checkout process with the id of the current carrier.
*
* @var int
*/
public $id_carrier;

/**
* @var array
*/
private $cachedPrices = [];

/**
* @param \Cart|mixed $cart
* @param int|float $shippingCost
*
* @return mixed
* @return int|float
* @throws \MyParcelNL\Pdk\Base\Exception\InvalidCastException
*/
public function getOrderShippingCost($cart, $shippingCost)
{
return $shippingCost;
if (! $this->hasPdk || ! $this->context->controller) {
return $shippingCost;
}

/** @var \MyParcelNL\PrestaShop\Contract\PsCarrierServiceInterface $carrierService */
$carrierService = Pdk::get(PsCarrierServiceInterface::class);

$carrier = $carrierService->getMyParcelCarrier($this->id_carrier);

if (! $carrier) {
return $shippingCost;
}

$deliveryOptions = $this->getDeliveryOptions($carrier);
$cacheKey = md5(json_encode($deliveryOptions->toArrayWithoutNull()));

if (! isset($this->cachedPrices[$cacheKey])) {
$this->cachedPrices[$cacheKey] = $this->calculateShippingCost($deliveryOptions, (float) $shippingCost);
}

return $this->cachedPrices[$cacheKey];
}

/**
Expand All @@ -26,4 +67,40 @@ public function getOrderShippingCostExternal($params): bool
{
return true;
}

/**
* @param \MyParcelNL\Pdk\Shipment\Model\DeliveryOptions $deliveryOptions
* @param float $shippingCost
*
* @return float
*/
private function calculateShippingCost(DeliveryOptions $deliveryOptions, float $shippingCost): float
{
/** @var \MyParcelNL\Pdk\App\DeliveryOptions\Contract\DeliveryOptionsFeesServiceInterface $service */
$service = Pdk::get(DeliveryOptionsFeesServiceInterface::class);

return $service
->getFees($deliveryOptions)
->reduce(function (float $carry, PdkCartFee $fee) {
return $carry + $fee->getAmount();
}, $shippingCost);
}

/**
* Get the actual delivery options from the checkout hidden input, or get the default delivery options for the current carrier, without any options.
*
* @param \MyParcelNL\Pdk\Carrier\Model\Carrier $carrier
*
* @return \MyParcelNL\Pdk\Shipment\Model\DeliveryOptions
*/
private function getDeliveryOptions(Carrier $carrier): DeliveryOptions
{
$deliveryOptions = Tools::getValue(Pdk::get('checkoutHiddenInputName'), null);

if ($deliveryOptions) {
return new DeliveryOptions(json_decode($deliveryOptions, true));
}

return new DeliveryOptions(['carrier' => $carrier]);
}
}
2 changes: 1 addition & 1 deletion src/Pdk/Frontend/Service/PsFrontendRenderService.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public function renderDeliveryOptions(PdkCart $cart): string
$context = $this->contextService->createContexts([Context::ID_CHECKOUT], ['cart' => $cart]);

return sprintf(
'<div id="mypa-delivery-options-wrapper" data-context="%s">%s<div id="myparcel-delivery-options"></div></div>',
'<div id="mypa-delivery-options-wrapper" class="mb-1" data-context="%s"><div class="card-block bg-faded">%s<div id="myparcel-delivery-options"></div></div></div>',
$this->encodeContext($context),
$customCss ? sprintf('<style>%s</style>', $customCss) : ''
);
Expand Down
43 changes: 43 additions & 0 deletions tests/Mock/MockPsTools.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php
/** @noinspection PhpUnused */

declare(strict_types=1);

namespace MyParcelNL\PrestaShop\Tests\Mock;

abstract class MockPsTools extends BaseMock
{
/**
* @var array
*/
private static $values = [];

/**
* @return array
*/
public static function getAllValues(): array
{
return self::$values;
}

/**
* @param string $key
*
* @return mixed
*/
public static function getValue(string $key)
{
return self::$values[$key] ?? null;
}

/**
* @param array $values
*
* @return void
* @internal
*/
public static function setValues(array $values): void
{
self::$values = $values;
}
}
139 changes: 139 additions & 0 deletions tests/Unit/Hooks/HasPsShippingCostHooksTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection,StaticClosureCanBeUsedInspection,AutoloadingIssuesInspection,PhpIllegalPsrClassPathInspection */

declare(strict_types=1);

namespace MyParcelNL\PrestaShop\Hooks;

use Carrier as PsCarrier;
use Cart;
use CartFactory;
use Context;
use FrontController;
use MyParcelNL\Pdk\Carrier\Model\Carrier;
use MyParcelNL\Pdk\Facade\Pdk;
use MyParcelNL\Pdk\Settings\Model\CarrierSettings;
use MyParcelNL\Pdk\Settings\Model\Settings;
use MyParcelNL\Pdk\Shipment\Model\DeliveryOptions;
use MyParcelNL\Pdk\Shipment\Model\ShipmentOptions;
use MyParcelNL\Pdk\Tests\Factory\Collection\FactoryCollection;
use MyParcelNL\PrestaShop\Entity\MyparcelnlCarrierMapping;
use MyParcelNL\PrestaShop\Tests\Mock\MockPsTools;
use MyParcelNL\PrestaShop\Tests\Uses\UsesMockPsPdkInstance;
use function MyParcelNL\Pdk\Tests\factory;
use function MyParcelNL\Pdk\Tests\usesShared;
use function MyParcelNL\PrestaShop\psFactory;

final class ClassWithTrait
{
use HasPsShippingCostHooks;

protected $context;

protected $hasPdk;

public function __construct(bool $hasPdk = true)
{
$this->context = Context::getContext();
$this->context->controller = new FrontController();
$this->hasPdk = $hasPdk;
$this->cachedPrices = [];
}

public function setIdCarrier(int $idCarrier): void
{
$this->id_carrier = $idCarrier;
}
}

usesShared(new UsesMockPsPdkInstance());

it('calculates shipping costs', function (CartFactory $cartFactory, array $deliveryOptions = [], float $addedCost = 0) {
if ($deliveryOptions) {
MockPsTools::setValues([
Pdk::get('checkoutHiddenInputName') => json_encode($deliveryOptions),
]);
}

$instance = new ClassWithTrait();
$instance->setIdCarrier(93);

$baseCost = 10;
$cost = $instance->getOrderShippingCost($cartFactory->make(), $baseCost);

expect(number_format($cost, 2))->toEqual(number_format($baseCost + $addedCost, 2));
})->with([
'no carrier' => [
function () {
return psFactory(Cart::class);
},
],

'carrier without linked myparcel carrier' => [
function () {
$psCarrier = psFactory(PsCarrier::class)
->withId(93)
->store();

return psFactory(Cart::class)->withCarrier($psCarrier);
},
],

'carrier with linked myparcel carrier but no delivery options' => [
function () {
$psCarrier = psFactory(PsCarrier::class)->withId(93);

(new FactoryCollection([
$psCarrier,
psFactory(MyparcelnlCarrierMapping::class)
->withCarrierId(93)
->withMyparcelCarrier(Carrier::CARRIER_POSTNL_NAME),
factory(Settings::class)->withCarrierPostNl([
CarrierSettings::PRICE_DELIVERY_TYPE_STANDARD => 2.95,
]),
]))->store();

return psFactory(Cart::class)->withCarrier($psCarrier);
},
[],
'cost' => 2.95,
],

'carrier with linked myparcel carrier and delivery options in values' => [
function () {
$psCarrier = psFactory(PsCarrier::class)->withId(93);

(new FactoryCollection([
$psCarrier,
psFactory(MyparcelnlCarrierMapping::class)
->withCarrierId(93)
->withMyparcelCarrier(Carrier::CARRIER_POSTNL_NAME),
factory(Settings::class)
->withCarrierPostNl([
CarrierSettings::PRICE_SIGNATURE => 0.45,
CarrierSettings::PRICE_DELIVERY_TYPE_STANDARD => 4.95,
]),
]))->store();

return psFactory(Cart::class)->withCarrier($psCarrier);
},
'values' => [
DeliveryOptions::CARRIER => Carrier::CARRIER_POSTNL_NAME,
DeliveryOptions::DELIVERY_TYPE => DeliveryOptions::DELIVERY_TYPE_STANDARD_NAME,
DeliveryOptions::SHIPMENT_OPTIONS => [
ShipmentOptions::SIGNATURE => true,
],
],
'cost' => 5.4,
],
]);

it('returns input shipping cost if pdk is not set up', function () {
$instance = new ClassWithTrait(false);

$result = $instance->getOrderShippingCost(psFactory(Cart::class)->make(), 123.45);

expect($result)->toBe(123.45);
});


25 changes: 24 additions & 1 deletion tests/factories/CartFactory.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
<?php

declare(strict_types=1);

use MyParcelNL\PrestaShop\Tests\Factory\AbstractPsObjectModelFactory;
use MyParcelNL\PrestaShop\Tests\Factory\Contract\WithCarrier;
use MyParcelNL\PrestaShop\Tests\Factory\Contract\WithCurrency;
use MyParcelNL\PrestaShop\Tests\Factory\Contract\WithCustomer;
use MyParcelNL\PrestaShop\Tests\Factory\Contract\WithLang;
use MyParcelNL\PrestaShop\Tests\Factory\Contract\WithShopGroup;
use MyParcelNL\PrestaShop\Tests\Factory\Contract\WithTimestamps;

/**
* @method $this withIdAddressDelivery(int $idAddressDelivery)
* @method $this withIdAddressInvoice(int $idAddressInvoice)
* @method $this withIdGuest(int $idGuest)
* @method $this withRecyclable(bool $recyclable)
* @method $this withGift(bool $gift)
* @method $this withGiftMessage(string $giftMessage)
* @method $this withMobileTheme(bool $mobileTheme)
* @method $this withSecureKey(string $secureKey)
* @method $this withCheckedTos(bool $checkedTos)
* @method $this withPictures(array $pictures)
* @method $this withTextFields(array $textFields)
* @method $this withDeliveryOption(string $deliveryOption)
* @method $this withAllowSeperatedPackage(bool $allowSeperatedPackage)
*
*/
final class CartFactory extends AbstractPsObjectModelFactory
final class CartFactory extends AbstractPsObjectModelFactory implements WithShopGroup, WithLang, WithCurrency,
WithCustomer, WithCarrier, WithTimestamps
{
protected function getObjectModelClass(): string
{
Expand Down
Loading

0 comments on commit e03a380

Please sign in to comment.