| @@ -299,16 +299,72 @@ | ||
| 299 | 299 | ]; |
| 300 | 300 | |
| 301 | 301 | } |
| 302 | 302 | |
| 303 | + /** | |
| 304 | + * Coupon restriction lists live inside the JSON `conditions` column. The model accessor | |
| 305 | + * decodes them to arrays, but rows written before that accessor existed can still hold a | |
| 306 | + * JSON string, so accept both shapes. | |
| 307 | + * | |
| 308 | + * @param mixed $value | |
| 309 | + * @return array | |
| 310 | + */ | |
| 311 | + protected static function normalizeConditionList($value): array | |
| 312 | + { | |
| 313 | + if (is_array($value)) { | |
| 314 | + return array_values($value); | |
| 315 | + } | |
| 316 | + | |
| 317 | + if (is_string($value) && $value !== '') { | |
| 318 | + $decoded = json_decode($value, true); | |
| 319 | + return is_array($decoded) ? array_values($decoded) : []; | |
| 320 | + } | |
| 321 | + | |
| 322 | + return []; | |
| 323 | + } | |
| 324 | + | |
| 303 | 325 | public static function checkProductEligibility($productId, $couponCode, $origin) |
| 304 | 326 | { |
| 305 | 327 | $coupon = static::getQuery()->where('code', $couponCode)->first(); |
| 306 | - $excludedCategories = json_decode(Arr::get($coupon, 'excluded_categories', '[]'), true) ?? []; | |
| 307 | - $includedCategories = json_decode(Arr::get($coupon, 'included_categories', '[]'), true) ?? []; | |
| 308 | - $excludedProducts = json_decode(Arr::get($coupon, 'excluded_products', '[]'), true) ?? []; | |
| 309 | - $includedProducts = json_decode(Arr::get($coupon, 'included_products', '[]'), true) ?? []; | |
| 310 | - $product = Product::with('wp_terms')->find($productId)->toArray(); | |
| 328 | + | |
| 329 | + if (!$coupon) { | |
| 330 | + return [ | |
| 331 | + 'isApplicable' => false, | |
| 332 | + 'message' => __('This coupon is no longer available.', 'fluent-cart'), | |
| 333 | + ]; | |
| 334 | + } | |
| 335 | + | |
| 336 | + $productModel = Product::with('wp_terms')->find($productId); | |
| 337 | + | |
| 338 | + if (!$productModel) { | |
| 339 | + return [ | |
| 340 | + 'isApplicable' => false, | |
| 341 | + 'message' => __('Product not found.', 'fluent-cart'), | |
| 342 | + ]; | |
| 343 | + } | |
| 344 | + | |
| 345 | + return static::evaluateProductEligibility($productModel, $coupon); | |
| 346 | + } | |
| 347 | + | |
| 348 | + /** | |
| 349 | + * Verdict for one already-loaded product/coupon pair, so a caller checking several | |
| 350 | + * coupons at once hydrates the product and the coupon rows a single time. | |
| 351 | + * | |
| 352 | + * @param \FluentCart\App\Models\Product $productModel | |
| 353 | + * @param \FluentCart\App\Models\Coupon $coupon | |
| 354 | + * @return array | |
| 355 | + */ | |
| 356 | + public static function evaluateProductEligibility($productModel, $coupon): array | |
| 357 | + { | |
| 358 | + $productId = $productModel->getKey(); | |
| 359 | + $couponCode = $coupon->code; | |
| 360 | + $conditions = $coupon->conditions; | |
| 361 | + $excludedCategories = static::normalizeConditionList(Arr::get($conditions, 'excluded_categories')); | |
| 362 | + $includedCategories = static::normalizeConditionList(Arr::get($conditions, 'included_categories')); | |
| 363 | + $excludedProducts = static::normalizeConditionList(Arr::get($conditions, 'excluded_products')); | |
| 364 | + $includedProducts = static::normalizeConditionList(Arr::get($conditions, 'included_products')); | |
| 365 | + | |
| 366 | + $product = $productModel->toArray(); | |
| 311 | 367 | |
| 312 | 368 | $productCategories = array_map(function ($productCategories) { |
| 313 | 369 | return $productCategories['term_taxonomy_id']; |
| 314 | 370 | }, $product['wp_terms']); |