SmcShipcloudLive/src/Core/Checkout/Cart/Delivery/SmcDeliveryProcessor.php

293 lines
11 KiB
PHP

<?php declare(strict_types=1);
namespace SmcShipcloudLive\Core\Checkout\Cart\Delivery;
use SmcShipcloudLive\Core\Checkout\Cart\Error\UnableToGetShippingQuoteError;
use Psr\Log\LoggerInterface;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartBehavior;
use Shopware\Core\Checkout\Cart\CartDataCollectorInterface;
use Shopware\Core\Checkout\Cart\CartProcessorInterface;
use Shopware\Core\Checkout\Cart\Delivery\DeliveryBuilder;
use Shopware\Core\Checkout\Cart\Delivery\DeliveryCalculator;
use Shopware\Core\Checkout\Cart\Delivery\Struct\Delivery;
use Shopware\Core\Checkout\Cart\Delivery\Struct\DeliveryCollection;
use Shopware\Core\Checkout\Cart\LineItem\CartDataCollection;
use Shopware\Core\Checkout\Cart\LineItem\LineItemCollection;
use Shopware\Core\Checkout\Cart\Price\Struct\CalculatedPrice;
use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice;
use Shopware\Core\Checkout\Cart\Price\Struct\QuantityPriceDefinition;
use Shopware\Core\Checkout\Cart\Price\QuantityPriceCalculator;
use Shopware\Core\Checkout\Cart\Tax\PercentageTaxRuleBuilder;
use Shopware\Core\Checkout\Cart\Tax\Struct\CalculatedTaxCollection;
use Shopware\Core\Checkout\Cart\Tax\Struct\TaxRuleCollection;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Core\Checkout\Shipping\Cart\Error\ShippingMethodBlockedError;
use Shopware\Core\Checkout\Shipping\ShippingMethodEntity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Checkout\Customer\Aggregate\CustomerAddress\CustomerAddressEntity;
use ComyoMedia\Shipcloud\Shipcloud;
class SmcDeliveryProcessor implements CartProcessorInterface, CartDataCollectorInterface
{
public const MANUAL_SHIPPING_COSTS = 'manualShippingCosts';
public const SKIP_DELIVERY_PRICE_RECALCULATION = 'skipDeliveryPriceRecalculation';
public const SKIP_DELIVERY_TAX_RECALCULATION = 'skipDeliveryTaxRecalculation';
protected DeliveryBuilder $builder;
protected DeliveryCalculator $deliveryCalculator;
protected EntityRepositoryInterface $shippingMethodRepository;
protected QuantityPriceCalculator $quantityPriceCalculator;
protected PercentageTaxRuleBuilder $percentageTaxRuleBuilder;
protected SystemConfigService $systemConfigService;
protected LoggerInterface $logger;
public function __construct(DeliveryBuilder $builder, DeliveryCalculator $deliveryCalculator,
EntityRepositoryInterface $shippingMethodRepository,
QuantityPriceCalculator $quantityPriceCalculator,
PercentageTaxRuleBuilder $percentageTaxRuleBuilder,
SystemConfigService $systemConfigService,
LoggerInterface $logger)
{
$this->builder = $builder;
$this->deliveryCalculator = $deliveryCalculator;
$this->shippingMethodRepository = $shippingMethodRepository;
$this->quantityPriceCalculator = $quantityPriceCalculator;
$this->percentageTaxRuleBuilder = $percentageTaxRuleBuilder;
$this->systemConfigService = $systemConfigService;
$this->logger = $logger;
/* obtain API key from SystemConfig */
if ($this->systemConfigService->get('SmcShipcloudLive.config.useSandbox')) {
$sc_api_key = $this->systemConfigService->get('SmcShipcloudLive.config.sandboxApiKey');
} else {
$sc_api_key = $this->systemConfigService->get('SmcShipcloudLive.config.apiKey');
}
$this->shipcloud = new Shipcloud($sc_api_key);
}
public static function buildKey(string $shippingMethodId): string
{
return 'shipping-method-' . $shippingMethodId;
}
public function collect(CartDataCollection $data, Cart $original, SalesChannelContext $context, CartBehavior $behavior): void
{
$this->logger->warning('collect');
/* ensure we have at least one shipping method key to avoid ShippingMethodNotFoundException */
$shipping_method = $context->getShippingMethod();
$shipping_method_id = $shipping_method->getId();
$this->logger->warning($shipping_method->getName());
$key = self::buildKey($shipping_method_id);
$data->set($key, $shipping_method);
}
public function process(CartDataCollection $data, Cart $original, Cart $toCalculate, SalesChannelContext $context, CartBehavior $behavior): void
{
/* below steps 1:1 from original shopware DeliveryProcessor */
$deliveries = $this->builder->build($toCalculate, $data, $context, $behavior);
$delivery = $deliveries->first();
if ($behavior->hasPermission(self::SKIP_DELIVERY_PRICE_RECALCULATION)) {
$originalDeliveries = $original->getDeliveries();
$originalDelivery = $originalDeliveries->first();
if ($delivery !== null && $originalDelivery !== null) {
$originalDelivery->setShippingMethod($delivery->getShippingMethod());
//Keep old prices
$delivery->setShippingCosts($originalDelivery->getShippingCosts());
//Recalculate tax
$this->deliveryCalculator->calculate($data, $toCalculate, $deliveries, $context);
$originalDelivery->setShippingCosts($delivery->getShippingCosts());
}
// New shipping method (if changed) but with old prices
$toCalculate->setDeliveries($originalDeliveries);
return;
}
/* below steps 1:1 from original shopware DeliveryProcessor */
$manualShippingCosts = $original->getExtension(self::MANUAL_SHIPPING_COSTS);
if ($delivery !== null && $manualShippingCosts instanceof CalculatedPrice) {
$delivery->setShippingCosts($manualShippingCosts);
}
/* this is the actual part where we override the shipping costs */
$this->calculate($data, $toCalculate, $deliveries, $context);
$toCalculate->setDeliveries($deliveries);
}
/* similar to DeliveryCalculater::calculate */
private function calculate(CartDataCollection $data, Cart $cart, DeliveryCollection $deliveries, SalesChannelContext $context): void
{
$this->logger->warning('calculate');
foreach ($deliveries as $delivery) {
$this->calculateDelivery($data, $cart, $delivery, $context);
}
}
/* similar to DeliveryCalculater::calculateDelivery */
private function calculateDelivery(CartDataCollection $data, Cart $cart, Delivery $delivery, SalesChannelContext $context): void
{
$deliver_loc = $delivery->getLocation();
$cust_addr = $deliver_loc->getAddress();
if ($cust_addr == null) {
/* address not yet known, cannot determine shipping cost */
return;
}
$cust_addr_sc = SmcDeliveryProcessor::custAddr2shipcloud($cust_addr, $deliver_loc->getCountry()->getIso());
$shipping_method = $context->getShippingMethod();
$service_sc = SmcDeliveryProcessor::shippingMethod2shipcloud($shipping_method);
/* compute total weight, volume and monetary value */
$weight_kg = $delivery->getPositions()->getWithoutDeliveryFree()->getWeight();
$volume = $delivery->getPositions()->getWithoutDeliveryFree()->getVolume();
$value = $delivery->getPositions()->getWithoutDeliveryFree()->getPrices()->sum()->getTotalPrice();
$this->logger->warning("weight_kg: ${weight_kg}, volume: ${volume}, value: ${value}");
if ($weight_kg == 0) {
$cart->addErrors(new UnableToGetShippingQuoteError("Total weight of delivery cannot be zero"));
return;
}
/* assume 10% of product weight as packaging weight; minimum 200g */
if ($weight_kg < 1) {
$tare_kg = 0.2;
} else {
$tare_kg = $weight_kg * 0.1;
}
/* perform Actual API call with address / weight / volume */
try {
$quote = $this->shipcloud->shipmentQuotes()->create([
'carrier' => 'ups',
'service' => $service_sc,
'from' => [
'street' => 'Alt-Moabit',
'street_no' => '93',
'zip_code' => '10559',
'city' => 'Berlin',
'country' => 'DE'
],
'to' => $cust_addr_sc,
'package' => SmcDeliveryProcessor::weight2shipcloudPackage($weight_kg),
]);
} catch (\GuzzleHttp\Exception\ClientException $e) {
$this->logger->warning(print_r($e->getMessage(), True));
if ($e->getResponse()->getStatusCode() == 422) {
$cart->addErrors( new ShippingMethodBlockedError((string) $shipping_method->getTranslation('name')));
} else {
$cart->addErrors(new UnableToGetShippingQuoteError($e->getMessage()));
}
return;
}
$quote_eur = $quote['shipment_quote']['price'];
if ($quote_eur < 5) {
$cart->addErrors(new UnableToGetShippingQuoteError("Rate implausible"));
return;
}
/* add safety margin of 5% on top of shipcloud quote */
$margin = (0.05 * $quote_eur);
/* add transport insurance costs 0.0035 */
$transport_insurance = (0.0035 * $value);
/* convert into a gross price, as API returns net */
$quote_eur_gross = round($quote_eur + $margin + $transport_insurance), 2);
if ($context->getTaxState() === CartPrice::TAX_STATE_GROSS) {
/* FIXME: don't use static 19% but destination country specific rate */
$quote_eur_gross = $quote_eur_gross * 1.19;
}
$this->logger->warning("shipcloud_quote 'ups/{$service_sc}': ${quote_eur}, weight_kg: ${weight_kg}, tare_kg: ${tare_kg}, margin_eur: ${margin}, transport_insurance: ${transport_insurance}, gross: ${quote_eur_gross}");
/* convert raw float into the right format/object required by shopware */
$price = $this->costWithTaxes($shipping_method, $delivery->getPositions()->getLineItems(), $context, $quote_eur_gross);
$delivery->setShippingCosts($price);
}
/* compute the shipping price with taxes (as applicable) as CalculatedPrice object */
private function costWithTaxes(ShippingMethodEntity $shippingMethod, LineItemCollection $calculatedLineItems, SalesChannelContext $context, $quote_eur): CalculatedPrice
{
switch ($shippingMethod->getTaxType()) {
case ShippingMethodEntity::TAX_TYPE_HIGHEST:
$rules = $calculatedLineItems->getPrices()->getHighestTaxRule();
break;
case ShippingMethodEntity::TAX_TYPE_FIXED:
$tax = $shippingMethod->getTax();
if ($tax !== null) {
$rules = $context->buildTaxRules($tax->getId());
break;
}
// no break
default:
$rules = $this->percentageTaxRuleBuilder->buildRules($calculatedLineItems->getPrices()->sum());
}
$q_p_def = new QuantityPriceDefinition($quote_eur, $rules, 1);
return $this->quantityPriceCalculator->calculate($q_p_def, $context);
}
private static function weight2shipcloudPackage(float $weight_kg)
{
$ret = SmcDeliveryProcessor::estimateDimensions($weight_kg);
$ret['weight'] = $weight_kg;
return $ret;
}
/* return the cubic root of 'x' */
private static function cbrt(float $x): float
{
return $x ** (1.0/3);
}
/* estimate the dimensions of a package given its weight + density */
private static function estimateDimensions(float $weight_kg, float $density_kg_per_dm3 = 0.5)
{
/* compute volume */
$volume_dm3 = $weight_kg / $density_kg_per_dm3;
$volume_cm3 = 1000 * $volume_dm3;
/* compute dimensions assuming l=3x, w=2x, h=1x -> x=6 */
$x = SmcDeliveryProcessor::cbrt($volume_cm3 / 6);
return ['length' => 3.0*$x, 'width' => 2.0*$x, 'height' => $x];
}
/* convert address from internal format to shipcloud REST compatible dict */
private static function custAddr2shipcloud(CustomerAddressEntity $addr, $country_code)
{
return [
'street' => $addr->getStreet(),
'street_no' => '1', // FIXME
'zip_code' => $addr->getZipcode(),
'city' => $addr->getCity(),
'country' => $country_code
];
}
private static function shippingMethod2shipcloud($method)
{
switch ($method->getName()) {
case 'UPS Standard':
return 'standard';
case 'UPS Expedited':
return 'ups_expedited';
case 'UPS Express Saver':
return 'one_day';
default:
return 'one_day';
}
}
}