| @@ -1,11 +1,13 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | namespace FluentCart\App\Http\Controllers; |
| 4 | 4 | |
| 5 | +use FluentCart\App\Models\Meta; | |
| 5 | 6 | use FluentCart\App\Models\TaxRate; |
| 6 | 7 | use FluentCart\App\Models\TaxClass; |
| 7 | 8 | use FluentCart\Framework\Support\Arr; |
| 9 | +use FluentCart\Framework\Support\Collection; | |
| 8 | 10 | use FluentCart\App\Modules\Tax\TaxModule; |
| 9 | 11 | use FluentCart\App\Services\Tax\TaxManager; |
| 10 | 12 | use FluentCart\Framework\Http\Request\Request; |
| 11 | 13 | |
| @@ -12,217 +14,393 @@ | ||
| 12 | 14 | class TaxEUController extends Controller |
| 13 | 15 | { |
| 14 | 16 | public function saveEuVatSettings(Request $request) |
| 15 | 17 | { |
| 16 | - if ($request->getSafe('action', 'sanitize_text_field') === 'euCrossBorderSettings') { | |
| 18 | + $action = $request->getSafe('action', 'sanitize_text_field'); | |
| 19 | + | |
| 20 | + if ($action === 'euCrossBorderSettings') { | |
| 17 | 21 | return $this->euCrossBorderSettings($request); |
| 22 | + } else if ($action === 'saveCountryRegistration') { | |
| 23 | + return $this->saveCountryRegistration($request); | |
| 24 | + } else if ($action === 'deleteCountryRegistration') { | |
| 25 | + return $this->deleteCountryRegistration($request); | |
| 18 | 26 | } else { |
| 19 | 27 | return $this->sendError([ |
| 20 | 28 | 'message' => __('Invalid method', 'fluent-cart') |
| 21 | - ], 423); | |
| 29 | + ], 422); | |
| 22 | 30 | } |
| 23 | 31 | } |
| 24 | 32 | |
| 25 | - public function getEuTaxRates(Request $request) | |
| 33 | + public function saveCountryRegistration(Request $request) | |
| 26 | 34 | { |
| 27 | - $taxRates = TaxRate::query()->where('group', 'EU')->select('group', 'country', 'name', 'rate', 'class_id','for_shipping') | |
| 28 | - ->orderBy('group') | |
| 29 | - ->orderBy('country') | |
| 30 | - ->orderBy('class_id') | |
| 31 | - ->get(); | |
| 35 | + $countryCode = $this->sanitizeCountryCode($request->get('country', '')); | |
| 36 | + $vat = $request->getSafe('vat', 'sanitize_text_field'); | |
| 37 | + $rawRates = $request->get('rates', []); | |
| 38 | + $rateErrors = []; | |
| 32 | 39 | |
| 33 | - $taxManager = TaxManager::getInstance(); | |
| 34 | - $groupedTaxRates = $taxManager->groupTaxRatesByGroup($taxRates); | |
| 40 | + // Normalise rates: each entry is either {rate, label} or a legacy plain float | |
| 41 | + $rates = []; | |
| 42 | + if (is_array($rawRates)) { | |
| 43 | + foreach ($rawRates as $slug => $rateData) { | |
| 44 | + $cleanSlug = sanitize_key($slug); | |
| 45 | + if (!$cleanSlug) { | |
| 46 | + continue; | |
| 47 | + } | |
| 48 | + if (is_array($rateData)) { | |
| 49 | + $rates[$cleanSlug] = [ | |
| 50 | + 'rate' => floatval($rateData['rate'] ?? 0), | |
| 51 | + 'label' => sanitize_text_field($rateData['label'] ?? ''), | |
| 52 | + ]; | |
| 53 | + } else { | |
| 54 | + // Legacy plain-number format | |
| 55 | + $rates[$cleanSlug] = ['rate' => floatval($rateData), 'label' => '']; | |
| 56 | + } | |
| 57 | + } | |
| 58 | + } | |
| 35 | 59 | |
| 36 | - return $this->sendSuccess([ | |
| 37 | - 'tax_rates' => $groupedTaxRates | |
| 38 | - ]); | |
| 39 | - } | |
| 60 | + // Legacy single-rate fallback (old clients send rate= not rates=) | |
| 61 | + if (empty($rates)) { | |
| 62 | + $legacyRate = floatval($request->get('rate', 0)); | |
| 63 | + if ($legacyRate > 0) { | |
| 64 | + $rates['standard'] = ['rate' => $legacyRate, 'label' => sanitize_text_field($request->get('tax_label', ''))]; | |
| 65 | + } | |
| 66 | + } | |
| 40 | 67 | |
| 41 | - public function saveOssTaxOverride(Request $request) | |
| 42 | - { | |
| 43 | - $newOssTaxOverride = $request->get('overrides', []); | |
| 44 | - $countryCode = $request->get('country_code'); | |
| 68 | + if (!$countryCode) { | |
| 69 | + return $this->sendError([ | |
| 70 | + 'message' => __('Select a registration country', 'fluent-cart'), | |
| 71 | + ], 422); | |
| 72 | + } | |
| 45 | 73 | |
| 46 | - $errors = []; | |
| 47 | - if (!$countryCode) { | |
| 48 | - $errors['country_code'] = __('Select country of OSS registration', 'fluent-cart'); | |
| 74 | + if (!$this->isEuVatCountry($countryCode)) { | |
| 75 | + return $this->sendError([ | |
| 76 | + 'message' => __('Select a valid EU VAT registration country', 'fluent-cart'), | |
| 77 | + 'errors' => [ | |
| 78 | + 'country' => __('Select a valid EU VAT registration country', 'fluent-cart') | |
| 79 | + ] | |
| 80 | + ], 422); | |
| 49 | 81 | } |
| 50 | 82 | |
| 51 | - if ($errors) { | |
| 83 | + if (strlen($vat) > 50) { | |
| 52 | 84 | return $this->sendError([ |
| 53 | - 'message' => __('Validation failed for OSS tax override', 'fluent-cart'), | |
| 54 | - 'errors' => $errors | |
| 55 | - ], 423); | |
| 85 | + 'message' => __('VAT number is too long', 'fluent-cart'), | |
| 86 | + ], 422); | |
| 56 | 87 | } |
| 57 | 88 | |
| 58 | - $taxClassesFromType = TaxClass::query()->whereIn('slug', ['standard', 'reduced', 'zero'])->get()->mapWithKeys(function ($item) { | |
| 59 | - return [$item->slug => $item->id]; | |
| 60 | - })->toArray(); | |
| 61 | - | |
| 89 | + $hasNonZeroRate = false; | |
| 90 | + foreach ($rates as $rateData) { | |
| 91 | + if (floatval($rateData['rate'] ?? 0) > 0) { | |
| 92 | + $hasNonZeroRate = true; | |
| 93 | + break; | |
| 94 | + } | |
| 95 | + } | |
| 62 | 96 | |
| 63 | - foreach ($newOssTaxOverride as $override) { | |
| 64 | - if (!isset($override['type']) || !isset($taxClassesFromType[$override['type']])) { | |
| 65 | - continue; | |
| 97 | + if (!$hasNonZeroRate) { | |
| 98 | + return $this->sendError([ | |
| 99 | + 'message' => __('At least one tax rate must be greater than 0%', 'fluent-cart'), | |
| 100 | + ], 422); | |
| 101 | + } | |
| 102 | + | |
| 103 | + $requestedClassSlugs = array_keys($rates); | |
| 104 | + $taxClasses = TaxClass::query() | |
| 105 | + ->whereIn('slug', $requestedClassSlugs) | |
| 106 | + ->get() | |
| 107 | + ->keyBy('slug'); | |
| 108 | + | |
| 109 | + foreach ($requestedClassSlugs as $classSlug) { | |
| 110 | + if (!$taxClasses->has($classSlug)) { | |
| 111 | + /* translators: %1$s: tax class slug (e.g. standard, reduced) */ | |
| 112 | + $rateErrors['rates.' . $classSlug] = sprintf( | |
| 113 | + __('Tax class "%1$s" could not be found. Create the class first and try again.', 'fluent-cart'), | |
| 114 | + $classSlug | |
| 115 | + ); | |
| 66 | 116 | } |
| 117 | + } | |
| 67 | 118 | |
| 68 | - $taxClassId = $taxClassesFromType[$override['type']]; | |
| 119 | + if ($rateErrors) { | |
| 120 | + return $this->sendError([ | |
| 121 | + 'message' => __('Validation failed for VAT registration rates', 'fluent-cart'), | |
| 122 | + 'errors' => $rateErrors, | |
| 123 | + ], 422); | |
| 124 | + } | |
| 69 | 125 | |
| 70 | - $existingTaxRate = TaxRate::query()->where('country', $countryCode)->where('group', 'EU')-> | |
| 71 | - where('class_id', intval($taxClassId))->first(); | |
| 126 | + $standardRate = floatval($rates['standard']['rate'] ?? 0); | |
| 127 | + $standardLabel = sanitize_text_field($rates['standard']['label'] ?? ''); | |
| 72 | 128 | |
| 129 | + TaxManager::getInstance()->saveEuVatRegistration($countryCode, [ | |
| 130 | + 'country' => $countryCode, | |
| 131 | + 'vat' => $vat, | |
| 132 | + 'rate' => $standardRate, | |
| 133 | + 'rates' => $rates, | |
| 134 | + 'tax_label' => $standardLabel, | |
| 135 | + ]); | |
| 73 | 136 | |
| 74 | - $overrideRate = Arr::get($override, 'rate'); | |
| 75 | - if ($existingTaxRate) { | |
| 76 | - $existingTaxRate->rate = $overrideRate; | |
| 77 | - $existingTaxRate->save(); | |
| 78 | - } else { | |
| 79 | - TaxRate::query()->create([ | |
| 80 | - 'country' => $countryCode, | |
| 81 | - 'name' => $override['type'], | |
| 82 | - 'rate' => $overrideRate, | |
| 83 | - 'group' => 'EU', | |
| 84 | - 'class_id' => intval($taxClassId), | |
| 85 | - 'tax_rate' => $overrideRate | |
| 86 | - ]); | |
| 87 | - } | |
| 137 | + $currentSettings = (new TaxModule())->getSettings(); | |
| 138 | + $euVatSettings = Arr::get($currentSettings, 'eu_vat_settings', []); | |
| 139 | + if (Arr::get($euVatSettings, 'method') === 'home' && Arr::get($euVatSettings, 'home_country') === $countryCode) { | |
| 140 | + $euVatSettings['home_vat'] = $vat; | |
| 141 | + $currentSettings['eu_vat_settings'] = $euVatSettings; | |
| 142 | + update_option('fluent_cart_tax_configuration_settings', $currentSettings, true); | |
| 88 | 143 | } |
| 89 | 144 | |
| 90 | 145 | return $this->sendSuccess([ |
| 91 | - 'message' => __('OSS tax override saved successfully', 'fluent-cart') | |
| 146 | + 'message' => __('Country VAT registration saved successfully', 'fluent-cart') | |
| 92 | 147 | ]); |
| 93 | 148 | } |
| 94 | 149 | |
| 95 | - public function saveOssShippingOverride(Request $request) | |
| 150 | + public function deleteCountryRegistration(Request $request) | |
| 96 | 151 | { |
| 97 | - $newOssTaxOverride = $request->get('overrides', []); | |
| 98 | - $countryCode = $request->get('country_code'); | |
| 152 | + $countryCode = $this->sanitizeCountryCode($request->get('country', '')); | |
| 99 | 153 | |
| 100 | - $errors = []; | |
| 101 | 154 | if (!$countryCode) { |
| 102 | - $errors['country_code'] = __('Select country of OSS registration', 'fluent-cart'); | |
| 155 | + return $this->sendError([ | |
| 156 | + 'message' => __('Country code is required', 'fluent-cart'), | |
| 157 | + ], 422); | |
| 103 | 158 | } |
| 104 | 159 | |
| 105 | - if ($errors) { | |
| 160 | + if (!$this->isEuVatCountry($countryCode)) { | |
| 106 | 161 | return $this->sendError([ |
| 107 | - 'message' => __('Validation failed for OSS tax override', 'fluent-cart'), | |
| 108 | - 'errors' => $errors | |
| 109 | - ], 423); | |
| 162 | + 'message' => __('Select a valid EU VAT registration country', 'fluent-cart'), | |
| 163 | + ], 422); | |
| 110 | 164 | } |
| 111 | 165 | |
| 112 | - $taxClassesFromType = TaxClass::query()->whereIn('slug', ['standard', 'reduced', 'zero'])->get()->mapWithKeys(function ($item) { | |
| 113 | - return [$item->slug => $item->id]; | |
| 114 | - })->toArray(); | |
| 115 | - | |
| 166 | + TaxManager::getInstance()->deleteEuVatRegistration($countryCode); | |
| 116 | 167 | |
| 117 | - foreach ($newOssTaxOverride as $override) { | |
| 118 | - if (!isset($override['type']) || !isset($taxClassesFromType[$override['type']])) { | |
| 119 | - continue; | |
| 120 | - } | |
| 168 | + return $this->sendSuccess([ | |
| 169 | + 'message' => __('Country registration removed successfully', 'fluent-cart') | |
| 170 | + ]); | |
| 171 | + } | |
| 121 | 172 | |
| 122 | - $taxClassId = $taxClassesFromType[$override['type']]; | |
| 173 | + public function getOssCountryRates() | |
| 174 | + { | |
| 175 | + $euCountries = TaxModule::euVatCountyOptions(); | |
| 123 | 176 | |
| 124 | - $existingTaxRate = TaxRate::query()->where('country', $countryCode)->where('group', 'EU')-> | |
| 125 | - where('class_id', intval($taxClassId))->first(); | |
| 177 | + // Get all tax classes | |
| 178 | + $taxClasses = TaxClass::query()->orderBy('id', 'ASC')->get(); | |
| 179 | + $classMap = $taxClasses->keyBy('slug'); | |
| 126 | 180 | |
| 181 | + // Load all EU rates grouped by country then class_id | |
| 182 | + $allDbRates = TaxRate::query() | |
| 183 | + ->where('group', 'EU') | |
| 184 | + ->where(function ($q) { | |
| 185 | + $q->whereNull('state')->orWhere('state', ''); | |
| 186 | + }) | |
| 187 | + ->get() | |
| 188 | + ->groupBy('country'); | |
| 127 | 189 | |
| 128 | - if ($existingTaxRate) { | |
| 129 | - $existingTaxRate->rate = $override['rate']; | |
| 130 | - $existingTaxRate->save(); | |
| 131 | - } else { | |
| 132 | - TaxRate::query()->create([ | |
| 133 | - 'country' => $countryCode, | |
| 134 | - 'name' => $override['type'], | |
| 135 | - 'rate' => $override['rate'], | |
| 136 | - 'for_shipping' => $override['for_shipping'] ?? 0, | |
| 137 | - 'group' => 'EU', | |
| 138 | - 'class_id' => intval($taxClassId), | |
| 139 | - 'tax_rate' => $override['rate'] | |
| 140 | - ]); | |
| 190 | + $rates = []; | |
| 191 | + foreach ($euCountries as $country) { | |
| 192 | + $code = $country['value']; | |
| 193 | + $countryDbRates = $allDbRates->get($code, new Collection())->keyBy('class_id'); | |
| 194 | + $defaultRates = $country['default_rates'] ?? []; | |
| 195 | + | |
| 196 | + $classRates = []; | |
| 197 | + foreach ($taxClasses as $tc) { | |
| 198 | + $dbRate = $countryDbRates->get($tc->id); | |
| 199 | + $defaultRate = $defaultRates[$tc->slug] ?? 0; | |
| 200 | + // Strip auto-generated fallback names like "AT Reduced Tax" → show as empty so UI shows placeholder | |
| 201 | + $dbLabel = ''; | |
| 202 | + if ($dbRate && $dbRate->name && !preg_match('/^[A-Z]{2} \w+ Tax$/', $dbRate->name)) { | |
| 203 | + $dbLabel = $dbRate->name; | |
| 204 | + } | |
| 205 | + $classRates[$tc->slug] = [ | |
| 206 | + 'rate' => $dbRate ? (float) $dbRate->rate : ($defaultRate ?? 0), | |
| 207 | + 'default_rate' => $defaultRate ?? 0, | |
| 208 | + 'has_custom' => (bool) $dbRate, | |
| 209 | + 'label' => $dbLabel, | |
| 210 | + ]; | |
| 141 | 211 | } |
| 212 | + | |
| 213 | + // Tax label for top-level (from standard class, for backward compat) | |
| 214 | + $standardClassId = $classMap->has('standard') ? $classMap->get('standard')->id : 1; | |
| 215 | + $standardDbRate = $countryDbRates->get($standardClassId); | |
| 216 | + $taxLabel = $classRates['standard']['label'] ?? 'VAT'; | |
| 217 | + if (!$taxLabel) $taxLabel = 'VAT'; | |
| 218 | + | |
| 219 | + $rates[] = [ | |
| 220 | + 'country' => $code, | |
| 221 | + 'label' => $country['label'], | |
| 222 | + 'rate' => $classRates['standard']['rate'] ?? ($country['default_rate'] ?? 0), | |
| 223 | + 'tax_label' => $taxLabel, | |
| 224 | + 'default_rate' => $country['default_rate'] ?? 0, | |
| 225 | + 'has_custom' => $classRates['standard']['has_custom'] ?? false, | |
| 226 | + 'class_rates' => $classRates, | |
| 227 | + ]; | |
| 142 | 228 | } |
| 143 | 229 | |
| 230 | + $classesInfo = $taxClasses->map(function ($tc) { | |
| 231 | + return ['slug' => $tc->slug, 'title' => $tc->title, 'id' => $tc->id]; | |
| 232 | + })->values(); | |
| 233 | + | |
| 144 | 234 | return $this->sendSuccess([ |
| 145 | - 'message' => __('OSS tax override saved successfully', 'fluent-cart') | |
| 235 | + 'rates' => $rates, | |
| 236 | + 'classes' => $classesInfo, | |
| 146 | 237 | ]); |
| 147 | 238 | } |
| 148 | 239 | |
| 149 | - public function deleteOssTaxOverride(Request $request) | |
| 240 | + public function saveOssCountryRates(Request $request) | |
| 150 | 241 | { |
| 151 | - $countryCode = $request->getSafe('country', 'sanitize_text_field'); | |
| 152 | - $state = $request->getSafe('state', 'sanitize_text_field'); | |
| 242 | + $rates = $request->get('rates', []); | |
| 243 | + $taxClasses = TaxClass::query()->get()->keyBy('slug'); | |
| 244 | + $errors = []; | |
| 153 | 245 | |
| 154 | - if (!$countryCode) { | |
| 155 | - return $this->sendError([ | |
| 156 | - 'message' => __('Country code is required', 'fluent-cart') | |
| 157 | - ], 423); | |
| 158 | - } | |
| 246 | + foreach ($rates as $index => $item) { | |
| 247 | + $country = $this->sanitizeCountryCode($item['country'] ?? ''); | |
| 248 | + $taxLabel = sanitize_text_field($item['tax_label'] ?? ''); | |
| 249 | + if (!$country) continue; | |
| 159 | 250 | |
| 160 | - // select on base of countryCode, state if given and group eu | |
| 161 | - $query = TaxRate::query()->where('country', $countryCode)->where('group', 'EU'); | |
| 162 | - if ($state) { | |
| 163 | - $query->where('state', $state); | |
| 251 | + if (!$this->isEuVatCountry($country)) { | |
| 252 | + $errors['rates.' . $index . '.country'] = __('Select a valid EU VAT country', 'fluent-cart'); | |
| 253 | + continue; | |
| 254 | + } | |
| 255 | + | |
| 256 | + $classRates = $item['class_rates'] ?? []; | |
| 257 | + | |
| 258 | + // If no class_rates provided, fall back to single 'rate' field (backward compat) | |
| 259 | + if (empty($classRates)) { | |
| 260 | + $standardClass = $taxClasses->get('standard'); | |
| 261 | + $classId = $standardClass ? $standardClass->id : 1; | |
| 262 | + $rate = floatval($item['rate'] ?? 0); | |
| 263 | + $rateName = $taxLabel ?: ($country . ' Standard Tax'); | |
| 264 | + | |
| 265 | + $this->upsertOssRate($country, $classId, $rate, $rateName); | |
| 266 | + continue; | |
| 267 | + } | |
| 268 | + | |
| 269 | + // Save rate for each class — use per-class label, fall back to shared tax_label | |
| 270 | + foreach ($classRates as $classSlug => $classData) { | |
| 271 | + $classSlug = sanitize_key($classSlug); | |
| 272 | + $taxClass = $taxClasses->get($classSlug); | |
| 273 | + if (!$taxClass) continue; | |
| 274 | + | |
| 275 | + $rate = floatval($classData['rate'] ?? 0); | |
| 276 | + $classLabel = sanitize_text_field($classData['label'] ?? ''); | |
| 277 | + $rateName = $classLabel ?: ($taxLabel ?: ($country . ' ' . ucfirst($classSlug) . ' Tax')); | |
| 278 | + | |
| 279 | + $this->upsertOssRate($country, $taxClass->id, $rate, $rateName); | |
| 280 | + } | |
| 164 | 281 | } |
| 165 | 282 | |
| 166 | - $deleted = $query->delete(); | |
| 167 | - | |
| 168 | - if (!$deleted) { | |
| 283 | + if ($errors) { | |
| 169 | 284 | return $this->sendError([ |
| 170 | - 'message' => __('No matching OSS tax override found to delete', 'fluent-cart') | |
| 171 | - ], 423); | |
| 285 | + 'message' => __('Validation failed for OSS country rates', 'fluent-cart'), | |
| 286 | + 'errors' => $errors | |
| 287 | + ], 422); | |
| 172 | 288 | } |
| 173 | 289 | |
| 174 | 290 | return $this->sendSuccess([ |
| 175 | - 'message' => __('OSS tax override deleted successfully', 'fluent-cart') | |
| 291 | + 'message' => __('OSS country rates saved successfully', 'fluent-cart') | |
| 176 | 292 | ]); |
| 177 | 293 | } |
| 178 | 294 | |
| 179 | - public function deleteOssShippingOverride(Request $request) | |
| 295 | + private function sanitizeNestedArray(array $data) | |
| 180 | 296 | { |
| 181 | - $countryCode = $request->getSafe('country', 'sanitize_text_field'); | |
| 182 | - $state = $request->getSafe('state', 'sanitize_text_field'); | |
| 297 | + $sanitized = []; | |
| 298 | + foreach ($data as $key => $value) { | |
| 299 | + if (is_array($value)) { | |
| 300 | + $sanitized[$key] = $this->sanitizeNestedArray($value); | |
| 301 | + } elseif (is_numeric($value)) { | |
| 302 | + $sanitized[$key] = $value + 0; | |
| 303 | + } else { | |
| 304 | + $sanitized[$key] = sanitize_text_field($value); | |
| 305 | + } | |
| 306 | + } | |
| 307 | + return $sanitized; | |
| 308 | + } | |
| 183 | 309 | |
| 184 | - if (!$countryCode) { | |
| 185 | - return $this->sendError([ | |
| 186 | - 'message' => __('Country code is required', 'fluent-cart') | |
| 187 | - ], 423); | |
| 310 | + private function upsertOssRate($country, $classId, $rate, $rateName) | |
| 311 | + { | |
| 312 | + TaxRate::query()->updateOrCreate( | |
| 313 | + [ | |
| 314 | + 'country' => $country, | |
| 315 | + 'group' => 'EU', | |
| 316 | + 'class_id' => $classId, | |
| 317 | + 'state' => '', | |
| 318 | + ], | |
| 319 | + [ | |
| 320 | + 'name' => $rateName, | |
| 321 | + 'rate' => $rate, | |
| 322 | + 'city' => '', | |
| 323 | + 'postcode' => '', | |
| 324 | + ] | |
| 325 | + ); | |
| 326 | + } | |
| 327 | + | |
| 328 | + public function getEuProductOverrides() | |
| 329 | + { | |
| 330 | + $euCountryCodes = array_column(TaxModule::euVatCountyOptions(), 'value'); | |
| 331 | + | |
| 332 | + $taxClasses = TaxClass::query()->get()->keyBy('id'); | |
| 333 | + | |
| 334 | + $overrides = Meta::query() | |
| 335 | + ->productCategoryTaxOverrides() | |
| 336 | + ->forTaxOverrideCountries($euCountryCodes) | |
| 337 | + ->get(); | |
| 338 | + | |
| 339 | + foreach ($overrides as $override) { | |
| 340 | + $classId = (int) Arr::get($override->meta_value, 'class_id', 0); | |
| 341 | + $taxClass = $classId ? $taxClasses->get($classId) : null; | |
| 342 | + $override->setAttribute('class_id', $classId); | |
| 343 | + $override->setAttribute('class_label', $taxClass ? $taxClass->title : ''); | |
| 188 | 344 | } |
| 189 | 345 | |
| 190 | - // select on base of countryCode, state if given and group eu | |
| 191 | - $query = TaxRate::query()->where('country', $countryCode)->where('group', 'EU'); | |
| 192 | - if ($state) { | |
| 193 | - $query->where('state', $state); | |
| 194 | - } | |
| 346 | + $shippingOverrides = TaxRate::query() | |
| 347 | + ->where('group', 'EU') | |
| 348 | + ->whereNotNull('for_shipping') | |
| 349 | + ->where(function ($q) { | |
| 350 | + $q->whereNull('state')->orWhere('state', ''); | |
| 351 | + }) | |
| 352 | + ->orderBy('country', 'asc') | |
| 353 | + ->orderBy('class_id', 'asc') | |
| 354 | + ->orderBy('id', 'asc') | |
| 355 | + ->get() | |
| 356 | + ->map(function ($rate) use ($taxClasses) { | |
| 357 | + $taxClass = $taxClasses->get($rate->class_id); | |
| 358 | + $rate->setAttribute('class_label', $taxClass ? $taxClass->title : ''); | |
| 359 | + return $rate; | |
| 360 | + }) | |
| 361 | + ->values(); | |
| 195 | 362 | |
| 196 | - $deleted = $query->delete(); | |
| 363 | + return $this->sendSuccess([ | |
| 364 | + 'overrides' => $overrides, | |
| 365 | + 'shipping_overrides' => $shippingOverrides, | |
| 366 | + ]); | |
| 367 | + } | |
| 197 | 368 | |
| 198 | - if (!$deleted) { | |
| 199 | - return $this->sendError([ | |
| 200 | - 'message' => __('No matching OSS shipping override found to delete', 'fluent-cart') | |
| 201 | - ], 423); | |
| 202 | - } | |
| 369 | + public function resetEuRatesToDefaults() | |
| 370 | + { | |
| 371 | + $taxManager = TaxManager::getInstance(); | |
| 372 | + $taxManager->resetEuRates(); | |
| 203 | 373 | |
| 204 | 374 | return $this->sendSuccess([ |
| 205 | - 'message' => __('OSS shipping override deleted successfully', 'fluent-cart') | |
| 375 | + 'message' => __('EU tax rates have been reset to defaults', 'fluent-cart') | |
| 206 | 376 | ]); |
| 207 | 377 | } |
| 208 | - | |
| 378 | + | |
| 209 | 379 | public function euCrossBorderSettings(Request $request) |
| 210 | 380 | { |
| 211 | - $newEuVatSettings = $request->get('eu_vat_settings', []); | |
| 212 | - $sanitizedEuVatSettings = array_map('sanitize_text_field', $newEuVatSettings); | |
| 381 | + $newEuVatSettings = (array) $request->get('eu_vat_settings', []); | |
| 382 | + $sanitizedEuVatSettings = $this->sanitizeNestedArray($newEuVatSettings); | |
| 213 | 383 | $method = Arr::get($sanitizedEuVatSettings, 'method'); |
| 384 | + $ossCountry = $this->sanitizeCountryCode(Arr::get($sanitizedEuVatSettings, 'oss_country')); | |
| 385 | + $homeCountry = $this->sanitizeCountryCode(Arr::get($sanitizedEuVatSettings, 'home_country')); | |
| 386 | + Arr::set($sanitizedEuVatSettings, 'oss_country', $ossCountry); | |
| 387 | + Arr::set($sanitizedEuVatSettings, 'home_country', $homeCountry); | |
| 214 | 388 | $errors = []; |
| 215 | 389 | |
| 216 | 390 | if (!in_array($method, ['oss', 'home', 'specific'], true)) { |
| 217 | 391 | $errors['method'] = __('Select a cross-border registration type', 'fluent-cart'); |
| 218 | 392 | } else if ($method === 'oss') { |
| 219 | - if (!Arr::get($sanitizedEuVatSettings, 'oss_country')) { | |
| 393 | + if (!$ossCountry) { | |
| 220 | 394 | $errors['oss_country'] = __('Select country of OSS registration', 'fluent-cart'); |
| 395 | + } else if (!$this->isEuVatCountry($ossCountry)) { | |
| 396 | + $errors['oss_country'] = __('Select a valid EU VAT country', 'fluent-cart'); | |
| 221 | 397 | } |
| 222 | 398 | } else if ($method === 'home') { |
| 223 | - if (!Arr::get($sanitizedEuVatSettings, 'home_country')) { | |
| 399 | + if (!$homeCountry) { | |
| 224 | 400 | $errors['home_country'] = __('Select home country of registration', 'fluent-cart'); |
| 401 | + } else if (!$this->isEuVatCountry($homeCountry)) { | |
| 402 | + $errors['home_country'] = __('Select a valid EU VAT country', 'fluent-cart'); | |
| 225 | 403 | } |
| 226 | 404 | } |
| 227 | 405 | |
| 228 | 406 | if ($errors) { |
| @@ -228,9 +406,9 @@ | ||
| 228 | 406 | if ($errors) { |
| 229 | 407 | return $this->sendError([ |
| 230 | 408 | 'message' => __('Validation failed for EU VAT settings', 'fluent-cart'), |
| 231 | 409 | 'errors' => $errors |
| 232 | - ], 423); | |
| 410 | + ], 422); | |
| 233 | 411 | } |
| 234 | 412 | $currentSettings = (new TaxModule())->getSettings(); |
| 235 | 413 | |
| 236 | 414 | $currentSettings['eu_vat_settings'] = array_merge( |
| @@ -248,5 +426,23 @@ | ||
| 248 | 426 | 'message' => __('EU VAT settings saved successfully', 'fluent-cart') |
| 249 | 427 | ]); |
| 250 | 428 | |
| 251 | 429 | } |
| 252 | -} | |
| 430 | + | |
| 431 | + private function sanitizeCountryCode($countryCode) | |
| 432 | + { | |
| 433 | + return strtoupper(sanitize_text_field($countryCode)); | |
| 434 | + } | |
| 435 | + | |
| 436 | + private function isEuVatCountry($countryCode) | |
| 437 | + { | |
| 438 | + static $euCountryCodes = null; | |
| 439 | + | |
| 440 | + if ($euCountryCodes === null) { | |
| 441 | + $euCountryCodes = array_map(function ($country) { | |
| 442 | + return strtoupper($country['value']); | |
| 443 | + }, TaxModule::euVatCountyOptions()); | |
| 444 | + } | |
| 445 | + | |
| 446 | + return in_array($countryCode, $euCountryCodes, true); | |
| 447 | + } | |
| 448 | +} | |