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'; } } }