| @@ -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. |
| @@ -492,8 +514,37 @@ | ||
| 492 | 514 | $total_qualifying_units += $eligible_line['eligible_qty']; |
| 493 | 515 | } |
| 494 | 516 | |
| 495 | 517 | $discountable_units = $this->grouped_discount_budget( $rule, $total_qualifying_units ); |
| 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 | + } | |
| 496 | 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 ) { |