| @@ -285,8 +285,15 @@ | ||
| 285 | 285 | if (Arr::get($discount, 'value', 0) > 0) { |
| 286 | 286 | static::distributeManualDiscount($items, Helper::toCent(Arr::get($discount, 'value', 0))); |
| 287 | 287 | } |
| 288 | 288 | |
| 289 | + $couponCheck = CouponResource::validateOrderCoupons($items, (array) Arr::get($data, 'applied_coupon', []), Arr::get($customer, 'email', '')); | |
| 290 | + if (is_wp_error($couponCheck)) { | |
| 291 | + return $couponCheck; | |
| 292 | + } | |
| 293 | + $items = $couponCheck['items']; | |
| 294 | + $data['applied_coupon'] = $couponCheck['applied_coupons']; | |
| 295 | + | |
| 289 | 296 | // admin order processor |
| 290 | 297 | $adminOrderProcessor = new AdminOrderProcessor($items, [ |
| 291 | 298 | 'customer_id' => $customer->id, |
| 292 | 299 | 'payment_method' => $paymentMethod, |
| @@ -1360,8 +1367,35 @@ | ||
| 1360 | 1367 | } |
| 1361 | 1368 | $subscription->save(); |
| 1362 | 1369 | } |
| 1363 | 1370 | |
| 1371 | + /** | |
| 1372 | + * Whether the submitted coupon and item discounts match the calculated ones, within a cent of rounding. | |
| 1373 | + * | |
| 1374 | + * @param array $submittedItems | |
| 1375 | + * @param array $submittedCoupons Applied-coupon map keyed by coupon code. | |
| 1376 | + * @param array $couponCheck Result of CouponResource::validateOrderCoupons(). | |
| 1377 | + * @return bool | |
| 1378 | + */ | |
| 1379 | + private static function couponDiscountsMatch(array $submittedItems, array $submittedCoupons, array $couponCheck): bool | |
| 1380 | + { | |
| 1381 | + foreach ($couponCheck['applied_coupons'] as $code => $calculated) { | |
| 1382 | + $submitted = isset($submittedCoupons[$code]['discount']) ? (float) $submittedCoupons[$code]['discount'] : 0; | |
| 1383 | + if (abs($submitted - (float) $calculated['discount']) > 1) { | |
| 1384 | + return false; | |
| 1385 | + } | |
| 1386 | + } | |
| 1387 | + | |
| 1388 | + foreach ($couponCheck['items'] as $index => $calculatedItem) { | |
| 1389 | + $submitted = (float) Arr::get($submittedItems, $index . '.discount_total', 0); | |
| 1390 | + if (abs($submitted - (float) Arr::get($calculatedItem, 'discount_total', 0)) > 1) { | |
| 1391 | + return false; | |
| 1392 | + } | |
| 1393 | + } | |
| 1394 | + | |
| 1395 | + return true; | |
| 1396 | + } | |
| 1397 | + | |
| 1364 | 1398 | private static function distributeManualDiscount(&$items, $manualDiscountTotal) |
| 1365 | 1399 | { |
| 1366 | 1400 | $totalSubtotal = array_reduce($items, function ($carry, $item) { |
| 1367 | 1401 | return $carry + ((int)Arr::get($item, 'unit_price', 0) * (int)Arr::get($item, 'quantity', 1)); |
| @@ -1607,8 +1641,27 @@ | ||
| 1607 | 1641 | $deletedItems = $data['deletedItems']; |
| 1608 | 1642 | $appliedCoupons = Arr::get($orderData, 'applied_coupon'); |
| 1609 | 1643 | $discount = $data['discount']; |
| 1610 | 1644 | $shipping = $data['shipping']; |
| 1645 | + | |
| 1646 | + if (!empty($appliedCoupons)) { | |
| 1647 | + $submittedItems = Arr::except((array) Arr::get($orderData, 'order_items', []), ['*']); | |
| 1648 | + $couponCheck = CouponResource::validateOrderCoupons( | |
| 1649 | + $submittedItems, | |
| 1650 | + (array) $appliedCoupons, | |
| 1651 | + $order->customer ? $order->customer->email : '' | |
| 1652 | + ); | |
| 1653 | + if (is_wp_error($couponCheck)) { | |
| 1654 | + return $couponCheck; | |
| 1655 | + } | |
| 1656 | + // Update saves the submitted items and totals, so they must already carry the calculated discounts. | |
| 1657 | + if (!static::couponDiscountsMatch($submittedItems, (array) $appliedCoupons, $couponCheck)) { | |
| 1658 | + return static::makeErrorResponse([ | |
| 1659 | + ['code' => 'coupon_discount_changed', 'message' => __('Coupon discounts have changed. Please re-apply the coupons and save again.', 'fluent-cart')] | |
| 1660 | + ], 422); | |
| 1661 | + } | |
| 1662 | + $appliedCoupons = $couponCheck['applied_coupons']; | |
| 1663 | + } | |
| 1611 | 1664 | |
| 1612 | 1665 | $orderId = $order->id; |
| 1613 | 1666 | |
| 1614 | 1667 | /** |