Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide an option to automatically add BuyXGetY discount rewards #1385

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<div class="space-y-4">


<header class="flex items-center justify-between">
<div>
<strong>Qualify Products</strong>
Expand Down Expand Up @@ -100,6 +99,14 @@ class="rounded border px-3 py-2 flex items-center"
</div>
@endforeach
</div>

@if($this->purchasableRewards->count())
<div class="grid grid-cols-2">
<x-hub::input.group for="automatically_add_rewards" :error="$errors->first('discount.data.automatically_add_rewards')" label="Automatically add rewards" instructions="Switch on to add reward products when not present in the basket">
<x-hub::input.toggle id="automatically_add_rewards" wire:model="discount.data.automatically_add_rewards" />
</x-hub::input.group>
</div>
@endif

<x-hub::alert>
If one or more items are in the cart, the cheapest item will be discounted.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public function rules()
'discount.data.min_qty' => 'required',
'discount.data.reward_qty' => 'required|numeric',
'discount.data.max_reward_qty' => 'required|numeric',
'discount.data.automatically_add_rewards' => 'nullable|boolean',
'selectedConditions' => 'array|min:1',
'selectedRewards' => 'array|min:1',
];
Expand Down
154 changes: 154 additions & 0 deletions packages/core/src/DiscountTypes/BuyXGetY.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Lunar\DiscountTypes;

use Illuminate\Pipeline\Pipeline;
use Illuminate\Support\Collection;
use Lunar\Base\ValueObjects\Cart\DiscountBreakdown;
use Lunar\Base\ValueObjects\Cart\DiscountBreakdownLine;
use Lunar\DataTypes\Price;
Expand Down Expand Up @@ -51,6 +53,7 @@ public function apply(Cart $cart): Cart
$minQty = $data['min_qty'] ?? null;
$rewardQty = $data['reward_qty'] ?? 1;
$maxRewardQty = $data['max_reward_qty'] ?? null;
$automaticallyAddRewards = $data['automatically_add_rewards'] ?? false;

// Get all purchasables that are eligible.
$conditions = $cart->lines->reject(function ($line) {
Expand Down Expand Up @@ -163,6 +166,10 @@ public function apply(Cart $cart): Cart
$cart->freeItems->push($rewardLine->purchasable);
}

if ($automaticallyAddRewards) {
[$affectedLines, $discountTotal] = $this->processAutomaticRewards($cart, $remainingRewardQty, $affectedLines, $discountTotal);
}

$this->addDiscountBreakdown($cart, new DiscountBreakdown(
discount: $this->discount,
lines: $affectedLines,
Expand All @@ -171,4 +178,151 @@ public function apply(Cart $cart): Cart

return $cart;
}

private function processAutomaticRewards(Cart $cart, int $remainingRewardQty, Collection $affectedLines, int $discountTotal)
{
$automaticLines = $cart->lines->filter(function ($line) {
return in_array($this->discount->id, array_keys($line->meta->added_by_discount ?? []));
});

$remainingRewardQty -= $automaticLines->sum(function ($line) {
return $line->meta->added_by_discount[$this->discount->id] ?? 0;
});

// we have lines to add
if ($remainingRewardQty > 0) {
while ($remainingRewardQty > 0) {
$selectedRewardItem = $this->discount->purchasableRewards->random()->purchasable;
$purchasable = $selectedRewardItem->variants->first();

// is it already in cart?
$rewardLine = $cart->lines->first(function ($line) use ($purchasable) {
return $line->purchasable->id == $purchasable->id;
});

if (! $rewardLine) {
$rewardLine = $cart->lines()->make([
'purchasable_type' => get_class($purchasable),
'purchasable_id' => $purchasable->id,
'quantity' => 1,
]);

if (! $cart->freeItems) {
$cart->freeItems = collect();
}

if (! $cart->freeItems->contains($selectedRewardItem)) {
$cart->freeItems->push($selectedRewardItem);
}

$rewardLine = app(Pipeline::class)
->send($rewardLine)
->through(
config('lunar.cart.pipelines.cart_lines', [])
)->thenReturn(function ($cartLine) {
$cartLine->cacheProperties();

return $cartLine;
});

$unitQuantity = $purchasable->getUnitQuantity();

$rewardLine->subTotal = new Price($rewardLine->unitPrice->value, $cart->currency, $unitQuantity);
$rewardLine->taxAmount = new Price(0, $cart->currency, $unitQuantity);
$rewardLine->total = new Price($rewardLine->unitPrice->value, $cart->currency, $unitQuantity);
}

$meta = $rewardLine->meta ?? json_decode('{}');
if (! isset($meta->added_by_discount)) {
$meta->added_by_discount = [];
}

if (! isset($meta->added_by_discount[$this->discount->id])) {
$meta->added_by_discount[$this->discount->id] = 1;
} else {
$meta->added_by_discount[$this->discount->id]++;
}

$affectedLine = $affectedLines->first(function ($line) use ($rewardLine) {
return $line->line == $rewardLine;
});

if (! $affectedLine) {
$affectedLines->push(new DiscountBreakdownLine(
line: $rewardLine,
quantity: 1
));
} else {
$affectedLine->quantity++;
}

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

$discountTotal += $unitPrice;

$rewardLine->discountTotal = new Price(
($rewardLine->discountTotal?->value ?? 0) + $unitPrice,
$cart->currency,
1
);

$rewardLine->subTotalDiscounted = new Price(
$rewardLine->subTotal->value - $rewardLine->discountTotal->value,
$cart->currency,
1
);

$rewardLine->meta = $meta;
$rewardLine->save();

$remainingRewardQty--;
}

// we have lines to remove
} elseif ($remainingRewardQty < 0) {
// while handles the situation where quantity of an item may be more than 1
while ($remainingRewardQty > 0 && ! empty($automaticLines)) {
// loop over automatic lines and decrement quantity
foreach ($automaticLines as $index => $line) {
if ($remainingRewardQty >= 0) {
continue;
}

$meta = $line->meta;
$addedByDiscountQty = $meta->added_by_discount[$this->discount->id] ?? 0;

if ($addedByDiscountQty > 0) {
$line->quantity = $line->quantity - 1;
$addedByDiscountQty--;
$remainingRewardQty++;

if ($addedByDiscountQty < 1) {
unset($meta->added_by_discount[$this->discount->id]);
} else {
$meta->added_by_discount[$this->discount->id] = $addedByDiscountQty;
}

if (empty($meta->added_by_discount)) {
unset($meta->added_by_discount);
}

$line->meta = $meta;
}

if ($line->quantity > 0) {
$line->save();
} else {
$line->delete();
$cart->freeItems->remove($line->product);
}

if ($addedByDiscountQty <= 0) {
unset($automaticLines[$index]);
}
}
}
}

return [$affectedLines, $discountTotal];
}
}
96 changes: 96 additions & 0 deletions packages/core/tests/Unit/DiscountTypes/BuyXGetYTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,102 @@ public function can_discount_eligible_products()
$this->assertCount(1, $cart->freeItems);
}

/**
* @test
*/
public function can_add_eligible_products_when_not_cart()
{
$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,
]);

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

$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
*/
Expand Down
Loading