33namespace Drupal \commerce_tax ;
44
55use Drupal \commerce_order \Entity \OrderInterface ;
6+ use Drupal \commerce_order \Entity \OrderItemInterface ;
67use Drupal \commerce_order \OrderProcessorInterface ;
8+ use Drupal \commerce_price \RounderInterface ;
9+ use Drupal \commerce_store \Entity \StoreInterface ;
10+ use Drupal \commerce_tax \Entity \TaxTypeInterface ;
11+ use Drupal \commerce_tax \Plugin \Commerce \TaxType \LocalTaxTypeInterface ;
12+ use Drupal \commerce_tax \Resolver \ChainTaxRateResolverInterface ;
713use Drupal \Core \Entity \EntityTypeManagerInterface ;
814
915/**
@@ -18,49 +24,192 @@ class TaxOrderProcessor implements OrderProcessorInterface {
1824 */
1925 protected $ entityTypeManager ;
2026
27+ /**
28+ * The rounder.
29+ *
30+ * @var \Drupal\commerce_price\RounderInterface
31+ */
32+ protected $ rounder ;
33+
34+ /**
35+ * The chain tax rate resolver.
36+ *
37+ * @var \Drupal\commerce_tax\Resolver\ChainTaxRateResolverInterface
38+ */
39+ protected $ chainRateResolver ;
40+
41+ /**
42+ * The store's tax zones, keyed by store ID.
43+ *
44+ * @var array
45+ */
46+ protected $ storeZones = [];
47+
48+ /**
49+ * The loaded tax types.
50+ *
51+ * @var \Drupal\commerce_tax\Entity\TaxTypeInterface[]
52+ */
53+ protected $ taxTypes = [];
54+
55+ /**
56+ * A cache of instantiated store profiles.
57+ *
58+ * @var \Drupal\profile\Entity\ProfileInterface
59+ */
60+ protected $ storeProfiles = [];
61+
2162 /**
2263 * Constructs a new TaxOrderProcessor object.
2364 *
2465 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
2566 * The entity type manager.
67+ * @param \Drupal\commerce_price\RounderInterface $rounder
68+ * The rounder.
69+ * @param \Drupal\commerce_tax\Resolver\ChainTaxRateResolverInterface $chain_rate_resolver
70+ * The chain tax rate resolver.
2671 */
27- public function __construct (EntityTypeManagerInterface $ entity_type_manager ) {
72+ public function __construct (EntityTypeManagerInterface $ entity_type_manager, RounderInterface $ rounder , ChainTaxRateResolverInterface $ chain_rate_resolver ) {
2873 $ this ->entityTypeManager = $ entity_type_manager ;
74+ $ this ->rounder = $ rounder ;
75+ $ this ->chainRateResolver = $ chain_rate_resolver ;
2976 }
3077
3178 /**
3279 * {@inheritdoc}
3380 */
3481 public function process (OrderInterface $ order ) {
35- $ tax_type_storage = $ this ->entityTypeManager ->getStorage ('commerce_tax_type ' );
36- /** @var \Drupal\commerce_tax\Entity\TaxTypeInterface[] $tax_types */
37- $ tax_types = $ tax_type_storage ->loadMultiple ();
82+ $ tax_types = $ this ->getTaxTypes ();
3883 foreach ($ tax_types as $ tax_type ) {
39- if ($ tax_type ->status () && $ tax_type -> getPlugin ()->applies ($ order )) {
84+ if ($ tax_type ->getPlugin ()->applies ($ order )) {
4085 $ tax_type ->getPlugin ()->apply ($ order );
4186 }
4287 }
43- // Tax types can create a negative adjustment when a price includes
44- // tax, but the customer is tax-exempt. These negative adjustments
45- // are removed and applied directly to the unit price, so that the
46- // customer always sees the actual price they are being charged.
47- // @todo Figure out if this conversion should be optional/configurable.
48- if ($ order ->getStore ()->get ('prices_include_tax ' )->value ) {
88+ // Don't overcharge a tax-exempt customer if the price is tax-inclusive.
89+ // For example, a 12 EUR price with 20% EU VAT gets reduced to 10 EUR
90+ // when selling to customers outside the EU, but only if no other tax
91+ // was applied (e.g. a Japanese customer paying Japanese tax due to the
92+ // store being registered to collect tax there).
93+ $ store = $ order ->getStore ();
94+ if ($ store ->get ('prices_include_tax ' )->value ) {
4995 foreach ($ order ->getItems () as $ order_item ) {
50- $ adjustments = $ order_item ->getAdjustments ();
51- $ negative_tax_adjustments = array_filter ($ adjustments , function ($ adjustment ) {
96+ $ tax_adjustments = array_filter ($ order_item ->getAdjustments (), function ($ adjustment ) {
5297 /** @var \Drupal\commerce_order\Adjustment $adjustment */
53- return $ adjustment ->getType () == 'tax ' && $ adjustment -> isNegative () ;
98+ return $ adjustment ->getType () == 'tax ' ;
5499 });
55- $ adjustments = array_diff_key ($ adjustments , $ negative_tax_adjustments );
56- $ unit_price = $ order_item ->getUnitPrice ();
57- foreach ($ negative_tax_adjustments as $ adjustment ) {
58- $ unit_price = $ unit_price ->add ($ adjustment ->getAmount ());
100+ if (empty ($ tax_adjustments )) {
101+ $ unit_price = $ order_item ->getUnitPrice ();
102+ $ rates = $ this ->getDefaultRates ($ order_item , $ store );
103+ foreach ($ rates as $ rate ) {
104+ $ percentage = $ rate ->getPercentage ();
105+ $ tax_amount = $ percentage ->calculateTaxAmount ($ order_item ->getUnitPrice (), TRUE );
106+ $ tax_amount = $ this ->rounder ->round ($ tax_amount );
107+ $ unit_price = $ unit_price ->subtract ($ tax_amount );
108+ }
109+ $ order_item ->setUnitPrice ($ unit_price );
110+ }
111+ }
112+ }
113+ }
114+
115+ /**
116+ * Gets the default tax rates for the given order item and store.
117+ *
118+ * @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
119+ * The order item.
120+ * @param \Drupal\commerce_store\Entity\StoreInterface $store
121+ * The store.
122+ *
123+ * @return \Drupal\commerce_tax\TaxRate[]
124+ * The tax rates, keyed by tax zone ID.
125+ */
126+ protected function getDefaultRates (OrderItemInterface $ order_item , StoreInterface $ store ) {
127+ $ store_profile = $ this ->buildStoreProfile ($ store );
128+ $ rates = [];
129+ foreach ($ this ->getStoreZones ($ store ) as $ zone ) {
130+ $ rate = $ this ->chainRateResolver ->resolve ($ zone , $ order_item , $ store_profile );
131+ if (is_object ($ rate )) {
132+ $ rates [$ zone ->getId ()] = $ rate ;
133+ }
134+ }
135+
136+ return $ rates ;
137+ }
138+
139+ /**
140+ * Gets the tax zones for the given store.
141+ *
142+ * @param \Drupal\commerce_store\Entity\StoreInterface $store
143+ * The store.
144+ *
145+ * @return \Drupal\commerce_tax\TaxZone[]
146+ * The tax zones.
147+ */
148+ protected function getStoreZones (StoreInterface $ store ) {
149+ $ store_id = $ store ->id ();
150+ if (!isset ($ this ->storeZones [$ store_id ])) {
151+ $ tax_types = $ this ->getTaxTypes ();
152+ $ tax_types = array_filter ($ tax_types , function (TaxTypeInterface $ tax_type ) {
153+ $ tax_type_plugin = $ tax_type ->getPlugin ();
154+ return ($ tax_type_plugin instanceof LocalTaxTypeInterface) && $ tax_type_plugin ->isDisplayInclusive ();
155+ });
156+
157+ $ this ->storeZones [$ store_id ] = [];
158+ $ store_address = $ store ->getAddress ();
159+ foreach ($ tax_types as $ tax_type ) {
160+ /** @var \Drupal\commerce_tax\Plugin\Commerce\TaxType\LocalTaxTypeInterface $tax_type_plugin */
161+ $ tax_type_plugin = $ tax_type ->getPlugin ();
162+ foreach ($ tax_type_plugin ->getZones () as $ zone ) {
163+ if ($ zone ->match ($ store_address )) {
164+ $ this ->storeZones [$ store_id ][] = $ zone ;
165+ }
166+ }
167+ // Assume that only a single tax type's zones will match.
168+ if (count ($ this ->storeZones [$ store_id ]) > 0 ) {
169+ break ;
59170 }
60- $ order_item ->setUnitPrice ($ unit_price );
61- $ order_item ->setAdjustments ($ adjustments );
62171 }
63172 }
173+
174+ return $ this ->storeZones [$ store_id ];
175+ }
176+
177+ /**
178+ * Builds a customer profile for the given store.
179+ *
180+ * @param \Drupal\commerce_store\Entity\StoreInterface $store
181+ * The store.
182+ *
183+ * @return \Drupal\profile\Entity\ProfileInterface
184+ * The customer profile.
185+ */
186+ protected function buildStoreProfile (StoreInterface $ store ) {
187+ $ store_id = $ store ->id ();
188+ if (!isset ($ this ->storeProfiles [$ store_id ])) {
189+ $ profile_storage = $ this ->entityTypeManager ->getStorage ('profile ' );
190+ $ this ->storeProfiles [$ store_id ] = $ profile_storage ->create ([
191+ 'type ' => 'customer ' ,
192+ 'uid ' => $ store ->getOwnerId (),
193+ 'address ' => $ store ->getAddress (),
194+ ]);
195+ }
196+
197+ return $ this ->storeProfiles [$ store_id ];
198+ }
199+
200+ /**
201+ * Gets the available tax types.
202+ *
203+ * @return \Drupal\commerce_tax\Entity\TaxTypeInterface[]
204+ * The tax types.
205+ */
206+ protected function getTaxTypes () {
207+ if (empty ($ this ->taxTypes )) {
208+ $ tax_type_storage = $ this ->entityTypeManager ->getStorage ('commerce_tax_type ' );
209+ $ this ->taxTypes = $ tax_type_storage ->loadByProperties (['status ' => TRUE ]);
210+ }
211+
212+ return $ this ->taxTypes ;
64213 }
65214
66215}
0 commit comments