PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.28
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.28
1.6.6 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 All 49 releases
fluent-cart / app / Services / Tax / TaxManager.php

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

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