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

Feature: Toggle interaction limitations on coupons and discounts #177

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('shop_coupons', function (Blueprint $table) {
$table->boolean('discount_allowed')->default(true)->after('can_cumulate');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('shop_coupons', function (Blueprint $table) {
$table->dropColumn('discount_allowed');
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('shop_discounts', function (Blueprint $table) {
$table->boolean('coupons_allowed')->default(true)->after('discount');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('shop_discounts', function (Blueprint $table) {
$table->dropColumn('coupons_allowed');
});
}
};
2 changes: 2 additions & 0 deletions resources/lang/en/admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
'active' => 'Active',
'usage' => 'Under usage limit',
'enable' => 'Enable the coupon',
'discount' => 'Allow usage of discounts with coupon',
],

'giftcards' => [
Expand All @@ -74,6 +75,7 @@
'global' => 'Should the discount be active on all the shop ?',
'active' => 'Active',
'enable' => 'Enable the discount',
'coupons' => 'Allow usage of coupons with discount',
'restricted' => 'Restrict this discount to certain roles only',
],

Expand Down
1 change: 1 addition & 0 deletions resources/lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
'add' => 'Add a coupon',
'error' => 'This coupon does not exist, has expired or can no longer be used.',
'cumulate' => 'You cannot use this coupon with an other coupon.',
'discount' => 'You cannot use this coupon with one or more of the existing discounts applied.',
],

'payment' => [
Expand Down
5 changes: 5 additions & 0 deletions resources/views/admin/coupons/_form.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,8 @@
<input type="checkbox" class="form-check-input" id="enableSwitch" name="is_enabled" @checked($coupon->is_enabled ?? true)>
<label class="form-check-label" for="enableSwitch">{{ trans('shop::admin.coupons.enable') }}</label>
</div>

<div class="mb-3 form-check form-switch">
<input type="checkbox" class="form-check-input" id="discountSwitch" name="discount_allowed" @checked($coupon->discount_allowed ?? true)>
<label class="form-check-label" for="discountSwitch">{{ trans('shop::admin.coupons.discount') }}</label>
</div>
5 changes: 5 additions & 0 deletions resources/views/admin/discounts/_form.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,8 @@
<input type="checkbox" class="form-check-input" id="enableSwitch" name="is_enabled" @checked($discount->is_enabled ?? true)>
<label class="form-check-label" for="enableSwitch">{{ trans('shop::admin.discounts.enable') }}</label>
</div>

<div class="mb-3 form-check form-switch">
<input type="checkbox" class="form-check-input" id="allowCouponsSwitch" name="coupons_allowed" @checked($discount->coupons_allowed ?? true)>
<label class="form-check-label" for="allowCouponsSwitch">{{ trans('shop::admin.discounts.coupons') }}</label>
</div>
6 changes: 3 additions & 3 deletions resources/views/cart/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,14 @@
<form action="{{ route('shop.cart.coupons.add') }}" method="POST" >
@csrf

<div class="input-group mb-3 @error('code') has-validation @enderror">
<input type="text" class="form-control @error('code') is-invalid @enderror" placeholder="{{ trans('shop::messages.fields.code') }}" id="code" name="code" value="{{ old('code') }}">
<div class="input-group mb-3 @error('coupon_code') has-validation @enderror">
<input type="text" class="form-control @error('coupon_code') is-invalid @enderror" placeholder="{{ trans('shop::messages.fields.code') }}" id="coupon_code" name="coupon_code" value="{{ old('coupon_code') }}">

<button type="submit" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> {{ trans('messages.actions.add') }}
</button>

@error('code')
@error('coupon_code')
<span class="invalid-feedback" role="alert"><strong>{{ $message }}</strong></span>
@enderror
</div>
Expand Down
88 changes: 85 additions & 3 deletions src/Cart/Cart.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

namespace Azuriom\Plugin\Shop\Cart;

use Azuriom\Plugin\Shop\Exceptions\CouponNotAllowedException;
use Azuriom\Plugin\Shop\Models\Concerns\Buyable;
use Azuriom\Plugin\Shop\Models\Coupon;
use Azuriom\Plugin\Shop\Models\Discount;
use Azuriom\Plugin\Shop\Models\Giftcard;
use Azuriom\Plugin\Shop\Models\Package;
use Illuminate\Contracts\Session\Session;
Expand Down Expand Up @@ -37,12 +39,21 @@ class Cart implements Arrayable
*/
private Collection $giftcards;

/**
* A collection of discounts applied to the cart.
*/
private Collection $packageDiscounts;

/**
* Create a new cart instance.
*/
private function __construct(Session $session = null)
private function __construct(?Session $session = null)
{
$this->session = $session;
$this->packageDiscounts = collect();

// Always add active global discounts instantly.
$this->addAppliedDiscounts(Discount::global()->active()->get());

if ($session === null) {
$this->items = collect();
Expand All @@ -58,14 +69,20 @@ private function __construct(Session $session = null)
/**
* Add an item to the cart.
*/
public function add(Buyable $buyable, int $quantity = 1, float $userPrice = null): void
public function add(Buyable $buyable, int $quantity = 1, ?float $userPrice = null): void
{
if ($quantity <= 0) {
return;
}

$cartItem = $this->get($buyable);

if ($buyable instanceof Package) {
/** @var Collection $discounts */
$discounts = $buyable->discounts()->active()->get();
$this->addAppliedDiscounts($discounts);
}

if ($cartItem === null) {
$this->set($buyable, $quantity, $userPrice);

Expand All @@ -81,7 +98,7 @@ public function add(Buyable $buyable, int $quantity = 1, float $userPrice = null
/**
* Set the quantity of an item in the cart.
*/
public function set(Buyable $buyable, int $quantity = 1, float $userPrice = null): void
public function set(Buyable $buyable, int $quantity = 1, ?float $userPrice = null): void
{
if ($quantity <= 0) {
$this->remove($buyable);
Expand Down Expand Up @@ -116,6 +133,10 @@ public function remove(Buyable $buyable): void
{
$this->items->forget($this->getItemId($buyable));

if ($buyable instanceof Package) {
$this->removeAppliedDiscounts($buyable->discounts()->active()->get());
}

$this->save();
}

Expand Down Expand Up @@ -251,9 +272,15 @@ public function coupons(): Collection

/**
* Add a coupon to the cart.
*
* @throws CouponNotAllowedException
*/
public function addCoupon(Coupon $coupon): void
{
if ($this->couponClashesWithActiveDiscount($coupon)) {
throw new CouponNotAllowedException();
}

$this->coupons->put($coupon->id, $coupon);

$this->save();
Expand Down Expand Up @@ -405,4 +432,59 @@ public function toArray(): array
'giftcards' => $this->giftcards->pluck('code')->all(),
];
}

/**
* Add discounts applied to cart.
*/
private function addAppliedDiscounts(Collection $discounts): void
{
if ($discounts->count() > 0) {
$discounts->each(function (Discount $discount) {
if (! $this->packageDiscounts->contains($discount->id)) {
$this->packageDiscounts->put($discount->id, $discount);
}
});
}
}

/**
* Remove given discounts from applied discounts collection.
* Skip global discounts as they are always applied regardless of what item
* has been placed in or removed from the cart.
*/
private function removeAppliedDiscounts(Collection $discounts): void
{
if ($discounts->count() > 0) {
$discounts->each(function (Discount $discount) {
if ($this->packageDiscounts->contains($discount->id) && ! $discount->is_global) {
$this->packageDiscounts->forget($discount->id);
}
});
}
}

/**
* Determine if the given coupon will clash with applied discounts.
*/
private function couponClashesWithActiveDiscount(Coupon $coupon): bool
{
$couponClashes = false;

if ($this->packageDiscounts->count() === 0) {
return false;
}

if (! $coupon->discount_allowed) {
return true;
}

/** @var Discount $discount */
foreach ($this->packageDiscounts as $discount) {
if (! $discount->coupons_allowed) {
$couponClashes = true;
}
}

return $couponClashes;
}
}
18 changes: 13 additions & 5 deletions src/Controllers/CouponController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Azuriom\Http\Controllers\Controller;
use Azuriom\Plugin\Shop\Cart\Cart;
use Azuriom\Plugin\Shop\Exceptions\CouponNotAllowedException;
use Azuriom\Plugin\Shop\Models\Coupon;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
Expand All @@ -17,13 +18,14 @@ class CouponController extends Controller
*/
public function add(Request $request)
{
$validated = $this->validate($request, ['code' => 'required']);
$validated = $this->validate($request, ['coupon_code' => 'required']);

$coupon = Coupon::active()->firstWhere($validated);
/** @var Coupon $coupon */
$coupon = Coupon::active()->firstWhere(['code' => $validated['coupon_code']]);

if ($coupon === null || $coupon->hasReachLimit($request->user())) {
throw ValidationException::withMessages([
'code' => trans('shop::messages.coupons.error'),
'coupon_code' => trans('shop::messages.coupons.error'),
]);
}

Expand All @@ -32,11 +34,17 @@ public function add(Request $request)
if ((! $coupon->can_cumulate && ! $cart->coupons()->isEmpty())
|| $cart->coupons()->contains('can_cumulate', false)) {
throw ValidationException::withMessages([
'code' => trans('shop::messages.coupons.cumulate'),
'coupon_code' => trans('shop::messages.coupons.cumulate'),
]);
}

$cart->addCoupon($coupon);
try {
$cart->addCoupon($coupon);
} catch (CouponNotAllowedException) {
throw ValidationException::withMessages([
'coupon_code' => trans('shop::messages.coupons.discount'),
]);
}

return to_route('shop.cart.index');
}
Expand Down
11 changes: 11 additions & 0 deletions src/Exceptions/CouponNotAllowedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Azuriom\Plugin\Shop\Exceptions;

use Exception;

final class CouponNotAllowedException extends Exception
{
}
4 changes: 3 additions & 1 deletion src/Models/Coupon.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* @property int $user_limit
* @property int $global_limit
* @property bool $can_cumulate
* @property bool $discount_allowed
* @property bool $is_enabled
* @property bool $is_global
* @property bool $is_fixed
Expand Down Expand Up @@ -42,7 +43,7 @@ class Coupon extends Model
* @var array<int, string>
*/
protected $fillable = [
'code', 'discount', 'start_at', 'expire_at', 'user_limit', 'global_limit', 'can_cumulate', 'is_enabled', 'is_global', 'is_fixed',
'code', 'discount', 'start_at', 'expire_at', 'user_limit', 'global_limit', 'can_cumulate', 'is_enabled', 'is_global', 'is_fixed', 'discount_allowed',
];

/**
Expand All @@ -57,6 +58,7 @@ class Coupon extends Model
'is_enabled' => 'boolean',
'is_global' => 'boolean',
'is_fixed' => 'boolean',
'discount_allowed' => 'boolean',
];

/**
Expand Down
4 changes: 3 additions & 1 deletion src/Models/Discount.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* @property int $id
* @property string $name
* @property int $discount
* @property bool $coupons_allowed
* @property array|null $roles
* @property bool $is_global
* @property bool $is_enabled
Expand Down Expand Up @@ -40,7 +41,7 @@ class Discount extends Model
* @var array<int, string>
*/
protected $fillable = [
'name', 'discount', 'packages', 'roles', 'is_global', 'is_enabled', 'start_at', 'end_at',
'name', 'discount', 'packages', 'roles', 'is_global', 'is_enabled', 'start_at', 'end_at', 'coupons_allowed',
];

/**
Expand All @@ -54,6 +55,7 @@ class Discount extends Model
'end_at' => 'datetime',
'is_global' => 'boolean',
'is_enabled' => 'boolean',
'coupons_allowed' => 'boolean',
];

/**
Expand Down
3 changes: 2 additions & 1 deletion src/Requests/CouponRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class CouponRequest extends FormRequest
* @var array<int, string>
*/
protected array $checkboxes = [
'can_cumulate', 'is_enabled', 'is_global',
'can_cumulate', 'is_enabled', 'is_global', 'discount_allowed',
];

/**
Expand Down Expand Up @@ -50,6 +50,7 @@ public function rules(): array
'is_enabled' => ['filled', 'boolean'],
'is_global' => ['filled', 'boolean'],
'is_fixed' => ['filled', 'boolean'],
'discount_allowed' => ['filled', 'boolean'],
];
}
}
Loading