Skip to content

Commit

Permalink
Tweak discount logic (#894)
Browse files Browse the repository at this point in the history
  • Loading branch information
alecritson authored Mar 24, 2023
1 parent a599243 commit d9ca112
Show file tree
Hide file tree
Showing 3 changed files with 304 additions and 4 deletions.
15 changes: 14 additions & 1 deletion packages/core/src/DiscountTypes/AmountOff.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ private function applyFixedValue(array $values, Cart $cart): Cart
} else {
$amount = $roundedChunk;
}

// If this discount already has a greater discount value
// don't add this one as they already have a better deal.
if ($line->discountTotal->value > $amount) {
continue;
}

$remaining -= $amount;

$line->discountTotal = new Price(
Expand Down Expand Up @@ -190,12 +197,18 @@ private function applyPercentage($value, $cart): Cart

foreach ($lines as $line) {
$subTotal = $line->subTotal->value;
$subTotalDiscounted = $line->subTotalDiscounted?->value ?: 0;

if ($subTotalDiscounted) {
$subTotal = $subTotalDiscounted;
}

$amount = (int) round($subTotal * ($value / 100));

$totalDiscount += $amount;

$line->discountTotal = new Price(
$amount,
$subTotalDiscounted + $amount,
$cart->currency,
1
);
Expand Down
15 changes: 12 additions & 3 deletions packages/core/src/DiscountTypes/BuyXGetY.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function getName(): string
*/
public function getRewardQuantity($linesQuantity, $minQty, $rewardQty, $maxRewardQty = null)
{
$result = ($linesQuantity / $minQty) * $rewardQty;
$result = ($linesQuantity / ($minQty ?: 1)) * $rewardQty;

if ($maxRewardQty && $result > $maxRewardQty) {
return $maxRewardQty;
Expand Down Expand Up @@ -77,6 +77,7 @@ public function apply(Cart $cart): Cart
$maxRewardQty
);


if (! $totalRewardQty) {
return $cart;
}
Expand All @@ -94,14 +95,20 @@ public function apply(Cart $cart): Cart
});
})->sortBy('subTotal.value');


foreach ($rewardLines as $rewardLine) {
if (! $remainingRewardQty) {
continue;
}

$remainder = $rewardLine->quantity % $remainingRewardQty;

$qtyToAllocate = (int) floor(($remainingRewardQty - $remainder) / $rewardLine->quantity);

$qtyToAllocate = (int) round(($remainingRewardQty - $remainder) / $rewardLine->quantity);

if (!$remainder && $remainingRewardQty < $rewardLine->quantity) {
$qtyToAllocate = $remainingRewardQty;
}

if (! $qtyToAllocate) {
continue;
Expand All @@ -112,6 +119,7 @@ public function apply(Cart $cart): Cart
quantity: $qtyToAllocate
));


