293 lines
11 KiB
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';
|
|
}
|
|
|
|
}
|
|
}
|