PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
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
← All changes | api/Resource/CouponResource.php +115 -14 1.6.0 → 1.6.5 View file →
@@ -1,10 +1,12 @@
1 1 <?php
2 2
3 3 namespace FluentCart\Api\Resource;
4 4
5 +use FluentCart\App\Helpers\CouponHelper;
5 6 use FluentCart\App\Helpers\Helper;
6 7 use FluentCart\App\Models\Coupon;
8 +use FluentCart\App\Models\Product;
7 9 use FluentCart\App\Services\Coupon\CouponServiceAdmin;
8 10 use FluentCart\App\Services\DateTime\DateTime;
9 11 use FluentCart\Framework\Database\Orm\Builder;
10 12 use FluentCart\Framework\Support\Arr;
@@ -17,8 +19,12 @@
17 19 * @package FluentCart\Api\Resource
18 20 */
19 21 class CouponResource extends BaseResourceApi
20 22 {
23 + /**
24 + * Upper bound on the applied-coupon list a single eligibility check will accept.
25 + */
26 + const MAX_ELIGIBILITY_COUPONS = 50;
21 27
22 28 /**
23 29 * Get the query builder instance for the Coupons model.
24 30 *
@@ -131,9 +137,9 @@
131 137
132 138 if (!$id) {
133 139 return static::makeErrorResponse([
134 140 ['code' => 403, 'message' => __('Please edit a valid coupon!', 'fluent-cart')]
135 - ]);
141 + ], 403);
136 142 }
137 143
138 144 $hasCoupon = self::find($id);
139 145
@@ -139,9 +145,9 @@
139 145
140 146 if (!$hasCoupon) {
141 147 return static::makeErrorResponse([
142 148 ['code' => 404, 'message' => __('Coupon not found, please reload the page and try again!', 'fluent-cart')]
143 - ]);
149 + ], 404);
144 150 }
145 151
146 152 // If the client sends conditions without a valid spend basis (e.g. an older REST client that
147 153 // predates the field), keep whatever is already stored so an unrelated edit never silently
@@ -163,9 +169,9 @@
163 169 }
164 170
165 171 return static::makeErrorResponse([
166 172 ['code' => 400, 'message' => __('Coupon update failed.', 'fluent-cart')]
167 - ]);
173 + ], 400);
168 174 }
169 175
170 176 public static function delete($id, $params = [])
171 177 {
@@ -171,9 +177,9 @@
171 177 {
172 178 if (!$id) {
173 179 return static::makeErrorResponse([
174 180 ['code' => 403, 'message' => __('Please use a valid coupon ID!', 'fluent-cart')]
175 - ]);
181 + ], 403);
176 182 }
177 183
178 184 $hasCoupon = self::find($id);
179 185
@@ -183,14 +189,14 @@
183 189 }
184 190
185 191 return static::makeErrorResponse([
186 192 ['code' => 400, 'message' => __('Coupon deletion failed!', 'fluent-cart')]
187 - ]);
193 + ], 400);
188 194 }
189 195
190 196 return static::makeErrorResponse([
191 197 ['code' => 404, 'message' => __('Coupon not found in database, failed to remove.', 'fluent-cart')]
192 - ]);
198 + ], 404);
193 199 }
194 200
195 201 private static function formatAmount($data)
196 202 {
@@ -212,11 +218,13 @@
212 218 $couponCode = Arr::get($data, 'coupon_code');
213 219 $lineItems = Arr::except(Arr::get($data, 'order_items', []), ['*']);
214 220 $orderId = Arr::get($data, 'order_uuid', null);
215 221 $getAppliedCouponLists = Arr::get($data, 'applied_coupons', []);
222 + $customerEmail = (string) Arr::get($data, 'customer_email', '');
216 223 $previouslyAppliedCouponCodes = new Collection();
217 224 if (!empty($orderId)) {
218 - $order = OrderResource::find($orderId, ['with' => 'appliedCoupons']);
225 + $order = OrderResource::find($orderId, ['with' => ['appliedCoupons', 'customer']]);
226 + $customerEmail = $order->customer->email ?? $customerEmail;
219 227 $previouslyAppliedCouponCodes = $order->appliedCoupons;
220 228 $previouslyAppliedCouponCodes = static::makeCouponFromAppliedCoupons($previouslyAppliedCouponCodes);
221 229 }
222 230 $appliedKeys = $previouslyAppliedCouponCodes->pluck('id')->toArray();
@@ -226,9 +234,9 @@
226 234 $getAppliedCouponLists = Coupon::query()->whereIn('id', $getAppliedCouponLists)->get()->keyBy('code')->toArray();
227 235 $previouslyAppliedCouponCodes = $previouslyAppliedCouponCodes->merge($getAppliedCouponLists);
228 236 }
229 237
230 - $couponService = new CouponServiceAdmin($lineItems, null, $previouslyAppliedCouponCodes->keys()->toArray());
238 + $couponService = new CouponServiceAdmin($lineItems, null, $previouslyAppliedCouponCodes->keys()->toArray(), $customerEmail);
231 239 $couponApplied = $couponService->applyCoupon($couponCode);
232 240 if (is_wp_error($couponApplied)) {
233 241 return $couponApplied;
234 242 }
@@ -240,11 +248,15 @@
240 248 $errors = $couponService->getCouponErrors();
241 249
242 250
243 251 if($errors->has($couponCode)){
252 + // 422: without the status argument the WP_Error carries no
253 + // data.status and WP_REST_Server answers 500 for what is a
254 + // validation outcome (stacking, expiry, limits, spend gates).
255 + // The update/delete paths already pass their status the same way.
244 256 return static::makeErrorResponse([
245 257 ['code' => 400, 'message' => $errors->get($couponCode)->get_error_message()]
246 - ]);
258 + ], 422);
247 259 }
248 260
249 261 if ($returnCouponService) {
250 262 return [
@@ -262,12 +274,14 @@
262 274 $couponCode = Arr::get($data, 'coupon_code', null);
263 275 $lineItems = Arr::except(Arr::get($data, 'order_items', []), ['*']);
264 276 $orderId = Arr::get($data, 'order_uuid', null);
265 277 $getAppliedCouponLists = Arr::get($data, 'applied_coupons', []);
278 + $customerEmail = (string) Arr::get($data, 'customer_email', '');
266 279 $previouslyAppliedCouponCodes = new Collection();
267 280
268 281 if (!empty($orderId)) {
269 - $order = OrderResource::find($orderId, ['with' => ['order_items', 'appliedCoupons']]);
282 + $order = OrderResource::find($orderId, ['with' => ['order_items', 'appliedCoupons', 'customer']]);
283 + $customerEmail = $order->customer->email ?? $customerEmail;
270 284 if (!empty($couponId)) {
271 285 $order->appliedCoupons()->where('id', $couponId)->delete();
272 286 Coupon::query()->where('code', $couponCode)->decrement('use_count', 1);
273 287 }
@@ -279,9 +293,9 @@
279 293 $previouslyAppliedCouponCodes = Collection::make($getAppliedCouponLists);
280 294 }
281 295
282 296
283 - $couponService = new CouponServiceAdmin($lineItems, null, $previouslyAppliedCouponCodes->keys()->toArray());
297 + $couponService = new CouponServiceAdmin($lineItems, null, $previouslyAppliedCouponCodes->keys()->toArray(), $customerEmail);
284 298
285 299 $isCancelled = $couponService->cancelCoupon($couponCode);
286 300
287 301 if (is_wp_error($isCancelled)) {
@@ -307,12 +321,14 @@
307 321 {
308 322 $lineItems = Arr::except(Arr::get($data, 'order_items', []), ['*']);
309 323 $orderId = Arr::get($data, 'order_uuid', null);
310 324 $appliedCouponIds = Arr::get($data, 'applied_coupons', []);
325 + $customerEmail = (string) Arr::get($data, 'customer_email', '');
311 326 $previouslyAppliedCouponCodes = null;
312 327
313 328 if (!empty($orderId)) {
314 - $order = OrderResource::find($orderId, ['with' => 'appliedCoupons']);
329 + $order = OrderResource::find($orderId, ['with' => ['appliedCoupons', 'customer']]);
330 + $customerEmail = $order->customer->email ?? $customerEmail;
315 331 if (empty($lineItems)) {
316 332 $order->appliedCoupons()->delete();
317 333 } else {
318 334 $previouslyAppliedCouponCodes = $order->appliedCoupons;
@@ -322,12 +338,12 @@
322 338 // When the request carries coupon IDs (current UI state, may include unsaved coupons),
323 339 // load them directly so freshly applied coupons survive item adjustments before save.
324 340 if (!empty($appliedCouponIds)) {
325 341 $couponCodes = Coupon::query()->whereIn('id', $appliedCouponIds)->pluck('code')->toArray();
326 - $couponService = new CouponServiceAdmin($lineItems, null, $couponCodes);
342 + $couponService = new CouponServiceAdmin($lineItems, null, $couponCodes, $customerEmail);
327 343 } else {
328 344 $previouslyAppliedCouponCodes = static::makeCouponFromAppliedCoupons($previouslyAppliedCouponCodes);
329 - $couponService = new CouponServiceAdmin($lineItems, $previouslyAppliedCouponCodes);
345 + $couponService = new CouponServiceAdmin($lineItems, $previouslyAppliedCouponCodes, [], $customerEmail);
330 346 }
331 347
332 348 $couponService->reapplyCoupons();
333 349
@@ -342,8 +358,93 @@
342 358 ];
343 359 }
344 360
345 361 return new WP_Error(__('Something went wrong while updating the cart.', 'fluent-cart'));
362 + }
363 +
364 + /**
365 + * Report whether a product may be bought while the given coupons are applied.
366 + *
367 + * A product is eligible only when every applied coupon accepts it, so the first
368 + * rejection wins and carries its message back to the caller.
369 + *
370 + * @param int $productId
371 + * @param array $appliedCouponCodes Coupon codes, per the published API contract.
372 + * @param string|null $origin Context the check came from; not currently branched on.
373 + * @return array|WP_Error
374 + */
375 + public static function checkProductEligibilityForCodes($productId, $appliedCouponCodes = [], $origin = null)
376 + {
377 + $appliedCouponCodes = (array)$appliedCouponCodes;
378 +
379 + // Rejected rather than truncated: a silently shortened list would answer a question
380 + // the caller did not ask. An order carries a handful of coupons, never this many.
381 + if (count($appliedCouponCodes) > static::MAX_ELIGIBILITY_COUPONS) {
382 + return new WP_Error(
383 + 'too_many_applied_coupons',
384 + sprintf(
385 + /* translators: %d: maximum number of applied coupons accepted per request */
386 + __('Too many applied coupons supplied. Send at most %d.', 'fluent-cart'),
387 + static::MAX_ELIGIBILITY_COUPONS
388 + ),
389 + ['status' => 400]
390 + );
391 + }
392 +
393 + $codes = [];
394 +
395 + foreach ($appliedCouponCodes as $appliedCouponCode) {
396 + if (!is_scalar($appliedCouponCode) || is_bool($appliedCouponCode) || $appliedCouponCode === '') {
397 + continue;
398 + }
399 +
400 + $codes[(string)$appliedCouponCode] = (string)$appliedCouponCode;
401 + }
402 +
403 + if (!$codes) {
404 + return [
405 + 'isApplicable' => true,
406 + ];
407 + }
408 +
409 + $productModel = Product::with('wp_terms')->find($productId);
410 +
411 + if (!$productModel) {
412 + return [
413 + 'isApplicable' => false,
414 + 'message' => __('Product not found.', 'fluent-cart'),
415 + ];
416 + }
417 +
418 + $coupons = static::getQuery()
419 + ->whereIn('code', array_values($codes))
420 + ->get()
421 + ->keyBy('code');
422 +
423 + foreach ($codes as $code) {
424 + // Direct access, not Arr::get — a coupon code may legitimately contain a dot.
425 + $coupon = isset($coupons[$code]) ? $coupons[$code] : null;
426 +
427 + if (!$coupon) {
428 + return [
429 + 'isApplicable' => false,
430 + 'message' => __('This coupon is no longer available.', 'fluent-cart'),
431 + ];
432 + }
433 +
434 + $eligibility = CouponHelper::evaluateProductEligibility($productModel, $coupon);
435 +
436 + if (!Arr::get($eligibility, 'isApplicable')) {
437 + return [
438 + 'isApplicable' => false,
439 + 'message' => Arr::get($eligibility, 'message', ''),
440 + ];
441 + }
442 + }
443 +
444 + return [
445 + 'isApplicable' => true,
446 + ];
346 447 }
347 448
348 449 private static function makeCouponFromAppliedCoupons($previouslyAppliedCouponCodes)
349 450 {