first working version making API calls

This commit is contained in:
Harald Welte 2022-06-26 20:09:22 +02:00
parent f7ce5ab683
commit c434664fd0
6 changed files with 312 additions and 23 deletions

View File

@ -1,21 +1,31 @@
{
"name": "sysmocom/shopware6-shipcloud-live",
"description": "shipcloud live quote pluging",
"version": "0.0.1",
"version": "0.0.2",
"type": "shopware-platform-plugin",
"license": "AGPL-3.0-or-later",
"authors": [
{
"name": "Harald Welte",
"email": "hwelte@sysmocom.de",
"role": "main developer"
}
],
"minimum-stability": "dev",
"repositories": [
{
"url": "https://github.com/comyo-media/shipcloud-php",
"type": "git"
}
],
"require": {
"shopware/core": "^6."
"shopware/core": "^6.",
"comyo-media/shipcloud-php": "9999999-dev"
},
"autoload": {
"psr-4": {
"SmcShipcloudLive\\": "src/"
"SmcShipcloudLive\\": "src/",
"ComyoMedia\\Shipcloud\\": "vendor/comyo-media/shipcloud-php/src/"
}
},
"extra": {

View File

@ -2,34 +2,73 @@
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\LineItem\CartDataCollection;
use Shopware\Core\Checkout\Cart\Price\Struct\CalculatedPrice;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\Checkout\Shipping\Cart\Error\ShippingMethodBlockedError;
use Shopware\Core\Checkout\Cart\Delivery\DeliveryBuilder;
use Shopware\Core\Checkout\Cart\Delivery\DeliveryCalculator;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
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
{
protected $builder;
protected $deliveryCalculator;
protected $shippingMethodRepository;
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)
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
@ -39,13 +78,15 @@ class SmcDeliveryProcessor implements CartProcessorInterface, CartDataCollectorI
public function collect(CartDataCollection $data, Cart $original, SalesChannelContext $context, CartBehavior $behavior): void
{
//$original->addErrors( new ShippingMethodBlockedError((string) "Foobar collect"));
$this->logger->warning('collect');
/* ensure we have at least one shipping method key to avoid ShippingMethodNotFoundException */
$default_ship = $context->getShippingMethod();
$default_ship_id = $default_ship->getId();
$key = self::buildKey($default_ship_id);
$data->set($key, $default_ship);
$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
@ -53,13 +94,199 @@ class SmcDeliveryProcessor implements CartProcessorInterface, CartDataCollectorI
/* below steps 1:1 from original shopware DeliveryProcessor */
$deliveries = $this->builder->build($toCalculate, $data, $context, $behavior);
$delivery = $deliveries->first();
/* custom computation of costs */
$costs = new CalculatedPrice(23, 42, new CalculatedTaxCollection(), new TaxRuleCollection());
$delivery->setShippingCosts($costs);
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 */
$this->deliveryCalculator->calculate($data, $toCalculate, $deliveries, $context);
$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 = ($quote_eur + $margin + $transport_insurance);
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';
}
}
}

View File

@ -0,0 +1,42 @@
<?php declare(strict_types=1);
namespace SmcShipcloudLive\Core\Checkout\Cart\Error;
use Shopware\Core\Checkout\Cart\Error\Error;
class UnableToGetShippingQuoteError extends Error
{
private $key = "unable-to-get-shipping-quote";
private $text;
public function __construct(string $message)
{
$this->key = 'unable-to-get-shipping-quote';
$this->text = $message;
}
public function getParameters(): array
{
return ['key' => $this->key, 'text' => $this->text];
}
public function getId(): string
{
return $this->key;
}
public function getMessageKey(): string
{
return $this->key;
}
public function getLevel(): int
{
return self::LEVEL_ERROR;
}
public function blockOrder(): bool
{
return true;
}
}

View File

@ -0,0 +1,5 @@
{
"checkout": {
"unable-to-get-shipping-quote": "Unable to get shipping quote via API: %text%"
}
}

View File

@ -34,6 +34,10 @@
<argument type="service" id="Shopware\Core\Checkout\Cart\Delivery\DeliveryBuilder"/>
<argument type="service" id="Shopware\Core\Checkout\Cart\Delivery\DeliveryCalculator"/>
<argument type="service" id="shipping_method.repository"/>
<argument type="service" id="Shopware\Core\Checkout\Cart\Price\QuantityPriceCalculator"/>
<argument type="service" id="Shopware\Core\Checkout\Cart\Tax\PercentageTaxRuleBuilder"/>
<argument type="service" id="Shopware\Core\System\SystemConfig\SystemConfigService"/>
<argument type="service" id="Psr\Log\LoggerInterface"/>
<tag name="shopware.cart.processor" priority="-5000"/>
<tag name="shopware.cart.collector" priority="-5000"/>
</service>

@ -0,0 +1 @@
Subproject commit 469e7abb6d430b8c4cc11bc1ce46ab023bc0b3d8