PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.27
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.27
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
fluent-cart / app / Services / Tax / TaxCalculator.php

TaxCalculator.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.27, at app/Services/Tax/TaxCalculator.php

272 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\Tax;
4
5 use FluentCart\App\Models\Cart;
6 use FluentCart\App\Models\Product;
7 use FluentCart\App\Models\TaxRate;
8 use FluentCart\App\Models\TaxClass;
9 use FluentCart\Framework\Support\Arr;
10 use FluentCart\App\Modules\Tax\TaxModule;
11
12 class TaxCalculator
13 {
14 protected $rates;
15 protected array $lineItems;
16
17 protected array $productTaxClassMap = [];
18
19 protected bool $isTaxIncludedWithPrice = false;
20
21 protected bool $roundTaxForEachItem = false;
22
23 protected float $totalTaxForItems = 0;
24 protected float $totalTaxForShipping = 0;
25
26 protected float $totalTax = 0;
27
28 protected string $country;
29 protected ?string $state;
30 protected ?string $city = null;
31 protected ?string $postCode = null;
32
33 /**
34 * @param array $lineItems
35 * @param string|null $country
36 * @param string|null $state
37 * @param string|null $city
38 * @param string|null $postCode
39 */
40 public function __construct(
41 array $lineItems,
42 string $country,
43 ?string $state = null,
44 ?string $city = null,
45 ?string $postCode = null
46 )
47 {
48 $this->lineItems = $lineItems;
49 $this->country = $country;
50 $this->state = $state;
51 $this->city = $city;
52 $this->postCode = $postCode;
53
54 $settings = (new TaxModule())->getSettings();
55
56 $this->isTaxIncludedWithPrice = Arr::get($settings, 'tax_inclusion') === 'included';
57 $this->roundTaxForEachItem = Arr::get($settings, 'tax_rounding') === 'item';
58
59 $productIds = array_column($this->lineItems, 'post_id');
60 $productIds = array_unique($productIds);
61 $products = Product::query()
62 ->select('ID')
63 ->with(['variants', 'wp_terms', 'detail'])
64 ->whereIn('ID', $productIds)
65 ->get()
66 ->keyBy('ID');
67
68
69 $this->productTaxClassMap = [];
70
71 $taxClasses = TaxClass::query()->whereNotNull('meta')->get()->filter(function ($taxClass) {
72 $meta = $taxClass->meta;
73 return isset($meta['categories']) && !empty($meta['categories']);
74 })->keyBy('id');
75 foreach ($products as $product) {
76 if ($product->detail->tax_class) {
77 $this->productTaxClassMap[$product->ID] = $product->detail->tax_class;
78 continue;
79 }
80 $categories = $product->wp_terms->pluck('term_taxonomy_id')->toArray();
81 //find the first tax class from $taxClasses if $taxClass->meta['categories'] contains any of $categories
82 foreach ($taxClasses as $taxClass) {
83 $taxCategories = $taxClass->meta['categories'] ?? []; // get categories from meta
84 if (array_intersect($categories, $taxCategories)) {
85 $this->productTaxClassMap[$product->ID] = $taxClass->id; // first match
86 break; // stop searching
87 }
88 }
89 }
90
91 $this->rates = $this->loadRates(
92 $this->country,
93 $this->state,
94 $this->city,
95 $this->postCode,
96 array_unique(array_values($this->productTaxClassMap))
97 );
98
99 $this->calculate();
100 }
101
102
103 public function lineItems(): array
104 {
105 return $this->lineItems;
106 }
107
108 public function calculate()
109 {
110 foreach ($this->lineItems as &$item) {
111 $this->calculateItemTax($item);
112 }
113
114 if (!$this->roundTaxForEachItem) {
115 $this->totalTaxForItems = round($this->totalTaxForItems, 2);
116 $this->totalTaxForShipping = round($this->totalTaxForShipping, 2);
117 $this->totalTax = round($this->totalTax, 2);
118 }
119 }
120
121 private function getTaxAmountFromPrice($totalPrice, $taxRate)
122 {
123 if ($this->isTaxIncludedWithPrice) {
124 $taxAmount = $totalPrice - ($totalPrice / (1 + $taxRate / 100));
125 } else {
126 $taxAmount = $totalPrice * ($taxRate / 100);
127 }
128 return $taxAmount;
129 }
130
131 private function calculateItemTax(&$item)
132 {
133 $taxClassId = Arr::get($this->productTaxClassMap, $item['post_id']);
134 if (empty($taxClassId)) {
135 return;
136 }
137 $taxRates = Arr::get($this->rates, $taxClassId);
138 if (empty($taxRates)) {
139 return;
140 }
141
142 $isShippingTaxCompound = false;
143 $isItemTaxCompound = false;
144
145 $taxRatesForItems = [];
146 $taxRatesForShipping = [];
147
148 //find is_compound is one in any of the tax rates
149 $totalTaxRateForItems = 0;
150 $totalTaxRateForShipping = 0;
151
152 ///for shipping column is skipped for now
153 foreach ($taxRates as $rate) {
154
155 if ($rate->for_shipping) {
156 $taxRatesForShipping[] = $rate;
157 $totalTaxRateForShipping += $rate->rate;
158 if ($rate->is_compound) {
159 $isShippingTaxCompound = true;
160 }
161 continue;
162 }
163
164 $taxRatesForItems[] = $rate;
165 $totalTaxRateForItems += $rate->rate;
166 if ($rate->is_compound) {
167 $isItemTaxCompound = true;
168 }
169 }
170
171 $taxAmountForItem = 0;
172 $totalPrice = (($item['unit_price'] * $item['quantity'])) - Arr::get($item, 'discount_total', 0);
173
174 if ($isItemTaxCompound) {
175 foreach ($taxRatesForItems as $rate) {
176 $tax = $this->getTaxAmountFromPrice($totalPrice, $rate->rate);
177
178 if($this->isTaxIncludedWithPrice){
179 $totalPrice -= $tax;
180 }else{
181 $totalPrice += $tax;
182 }
183
184 $taxAmountForItem += $tax;
185 }
186 } else {
187 $taxAmountForItem = $this->getTaxAmountFromPrice($totalPrice, $totalTaxRateForItems);
188 }
189
190
191 $shippingCharge = Arr::get($item, 'shipping_charge', 0);
192 $taxAmountForShipping = 0;
193 if($shippingCharge){
194 if($isShippingTaxCompound){
195 foreach ($taxRatesForShipping as $rate) {
196 $taxAmountForShipping += $shippingCharge * ($rate->rate / 100);
197 }
198 }else{
199 $taxAmountForShipping += $shippingCharge * ($totalTaxRateForShipping / 100);
200 }
201 }
202
203
204
205
206 if ($this->roundTaxForEachItem) {
207 $taxAmountForItem = round($taxAmountForItem, 2);
208 }
209
210 $this->totalTaxForItems += $taxAmountForItem;
211 $this->totalTaxForShipping = $totalTaxRateForShipping;
212 $this->totalTax = $this->totalTaxForItems + $this->totalTaxForShipping;
213
214 $item['tax_amount'] = $taxAmountForItem;
215 $item['shipping_tax_amount'] = $taxAmountForShipping;
216 }
217
218 protected function loadRates($country, $state = null, $city = null, $postCode = null, $taxClassIds = [])
219 {
220
221 $taxQuery = TaxRate::query()
222 ->where('country', $country);
223 if ($state) {
224 $taxQuery->where('state', $state);
225 } else {
226 $taxQuery->whereNull('state');
227 }
228
229 if (!empty($city)) {
230 $taxQuery->where('city', $city);
231 } else {
232 $taxQuery->where(function($q) {
233 $q->whereNull('city')->orWhere('city', '');
234 });
235 }
236
237 if (!empty($postCode)) {
238 $taxQuery->where('postcode', $postCode);
239 } else {
240 $taxQuery->where(function($q) {
241 $q->whereNull('postcode')->orWhere('postcode', '');
242 });
243 }
244
245 if (!empty($taxClassIds)) {
246 $taxQuery->whereIn('class_id', $taxClassIds);
247 }
248
249 return $taxQuery->orderBy('priority')->get()->groupBy('class_id'); // group by class_id for faster lookup
250
251 }
252
253 public static function calculateTaxForCart(Cart $cart, $country = '', $state = null, $city = null, $postCode = null): TaxCalculator
254 {
255 // $country = 'BD';
256 // $state = 'BD-05';
257 // $country = 'US';
258 // $state = 'AL';
259 $instance = new self($cart->cart_data, $country, $state, $city, $postCode);
260 $cart->cart_data = $instance->lineItems();
261 $cart->save();
262 return $instance;
263 }
264
265 public function getTotalTax(): float
266 {
267 return $this->totalTax;
268 }
269
270
271 }
272