| @@ -64,9 +64,9 @@ | ||
| 64 | 64 | ) { |
| 65 | 65 | continue; |
| 66 | 66 | } |
| 67 | 67 | |
| 68 | - if ( $campaign->discount_intent === 'BOGO' ) { | |
| 68 | + if ( 'BOGO' === $campaign->discount_intent ) { | |
| 69 | 69 | $campaign->discount_intent = 'BuyXGetX'; |
| 70 | 70 | |
| 71 | 71 | if ( in_array( $campaign->bogo_type, array( 'products', 'categories' ), true ) ) { |
| 72 | 72 | $campaign->discount_intent = 'BuyXGetY'; |
| @@ -97,9 +97,9 @@ | ||
| 97 | 97 | /** |
| 98 | 98 | * Check if the discount type is BuyXGetY and the Y product is in the cart. |
| 99 | 99 | * If so, add the Y product to the items array. |
| 100 | 100 | */ |
| 101 | - if ( $discount_type === 'BuyXGetY' ) { | |
| 101 | + if ( 'BuyXGetY' === $discount_type ) { | |
| 102 | 102 | $rule_ids = $campaign->get_rule_product_ids(); |
| 103 | 103 | $y_products = $this->get_y_product( $rule_ids, $cart_items, $bogo_type ); |
| 104 | 104 | $verified_yproducts = $this->verify_yproduct_in_cart( $rule_ids, $cart_items, $bogo_type ); |
| 105 | 105 | } |
| @@ -134,9 +134,9 @@ | ||
| 134 | 134 | * Check if the discount type is BuyXGetY and the Y product is in the cart. |
| 135 | 135 | * If so, add the Y product to the items array. |
| 136 | 136 | * These products not need to be passed the filter. |
| 137 | 137 | */ |
| 138 | - if ( $discount_type === 'BuyXGetY' && $verified_yproducts ) { | |
| 138 | + if ( 'BuyXGetY' === $discount_type && $verified_yproducts ) { | |
| 139 | 139 | $items = array_merge( $items, $y_products ); |
| 140 | 140 | } |
| 141 | 141 | |
| 142 | 142 | $items[] = $item; |
| @@ -251,17 +251,39 @@ | ||
| 251 | 251 | public function calculate_discount( $cost, $discount_type, $discount_value ) { |
| 252 | 252 | $cost = (float) $cost; |
| 253 | 253 | $discount_value = (float) $discount_value; |
| 254 | 254 | |
| 255 | - if ( 'percent' === $discount_type ) { | |
| 256 | - $cost = $cost * $discount_value / 100; | |
| 257 | - } | |
| 255 | + switch ( $discount_type ) { | |
| 256 | + case 'percent': | |
| 257 | + return $cost * $discount_value / 100; | |
| 258 | 258 | |
| 259 | - if ( 'fixed' === $discount_type || 'fixed_per_product' === $discount_type ) { | |
| 260 | - $cost = $discount_value; | |
| 261 | - } | |
| 259 | + case 'fixed': | |
| 260 | + case 'fixed_per_product': | |
| 261 | + return $discount_value; | |
| 262 | 262 | |
| 263 | - return $cost; | |
| 263 | + case 'free': | |
| 264 | + // The whole cost comes off, which is what makes the item free. | |
| 265 | + return $cost; | |
| 266 | + | |
| 267 | + default: | |
| 268 | + /** | |
| 269 | + * Anything else discounts nothing. | |
| 270 | + * | |
| 271 | + * This used to fall through and return $cost untouched, which the | |
| 272 | + * callers then treat as the discount: ProductIntent hands it the | |
| 273 | + * product price and CartIntent the cart subtotal, so an | |
| 274 | + * unrecognised type gave away the product or the entire cart. | |
| 275 | + * | |
| 276 | + * Only Bulk, Bundle and BOGO route through the Calc classes, | |
| 277 | + * where every type has an implementation; Product and Cart come | |
| 278 | + * straight here, so the gap was reachable by any rule carrying a | |
| 279 | + * type this method does not know: a campaign saved while | |
| 280 | + * fixed_price and percent_per_product were still offered in the | |
| 281 | + * builder, a REST write (discount_type inside discount_rules is | |
| 282 | + * not validated), or a hand edited row. | |
| 283 | + */ | |
| 284 | + return 0.0; | |
| 285 | + }//end switch | |
| 264 | 286 | } |
| 265 | 287 | |
| 266 | 288 | /** |
| 267 | 289 | * Get the discounted price. |
| @@ -304,9 +326,9 @@ | ||
| 304 | 326 | * @return bool Returns true if the number is a multiple of the given number, false otherwise. |
| 305 | 327 | */ |
| 306 | 328 | public function is_multiple( $number, $of ) { |
| 307 | 329 | if ( $of >= $number ) { |
| 308 | - return (float) $of % (float) $number === 0; | |
| 330 | + return 0 === (float) $of % (float) $number; | |
| 309 | 331 | } |
| 310 | 332 | |
| 311 | 333 | return false; |
| 312 | 334 | } |
| @@ -493,8 +515,37 @@ | ||
| 493 | 515 | } |
| 494 | 516 | |
| 495 | 517 | $discountable_units = $this->grouped_discount_budget( $rule, $total_qualifying_units ); |
| 496 | 518 | |
| 519 | + /** | |
| 520 | + * A flat amount is charged for the pooled group as a whole. | |
| 521 | + * | |
| 522 | + * Pooling exists so the cart together can reach the tier minimum, so | |
| 523 | + * the rule qualifies for the group rather than for each line that | |
| 524 | + * contributed quantity. The Calc layer returns the rule value whole | |
| 525 | + * for every line it is handed, because a flat amount does not scale | |
| 526 | + * with units, so spreading it across lines multiplied the discount by | |
| 527 | + * the line count: a flat 10 came out as 20 over two lines. | |
| 528 | + * | |
| 529 | + * It is applied to the first eligible line carrying the entire pooled | |
| 530 | + * budget, which is what lets a recursive rule still charge once per | |
| 531 | + * bundle: the Calc layer derives the bundle count from that quantity. | |
| 532 | + * Per unit types keep every line, since their amount is meant to | |
| 533 | + * scale with the units. | |
| 534 | + */ | |
| 535 | + if ( in_array( $rule['discount_type'], array( 'fixed', 'fixed_price' ), true ) ) { | |
| 536 | + $first_line = reset( $eligible_lines ); | |
| 537 | + | |
| 538 | + if ( ! empty( $first_line['item'] ) && $discountable_units > 0 ) { | |
| 539 | + $item = $first_line['item']; | |
| 540 | + $item['disco_forced_qty'] = $discountable_units; | |
| 541 | + | |
| 542 | + $this->apply_rule_to_discount( $discounts, $campaign, $rule, $item, $cart, $total_applicable_qty ); | |
| 543 | + } | |
| 544 | + | |
| 545 | + continue; | |
| 546 | + } | |
| 547 | + | |
| 497 | 548 | // Hand out the discountable units across the eligible lines in cart order. |
| 498 | 549 | foreach ( $eligible_lines as $eligible_line ) { |
| 499 | 550 | if ( $discountable_units <= 0 ) { |
| 500 | 551 | break; |
| @@ -572,9 +623,9 @@ | ||
| 572 | 623 | // Set discount applies to |
| 573 | 624 | $discounts[ $effective_product_id ]['discount_applies_to'][ $rule_key ] = CalcFactory::discount_applies_to( $rule, $campaign ); |
| 574 | 625 | |
| 575 | 626 | // Set Discounts. |
| 576 | - if ( $rule['discount_type'] === 'free' ) { | |
| 627 | + if ( 'free' === $rule['discount_type'] ) { | |
| 577 | 628 | $discounted_amount = array( |
| 578 | 629 | 'price' => $product->get_price(), |
| 579 | 630 | 'discount' => 0, |
| 580 | 631 | 'line_subtotal' => $product->get_price() * $item['quantity'], |
| @@ -1464,10 +1515,10 @@ | ||
| 1464 | 1515 | */ |
| 1465 | 1516 | private function verify_bundle_rule( int $quantity, array $rule ): bool { |
| 1466 | 1517 | $rule_basis = abs( $rule['min'] ); |
| 1467 | 1518 | |
| 1468 | - if ( $rule['recursive'] === 'yes' ) { | |
| 1469 | - if ( $quantity >= $rule_basis && ( $quantity % $rule_basis === 0 || $quantity > $rule_basis ) ) { | |
| 1519 | + if ( 'yes' === $rule['recursive'] ) { | |
| 1520 | + if ( $quantity >= $rule_basis && ( 0 === $quantity % $rule_basis || $quantity > $rule_basis ) ) { | |
| 1470 | 1521 | return true; |
| 1471 | 1522 | } |
| 1472 | 1523 | } elseif ( $quantity >= $rule_basis ) { |
| 1473 | 1524 | return true; |
| @@ -1486,16 +1537,16 @@ | ||
| 1486 | 1537 | private function verify_buyxgetx_rule( float $quantity, array $item, array $rule ): bool {//phpcs:ignore |
| 1487 | 1538 | |
| 1488 | 1539 | $verified = ( $quantity >= $rule['min'] && $quantity <= $rule['max'] ); |
| 1489 | 1540 | |
| 1490 | - if ( $verified && $rule['recursive'] === 'no' ) { | |
| 1541 | + if ( $verified && 'no' === $rule['recursive'] ) { | |
| 1491 | 1542 | return true; |
| 1492 | 1543 | } |
| 1493 | 1544 | |
| 1494 | 1545 | $base_quantity = abs( $rule['min'] ); |
| 1495 | 1546 | |
| 1496 | - if ( $rule['recursive'] === 'yes' && ! isset( $rule['max'] ) ) { | |
| 1497 | - if ( $quantity >= $base_quantity && $quantity % $base_quantity === 0 ) { | |
| 1547 | + if ( 'yes' === $rule['recursive'] && ! isset( $rule['max'] ) ) { | |
| 1548 | + if ( $quantity >= $base_quantity && 0 === $quantity % $base_quantity ) { | |
| 1498 | 1549 | return true; |
| 1499 | 1550 | } |
| 1500 | 1551 | } elseif ( $quantity >= $base_quantity ) { |
| 1501 | 1552 | return true; |
| @@ -1522,9 +1573,9 @@ | ||
| 1522 | 1573 | |
| 1523 | 1574 | /** |
| 1524 | 1575 | * This code use for automatically apply free items rule for all product BOGO campaigns. |
| 1525 | 1576 | */ |
| 1526 | - if ( $rule['discount_type'] === 'free' && 'all' === $campaign->get_bogo_type() ) { | |
| 1577 | + if ( 'free' === $rule['discount_type'] && 'all' === $campaign->get_bogo_type() ) { | |
| 1527 | 1578 | return $this->verify_buyxgetx_rule( $quantity, $item, $rule ); |
| 1528 | 1579 | } |
| 1529 | 1580 | |
| 1530 | 1581 | /** |
| @@ -1529,9 +1580,9 @@ | ||
| 1529 | 1580 | |
| 1530 | 1581 | /** |
| 1531 | 1582 | * This code use for automatically apply free items rule product based BOGO campaigns. |
| 1532 | 1583 | */ |
| 1533 | - if ( $rule['discount_type'] === 'free' && 'products' === $campaign->get_bogo_type() ) { | |
| 1584 | + if ( 'free' === $rule['discount_type'] && 'products' === $campaign->get_bogo_type() ) { | |
| 1534 | 1585 | return $this->verify_xproduct_cart_rule( $campaign, $rule ); |
| 1535 | 1586 | } |
| 1536 | 1587 | |
| 1537 | 1588 | if ( 'products' === $campaign->get_bogo_type() && in_array( $id, $rule_ids, true ) ) { |