$conditionQtyToAllocate = $qtyToAllocate * $rewardQty;
$conditions->each(function ($conditionLine) use ($affectedLines, &$conditionQtyToAllocate) {
if (! $conditionQtyToAllocate) {
Expand All @@ -132,8 +140,9 @@ public function apply(Cart $cart): Cart
$remainingRewardQty -= $qtyToAllocate;

$subTotal = $rewardLine->subTotal->value;
$unitPrice = $rewardLine->unitPrice->value;

$lineDiscountTotal = $subTotal * $qtyToAllocate;
$lineDiscountTotal = $unitPrice * $qtyToAllocate;
$discountTotal += $lineDiscountTotal;

$rewardLine->discountTotal = new Price(
Expand Down
278 changes: 278 additions & 0 deletions packages/core/tests/Unit/DiscountTypes/BuyXGetYTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Lunar\Tests\Unit\DiscountTypes;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Lunar\DiscountTypes\AmountOff;
use Lunar\DiscountTypes\BuyXGetY;
use Lunar\Models\Cart;
use Lunar\Models\Channel;
Expand Down Expand Up @@ -283,4 +284,281 @@ public function can_discount_eligible_products()
$this->assertEquals(1200, $cart->total->value);
$this->assertCount(1, $cart->freeItems);
}

/**
* @test
*/
public function can_discount_purchasable_with_priority()
{
$customerGroup = CustomerGroup::factory()->create([
'default' => true,
]);

$channel = Channel::factory()->create([
'default' => true,
]);

$currency = Currency::factory()->create([
'code' => 'GBP',
]);

$cart = Cart::factory()->create([
'channel_id' => $channel->id,
'currency_id' => $currency->id,
]);

$productA = Product::factory()->create();
$productB = Product::factory()->create();

$purchasableA = ProductVariant::factory()->create([
'product_id' => $productA->id,
]);
$purchasableB = ProductVariant::factory()->create([
'product_id' => $productB->id,
]);

Price::factory()->create([
'price' => 1000, // £10
'tier' => 1,
'currency_id' => $currency->id,
'priceable_type' => get_class($purchasableA),
'priceable_id' => $purchasableA->id,
]);

Price::factory()->create([
'price' => 1000, // £10
'tier' => 1,
'currency_id' => $currency->id,
'priceable_type' => get_class($purchasableB),
'priceable_id' => $purchasableB->id,
]);

$cart->lines()->create([
'purchasable_type' => get_class($purchasableA),
'purchasable_id' => $purchasableA->id,
'quantity' => 1,
]);

$cart->lines()->create([
'purchasable_type' => get_class($purchasableB),
'purchasable_id' => $purchasableB->id,
'quantity' => 1,
]);

$discount = Discount::factory()->create([
'type' => BuyXGetY::class,
'priority' => 2,
'name' => 'Test Product Discount',
'data' => [
'min_qty' => 1,
'reward_qty' => 2,
],
]);

Discount::factory()->create([
'type' => AmountOff::class,
'name' => 'Test A amount off',
'uses' => 0,
'priority' => 1,
'max_uses' => 1,
'data' => [
'fixed_value' => true,
'fixed_values' => [
'GBP' => 10,
],
'min_prices' => [
'GBP' => 0,
],
],
]);

$discount->customerGroups()->sync([
$customerGroup->id => [
'enabled' => true,
'starts_at' => now(),
],
]);

$discount->channels()->sync([
$channel->id => [
'enabled' => true,
'starts_at' => now()->subHour(),
],
]);

$discount->purchasableConditions()->create([
'purchasable_type' => Product::class,
'purchasable_id' => $productA->id,
]);

$discount->purchasableRewards()->create([
'purchasable_type' => Product::class,
'purchasable_id' => $productB->id,
'type' => 'reward',
]);

$cart = $cart->calculate();

$this->assertEquals(1200, $cart->total->value);
$this->assertCount(1, $cart->freeItems);
}

/**
* @test
* @group flub
*
* Scenario
* ----------------------------------------------------
*
* Product A costs 10.00 before tax
* Product B costs 5.00 before tax
* Discount A is a BuyXGetY. Reward: Product B Condition: 2 x Product A
* Discount B is an Amount off for 10%
*
* Cart:
* 2 x Product A = (10.00 x 2) - 10% = 18.00 excl tax
* 2 x Product B = ((5.00 x 2) - 5.00) - 10% = 4.50 excl tax
* Sub total: 22.50
* Discount total: 7.50
* Tax total: 4.50
* Total: 27.00
*/
public function can_apply_multiple_different_discounts()
{
$customerGroup = CustomerGroup::factory()->create([
'default' => true,
]);

$channel = Channel::factory()->create([
'default' => true,
]);

$currency = Currency::factory()->create([
'code' => 'GBP',
]);

/**
* Product set up.
*/
$productA = Product::factory()->create();
$productB = Product::factory()->create();

$purchasableA = ProductVariant::factory()->create([
'product_id' => $productA->id,
]);
$purchasableB = ProductVariant::factory()->create([
'product_id' => $productB->id,
]);

Price::factory()->create([
'price' => 1000, // £10
'tier' => 1,
'currency_id' => $currency->id,
'priceable_type' => get_class($purchasableA),
'priceable_id' => $purchasableA->id,
]);

Price::factory()->create([
'price' => 500, // £5
'tier' => 1,
'currency_id' => $currency->id,
'priceable_type' => get_class($purchasableB),
'priceable_id' => $purchasableB->id,
]);

/**
* Cart set up.
*/
$cart = Cart::factory()->create([
'channel_id' => $channel->id,
'currency_id' => $currency->id,
'coupon_code' => 'AMOUNTOFFTEST',
]);

$cart->lines()->create([
'purchasable_type' => get_class($purchasableA),
'purchasable_id' => $purchasableA->id,
'quantity' => 2,
]);

$cart->lines()->create([
'purchasable_type' => get_class($purchasableB),
'purchasable_id' => $purchasableB->id,
'quantity' => 2,
]);

/**
* Discount set up.
*/

$discountA = Discount::factory()->create([
'type' => BuyXGetY::class,
'priority' => 1,
'name' => 'Test Product Discount',
'data' => [
'min_qty' => 2,
'reward_qty' => 1,
],
]);

$discountB = Discount::factory()->create([
'type' => AmountOff::class,
'name' => 'Test amount off',
'uses' => 0,
'priority' => 1,
'max_uses' => 1,
'coupon' => 'AMOUNTOFFTEST',
'data' => [
'fixed_value' => false,
'percentage' => 10,
],
]);

foreach ([$discountA, $discountB] as $discount) {
$discount->customerGroups()->sync([
$customerGroup->id => [
'enabled' => true,
'starts_at' => now(),
],
]);
$discount->channels()->sync([
$channel->id => [
'enabled' => true,
'starts_at' => now()->subHour(),
],
]);
}

$discountA->purchasableConditions()->create([
'purchasable_type' => Product::class,
'purchasable_id' => $productA->id,
]);

$discountA->purchasableRewards()->create([
'purchasable_type' => Product::class,
'purchasable_id' => $productB->id,
'type' => 'reward',
]);

$cart = $cart->calculate();

$lineA = $cart->lines->first(function ($line) use ($purchasableA) {
return $line->purchasable_id == $purchasableA->id;
});

$lineB = $cart->lines->first(function ($line) use ($purchasableB) {
return $line->purchasable_id == $purchasableB->id;
});

$this->assertEquals(2000, $lineA->subTotal->value);
$this->assertEquals(1800, $lineA->subTotalDiscounted->value);
$this->assertEquals(200, $lineA->discountTotal->value);

$this->assertEquals(1000, $lineB->subTotal->value);
$this->assertEquals(450, $lineB->subTotalDiscounted->value);
$this->assertEquals(550, $lineB->discountTotal->value);

$this->assertEquals(750, $cart->discountTotal->value);
$this->assertCount(2, $cart->discountBreakdown);
}
}

0 comments on commit d9ca112

Please sign in to comment.