commerce-8.x-2.8/modules/tax/src/TaxOrderProcessor.php
modules/tax/src/TaxOrderProcessor.php
<?php
namespace Drupal\commerce_tax;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_order\Entity\OrderItemInterface;
use Drupal\commerce_order\OrderProcessorInterface;
use Drupal\commerce_price\RounderInterface;
use Drupal\commerce_store\Entity\StoreInterface;
use Drupal\commerce_tax\Entity\TaxTypeInterface;
use Drupal\commerce_tax\Plugin\Commerce\TaxType\LocalTaxTypeInterface;
use Drupal\commerce_tax\Resolver\ChainTaxRateResolverInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Applies taxes to orders during the order refresh process.
*/
class TaxOrderProcessor implements OrderProcessorInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The rounder.
*
* @var \Drupal\commerce_price\RounderInterface
*/
protected $rounder;
/**
* The chain tax rate resolver.
*
* @var \Drupal\commerce_tax\Resolver\ChainTaxRateResolverInterface
*/
protected $chainRateResolver;
/**
* The store's tax zones, keyed by store ID.
*
* @var array
*/
protected $storeZones = [];
/**
* The loaded tax types.
*
* @var \Drupal\commerce_tax\Entity\TaxTypeInterface[]
*/
protected $taxTypes = [];
/**
* A cache of instantiated store profiles.
*
* @var \Drupal\profile\Entity\ProfileInterface
*/
protected $storeProfiles = [];
/**
* Constructs a new TaxOrderProcessor object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\commerce_price\RounderInterface $rounder
* The rounder.
* @param \Drupal\commerce_tax\Resolver\ChainTaxRateResolverInterface $chain_rate_resolver
* The chain tax rate resolver.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, RounderInterface $rounder, ChainTaxRateResolverInterface $chain_rate_resolver) {
$this->entityTypeManager = $entity_type_manager;
$this->rounder = $rounder;
$this->chainRateResolver = $chain_rate_resolver;
}
/**
* {@inheritdoc}
*/
public function process(OrderInterface $order) {
$tax_types = $this->getTaxTypes();
foreach ($tax_types as $tax_type) {
if ($tax_type->getPlugin()->applies($order)) {
$tax_type->getPlugin()->apply($order);
}
}
// Don't overcharge a tax-exempt customer if the price is tax-inclusive.
// For example, a 12 EUR price with 20% EU VAT gets reduced to 10 EUR
// when selling to customers outside the EU, but only if no other tax
// was applied (e.g. a Japanese customer paying Japanese tax due to the
// store being registered to collect tax there).
$store = $order->getStore();
if ($store->get('prices_include_tax')->value) {
foreach ($order->getItems() as $order_item) {
$tax_adjustments = array_filter($order_item->getAdjustments(), function ($adjustment) {
/** @var \Drupal\commerce_order\Adjustment $adjustment */
return $adjustment->getType() == 'tax';
});
if (empty($tax_adjustments)) {
$unit_price = $order_item->getUnitPrice();
$rates = $this->getDefaultRates($order_item, $store);
foreach ($rates as $rate) {
$percentage = $rate->getPercentage();
$tax_amount = $percentage->calculateTaxAmount($order_item->getUnitPrice(), TRUE);
$tax_amount = $this->rounder->round($tax_amount);
$unit_price = $unit_price->subtract($tax_amount);
}
$order_item->setUnitPrice($unit_price);
}
}
}
}
/**
* Gets the default tax rates for the given order item and store.
*
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
* The order item.
* @param \Drupal\commerce_store\Entity\StoreInterface $store
* The store.
*
* @return \Drupal\commerce_tax\TaxRate[]
* The tax rates, keyed by tax zone ID.
*/
protected function getDefaultRates(OrderItemInterface $order_item, StoreInterface $store) {
$store_profile = $this->buildStoreProfile($store);
$rates = [];
foreach ($this->getStoreZones($store) as $zone) {
$rate = $this->chainRateResolver->resolve($zone, $order_item, $store_profile);
if (is_object($rate)) {
$rates[$zone->getId()] = $rate;
}
}
return $rates;
}
/**
* Gets the tax zones for the given store.
*
* @param \Drupal\commerce_store\Entity\StoreInterface $store
* The store.
*
* @return \Drupal\commerce_tax\TaxZone[]
* The tax zones.
*/
protected function getStoreZones(StoreInterface $store) {
$store_id = $store->id();
if (!isset($this->storeZones[$store_id])) {
$tax_types = $this->getTaxTypes();
$tax_types = array_filter($tax_types, function (TaxTypeInterface $tax_type) {
$tax_type_plugin = $tax_type->getPlugin();
return ($tax_type_plugin instanceof LocalTaxTypeInterface) && $tax_type_plugin->isDisplayInclusive();
});
$this->storeZones[$store_id] = [];
$store_address = $store->getAddress();
foreach ($tax_types as $tax_type) {
/** @var \Drupal\commerce_tax\Plugin\Commerce\TaxType\LocalTaxTypeInterface $tax_type_plugin */
$tax_type_plugin = $tax_type->getPlugin();
foreach ($tax_type_plugin->getZones() as $zone) {
if ($zone->match($store_address)) {
$this->storeZones[$store_id][] = $zone;
}
}
// Assume that only a single tax type's zones will match.
if (count($this->storeZones[$store_id]) > 0) {
break;
}
}
}
return $this->storeZones[$store_id];
}
/**
* Builds a customer profile for the given store.
*
* @param \Drupal\commerce_store\Entity\StoreInterface $store
* The store.
*
* @return \Drupal\profile\Entity\ProfileInterface
* The customer profile.
*/
protected function buildStoreProfile(StoreInterface $store) {
$store_id = $store->id();
if (!isset($this->storeProfiles[$store_id])) {
$profile_storage = $this->entityTypeManager->getStorage('profile');
$this->storeProfiles[$store_id] = $profile_storage->create([
'type' => 'customer',
'uid' => $store->getOwnerId(),
'address' => $store->getAddress(),
]);
}
return $this->storeProfiles[$store_id];
}
/**
* Gets the available tax types.
*
* @return \Drupal\commerce_tax\Entity\TaxTypeInterface[]
* The tax types.
*/
protected function getTaxTypes() {
if (empty($this->taxTypes)) {
$tax_type_storage = $this->entityTypeManager->getStorage('commerce_tax_type');
$this->taxTypes = $tax_type_storage->loadByProperties(['status' => TRUE]);
}
return $this->taxTypes;
}
}
