PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.21
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.21
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 / TaxManager.php

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

412 lines 12.8 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
6 use FluentCart\App\App;
7 use FluentCart\Api\StoreSettings;
8 use FluentCart\App\Models\TaxRate;
9 use FluentCart\App\Models\TaxClass;
10 use FluentCart\Framework\Support\Arr;
11 use FluentCart\Framework\Support\Str;
12 use FluentCart\App\Helpers\AddressHelper;
13 use FluentCart\App\Modules\Tax\TaxModule;
14 use FluentCart\Framework\Support\Collection;
15 use FluentCart\App\Services\DateTime\DateTime;
16
17 class TaxManager
18 {
19 /**
20 * @var TaxManager|null
21 */
22 private static $instance = null;
23
24 /**
25 * @var array
26 */
27 private $rates = [];
28
29
30 /**
31 * @var array
32 */
33 private $config = [];
34
35
36 /**
37 * @var array
38 */
39 private array $descriptionMap = [];
40
41 /**
42 * Private constructor to prevent direct instantiation
43 */
44 private function __construct()
45 {
46 $this->descriptionMap = [
47 'standard' => __('Default tax class for most products.', 'fluent-cart'),
48 'zero' => __('For items with 0% tax.', 'fluent-cart'),
49 'reduced' => __('For items with a reduced tax rate.', 'fluent-cart'),
50 ];
51 $this->rates = require __DIR__ . '/tax.php';
52 $this->config = require __DIR__ . '/config.php';
53 }
54
55 /**
56 * Get the singleton instance
57 *
58 * @return TaxManager
59 */
60 public static function getInstance(): TaxManager
61 {
62 if (self::$instance === null) {
63 self::$instance = new self();
64 }
65 return self::$instance;
66 }
67
68 /**
69 * Get all tax rates
70 *
71 * @return array
72 */
73 public function getRates(): array
74 {
75 return $this->rates;
76 }
77
78 /**
79 * Generate human-readable label from a tax key
80 *
81 * @param string $key
82 * @return string
83 */
84 private function formatLabel(string $key): string
85 {
86 // Special mappings
87 $map = [
88 'standard' => __('Standard', 'fluent-cart'),
89 'zero' => __('Zero', 'fluent-cart'),
90 ];
91
92 if (isset($map[$key])) {
93 return $map[$key];
94 }
95
96 // If key ends with number (like reduced_1, reduced_2)
97 if (preg_match('/^(.+?)_(\d+)$/', $key, $matches)) {
98 $prefix = ucfirst(str_replace('_', ' ', $matches[1]));
99 $num = (int)$matches[2];
100
101 $numberMap = [
102 1 => __('One', 'fluent-cart'),
103 2 => __('Two', 'fluent-cart'),
104 3 => __('Three', 'fluent-cart'),
105 4 => __('Four', 'fluent-cart'),
106 5 => __('Five', 'fluent-cart'),
107 6 => __('Six', 'fluent-cart'),
108 7 => __('Seven', 'fluent-cart'),
109 8 => __('Eight', 'fluent-cart'),
110 9 => __('Nine', 'fluent-cart'),
111 10 => __('Ten', 'fluent-cart'),
112 11 => __('Eleven', 'fluent-cart'),
113 12 => __('Twelve', 'fluent-cart'),
114 13 => __('Thirteen', 'fluent-cart'),
115 14 => __('Fourteen', 'fluent-cart'),
116 15 => __('Fifteen', 'fluent-cart')
117 ];
118
119 return $prefix . ' ' . ($numberMap[$num] ?? $num);
120 }
121
122 // Default: convert snake_case → words
123 return ucwords(str_replace('_', ' ', $key));
124 }
125
126 /**
127 * Iterate all countries and collect all unique tax labels
128 *
129 * @return array
130 */
131 public function generateAllTaxLabels($only = []): array
132 {
133 $labels = [];
134
135 $rates = $this->rates;
136
137 if (!empty($only)) {
138 $rates = Arr::only($rates, $only);
139 }
140
141 foreach ($rates as $country => $data) {
142 if (!isset($data['tax'])) {
143 continue;
144 }
145
146 foreach (array_keys($data['tax']) as $key) {
147 if (!isset($labels[$key])) {
148 $labels[$key] = $this->formatLabel($key);
149 }
150 }
151 }
152
153 return $labels;
154 }
155
156 public function generateTaxClasses($only = [])
157 {
158
159 $taxClassLabels = [
160 'standard' => __('Standard', 'fluent-cart'),
161 'reduced' => __('Reduced', 'fluent-cart'),
162 'zero' => __('Zero', 'fluent-cart'),
163 ];
164
165 $taxClassIds = [];
166
167 foreach ($taxClassLabels as $key => $label) {
168 $description = $this->descriptionMap[$key];
169 $priority = $key === 'standard' ? 10 : ($key === 'reduced' ? 5 : 2);
170 $taxClass = TaxClass::query()->firstOrCreate(
171 ['title' => $label], // search by title
172 [
173 'slug' => $key,
174 'description' => $description,
175 'meta' => [
176 'categories' => [],
177 'priority' => $priority,
178 ]
179 ]
180 );
181
182 $taxClassIds[$key] = $taxClass->id;
183 }
184
185 $ratesMap = [];
186
187 $rates = $this->rates;
188
189 if (!empty($only)) {
190 $rates = Arr::only($rates, $only);
191 }
192
193 // Get existing countries already in tax_rates
194 $existingCountries = TaxRate::query()
195 ->pluck('country')
196 ->unique()
197 ->toArray();
198
199 foreach ($rates as $country => $data) {
200 if (in_array($country, $existingCountries)) {
201 // Skip if this country already exists
202 continue;
203 }
204
205 if (!isset($data['tax'] )) {
206 continue;
207 }
208
209 foreach ($data['tax'] as $key => $rate) {
210 $compound = $rate['compound'] ?? false;
211 $shipping = $rate['shipping'] ?? false;
212
213 $typeKey = $rate['type'] ?? explode('_', $key)[0];
214
215
216 $ratesMap[] = [
217 'country' => $country,
218 'name' => $rate['name'] ?? $country . ' ' . $taxClassLabels[$typeKey] . ' Tax',
219 'class_id' => $taxClassIds[$typeKey],
220 'rate' => $rate['rate'],
221 'is_compound' => $compound ? 1 : 0,
222 'group' => $data['group'] ?? '',
223 'for_shipping' => null,
224 'state' => $rate['state'] ?? '',
225 'city' => $rate['city'] ?? '',
226 ];
227 //$this->rates[$country]['tax'][$key]['tax_class_id'] = $idMap[$key] ?? null;
228 }
229 }
230
231 TaxRate::query()->insert($ratesMap);
232 }
233
234
235 public function getEuTaxRatesFromPhp(string $country = '', $taxClassSlug = ''): array
236 {
237 if (!empty($country)) {
238 $countryData = $this->rates[$country] ?? null;
239 $rates = Arr::get($countryData, 'tax', []);
240 if (empty($taxClassSlug)) {
241 return $rates;
242 }
243 $rates = array_filter($rates, function ($tax) use ($taxClassSlug) {
244 return $tax['type'] === $taxClassSlug;
245 });
246 return $rates;
247 }
248
249 $formattedData = [];
250 foreach ($this->rates as $countryCode => $rate) {
251 if (isset($rate['group']) && $rate['group'] === 'EU' && isset($rate['tax'])) {
252 $formattedData[$countryCode] = $rate['tax'];
253 }
254 }
255
256 return $formattedData;
257 }
258
259 public function getTaxRatesFromTaxPhp(): array
260 {
261 $rates = $this->rates;
262 $formattedData = [];
263
264 foreach ($rates as $countryCode => $countries) {
265 $group = $countries['group'];
266
267 $countryName = AddressHelper::getCountryNameByCode($countryCode);
268
269 if (!isset($formattedData[$group])) {
270 $localization = App::localization();
271 $continent = $localization->continents($group);
272
273 if ($group === 'EU') {
274 $groupName = __('European Union', 'fluent-cart');
275 } else {
276 $groupName = Arr::get($continent, 'name') ?? __('Rest of the World', 'fluent-cart');
277 }
278
279 $formattedData[$group] = [
280 'group_name' => $groupName,
281 'group_code' => $group,
282 'countries' => [],
283 'total_countries' => 0
284 ];
285 }
286
287 $formattedData[$group]['countries'][] = [
288 'country_code' => $countryCode,
289 'country_name' => $countryName,
290 'total_rates' => count($countries['tax']),
291 'rates' => $countries['tax']
292 ];
293
294 $formattedData[$group]['total_countries'] += 1;
295 }
296 return $formattedData;
297 }
298
299 public function getTaxRates(): array
300 {
301 $taxRates = TaxRate::query()->select('group', 'country', 'name', 'rate', 'class_id')
302 ->orderBy('group')
303 ->orderBy('country')
304 ->orderBy('class_id')
305 ->get();
306
307 $groupedTaxRates = $this->groupTaxRatesByGroup($taxRates);
308
309 return $groupedTaxRates;
310 }
311
312
313 public function groupTaxRatesByGroup($taxRates): array
314 {
315 $grouped = [];
316
317 foreach ($taxRates as $rate) {
318 $localization = App::localization();
319 $continent = $localization->continents($rate->group);
320 $groupName = Arr::get($continent, 'name') ?? __('Other', 'fluent-cart');
321 $countryCode = $rate->country;
322 $countryName = AddressHelper::getCountryNameByCode($countryCode);
323
324 // Initialize group
325 if (!isset($grouped[$groupName])) {
326 $grouped[$groupName] = [
327 'group_name' => $groupName,
328 'group_code' => $rate->group,
329 'countries' => [],
330 'total_countries' => 0
331 ];
332 }
333
334 // Initialize country
335 if (!isset($grouped[$groupName]['countries'][$countryCode])) {
336 $grouped[$groupName]['countries'][$countryCode] = [
337 'country_code' => $countryCode,
338 'country_name' => $countryName,
339 'rates' => [],
340 'total_rates' => 0
341 ];
342 }
343
344 // Add rate
345 $grouped[$groupName]['countries'][$countryCode]['rates'][] = [
346 'class_id' => $rate->class_id,
347 'name' => $rate->name,
348 'rate' => $rate->rate,
349 'for_shipping' => $rate->for_shipping
350 ];
351 }
352
353 // Format result
354 $result = [];
355 foreach ($grouped as $groupName => $groupData) {
356 $countries = [];
357 foreach ($groupData['countries'] as $countryData) {
358 $countryData['total_rates'] = count($countryData['rates']);
359 $countries[] = $countryData;
360 }
361
362 $result[] = [
363 'group_name' => $groupName,
364 'group_code' => $groupData['group_code'],
365 'countries' => $countries,
366 'total_countries' => count($countries)
367 ];
368 }
369
370 return $result;
371 }
372
373 public function getCountryConfiguration(string $countryCode)
374 {
375 $config = Arr::get($this->config, 'countries.' . $countryCode);
376
377 if(!empty($config)){
378 return $config;
379 }
380
381 $group = Arr::get($this->rates, $countryCode . '.group');
382 return Arr::get($this->config, 'continents.' . $group);
383 }
384
385 public function calculateTotalCartTax()
386 {
387 $getCart = \FluentCart\App\Helpers\CartHelper::getCart();
388
389 $taxSettings = (new TaxModule())->getSettings();
390
391 if (Arr::get($taxSettings, 'tax_calculation_basis') === 'store') {
392 $country = (new StoreSettings())->get('store_country') ?? '';
393 $state = (new StoreSettings())->get('store_state') ?? null;
394 $city = (new StoreSettings())->get('store_city') ?? null;
395 $postCode = (new StoreSettings())->get('store_postcode') ?? null;
396 } else if (Arr::get($taxSettings, 'tax_calculation_basis') === 'billing') {
397 $country = Arr::get($getCart, 'checkout_data.form_data.billing_country') ?? '';
398 $state = Arr::get($getCart, 'checkout_data.form_data.billing_state') ?? null;
399 $city = Arr::get($getCart, 'checkout_data.form_data.billing_city') ?? null;
400 $postCode = Arr::get($getCart, 'checkout_data.form_data.billing_postcode') ?? null;
401 } else {
402 $country = Arr::get($getCart, 'checkout_data.form_data.shipping_country') ?? '';
403 $state = Arr::get($getCart, 'checkout_data.form_data.shipping_state') ?? null;
404 $city = Arr::get($getCart, 'checkout_data.form_data.shipping_city') ?? null;
405 $postCode = Arr::get($getCart, 'checkout_data.form_data.shipping_postcode') ?? null;
406 }
407
408 $calculator = TaxCalculator::calculateTaxForCart($getCart, $country, $state, $city, $postCode);
409 return $calculator->getTotalTax();
410 }
411 }
412