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

Discount query improvements #1109

Merged
merged 7 commits into from
Jul 6, 2023
Merged
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
68 changes: 44 additions & 24 deletions docs/core/reference/discounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@
Lunar\Models\Discount
```

|Field|Description|Example|
|:-|:-|:-|
|`id`|||
|`name`|The given name for the discount||
|`handle`|The unique handle for the discount||
|`type`|The type of discount|`Lunar\DiscountTypes\Coupon`|
|`data`|JSON|Any data to be used by the type class
|`starts_at`|The datetime the discount starts (required)|
|`ends_at`|The datetime the discount expires, if `NULL` it won't expire|
|`uses`|How many uses the discount has had|
|`max_uses`|The maximum times this discount can be applied storewide|
|`priority`|The order of priority|
|`stop`|Whether this discount will stop others after propagating|
|`created_at`|||
|`updated_at`|||
| Field | Description | Example |
|:-------------|:-------------------------------------------------------------|:--------------------------------------|
| `id` | | |
| `name` | The given name for the discount | |
| `handle` | The unique handle for the discount | |
| `type` | The type of discount | `Lunar\DiscountTypes\Coupon` |
| `data` | JSON | Any data to be used by the type class
| `starts_at` | The datetime the discount starts (required) |
| `ends_at` | The datetime the discount expires, if `NULL` it won't expire |
| `uses` | How many uses the discount has had |
| `max_uses` | The maximum times this discount can be applied storewide |
| `priority` | The order of priority |
| `stop` | Whether this discount will stop others after propagating |
| `created_at` | | |
| `updated_at` | | |

### Creating a discount

Expand All @@ -45,6 +45,27 @@ Lunar\Models\Discount::create([
])
```

### Fetching a discount

The following scopes are available:

```php
/**
* Query for discounts using the `start_at` and `end_at` dates.
*/
Discount::active()->get();

/**
* Query for discounts where the `uses` column is less than the `max_uses` column or `max_uses` is null.
*/
Discount::usable()->get();

/**
* Query for discounts where the associated products are of a certain type, based on given product ids.
*/
Discount::products($productIds, $type = 'condition');
```

## Discount Purchasable

You can relate a purchasable to a discount via this model. Each has a type for whether it's a `condition` or `reward`.
Expand All @@ -56,14 +77,14 @@ You can relate a purchasable to a discount via this model. Each has a type for w
Lunar\Models\DiscountPurchasable
```

|Field|Description|Example|
|:-|:-|:-|
|`id`|||
|`discount_id`|||
|`purchasable_type`||`Lunar\Models\ProductVariant`
|`type`|`condition` or `reward`|
|`created_at`|||
|`updated_at`|||
| Field | Description | Example |
|:-------------------|:------------------------|:------------------------------|
| `id` | | |
| `discount_id` | | |
| `purchasable_type` | | `Lunar\Models\ProductVariant`
| `type` | `condition` or `reward` |
| `created_at` | | |
| `updated_at` | | |

### Relationships

Expand All @@ -72,7 +93,6 @@ Lunar\Models\DiscountPurchasable

### Adding your own Discount type


```php
namespace App\Discounts;

Expand Down
8 changes: 5 additions & 3 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Added `PaymentAttemptedEvent`
- Added `fingerprint` method to the `Cart` model.
- Added `checkFingerprint` method to the `Cart` model.
- Added `products` scope to the `Discount` model.
- Added `usable` scope to the `Discount` model.

### Changed

- Added `orders` relationship to the `LunarUser` trait.
- Added 'label' JSON field to `ProductOption` model.
- Added pipelines to PricingManager.
- Config to disable database migrations.

### Changed

- The `getThumbnail()` method on variants has been changed to allow for eager loading.
- The logic in the `CreateOrder` action has been extracted into pipelines.
- `order_id` has been deprecated on the `carts` table in favour of a `cart_id` column on the `orders` table.
Expand Down
48 changes: 19 additions & 29 deletions packages/core/src/Managers/DiscountManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public function getChannels(): Collection
/**
* Returns the available discounts.
*/
public function getDiscounts(): Collection
public function getDiscounts(Cart $cart = null): Collection
{
if ($this->channels->isEmpty() && $defaultChannel = Channel::getDefault()) {
$this->channel($defaultChannel);
Expand All @@ -126,33 +126,23 @@ public function getDiscounts(): Collection
$this->customerGroup($defaultGroup);
}

return Discount::active()->whereHas('channels', function ($query) {
$joinTable = (new Discount)->channels()->getTable();
$query->whereIn("{$joinTable}.channel_id", $this->channels->pluck('id'))
->where("{$joinTable}.enabled", true)
->where(function ($query) use ($joinTable) {
$query->whereNull("{$joinTable}.starts_at")
->orWhere("{$joinTable}.starts_at", '<=', now());
})
->where(function ($query) use ($joinTable) {
$query->whereNull("{$joinTable}.ends_at")
->orWhere("{$joinTable}.ends_at", '>', now());
});
})->whereHas('customerGroups', function ($query) {
$joinTable = (new Discount)->customerGroups()->getTable();

$query->whereIn("{$joinTable}.customer_group_id", $this->customerGroups->pluck('id'))
->where("{$joinTable}.enabled", true)
->where(function ($query) use ($joinTable) {
$query->whereNull("{$joinTable}.starts_at")
->orWhere("{$joinTable}.starts_at", '<=', now());
})
->where(function ($query) use ($joinTable) {
$query->whereNull("{$joinTable}.ends_at")
->orWhere("{$joinTable}.ends_at", '>', now());
});
})
->orderBy('priority', 'desc')
return Discount::active()
->usable()
->channel($this->channels)
->customerGroup($this->customerGroups)
->with([
'purchasables',
])
->when(
$cart,
fn ($query, $value) => $query->products(
$value->lines->pluck('purchasable.product_id')->filter()->values()
)
)->when(
$cart?->coupon_code,
fn ($query, $value) => $query->where('coupon', '=', $value)->orWhereNull('coupon'),
fn ($query, $value) => $query->whereNull('coupon')
)->orderBy('priority', 'desc')
->orderBy('id')
->get();
}
Expand Down Expand Up @@ -194,7 +184,7 @@ public function getApplied(): Collection
public function apply(Cart $cart): Cart
{
if (! $this->discounts) {
$this->discounts = $this->getDiscounts();
$this->discounts = $this->getDiscounts($cart);
}

foreach ($this->discounts as $discount) {
Expand Down
34 changes: 33 additions & 1 deletion packages/core/src/Models/Discount.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public function brands()
/**
* Return the active scope.
*
* @return void
* @return Builder
*/
public function scopeActive(Builder $query)
{
Expand All @@ -152,4 +152,36 @@ public function scopeActive(Builder $query)
->orWhere('ends_at', '>', now());
});
}

/**
* Return the products scope.
*
* @return Builder
*/
public function scopeProducts(Builder $query, iterable $productIds = [], string $type = null)
{
if (is_array($productIds)) {
$productIds = collect($productIds);
}

return $query->where(
fn ($subQuery) => $subQuery->whereDoesntHave('purchasables')
->orWhereHas('purchasables',
fn ($relation) => $relation->whereIn('purchasable_id', $productIds)
->wherePurchasableType(Product::class)
->when(
$type,
fn ($query) => $query->whereType($type)
)
)
);
}

public function scopeUsable(Builder $query)
{
return $query->where(function ($subQuery) {
$subQuery->whereRaw('uses < max_uses')
->orWhereNull('max_uses');
});
}
}
1 change: 1 addition & 0 deletions packages/core/tests/Unit/DiscountTypes/AmountOffTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,7 @@ public function can_apply_discount_without_coupon_code()
$discount = Discount::factory()->create([
'type' => AmountOff::class,
'name' => 'Test Coupon',
'coupon' => null,
'data' => [
'fixed_value' => true,
'fixed_values' => [
Expand Down
2 changes: 1 addition & 1 deletion packages/core/tests/Unit/DiscountTypes/BuyXGetYTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ public function can_discount_eligible_product()
],
]);

$discount->purchasableConditions()->create([
$discount->purchasableLimitations()->create([
'purchasable_type' => Product::class,
'purchasable_id' => $productA->id,
]);
Expand Down
116 changes: 116 additions & 0 deletions packages/core/tests/Unit/Managers/DiscountManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@
use Lunar\Base\DataTransferObjects\CartDiscount;
use Lunar\Base\DiscountManagerInterface;
use Lunar\DiscountTypes\AmountOff;
use Lunar\Facades\Discounts;
use Lunar\Managers\DiscountManager;
use Lunar\Models\Cart;
use Lunar\Models\CartLine;
use Lunar\Models\Channel;
use Lunar\Models\Currency;
use Lunar\Models\CustomerGroup;
use Lunar\Models\Discount;
use Lunar\Models\Price;
use Lunar\Models\Product;
use Lunar\Models\ProductVariant;
use Lunar\Tests\Stubs\TestDiscountType;
Expand Down Expand Up @@ -312,4 +316,116 @@ public function can_validate_coupons()
$manager->validateCoupon('20OFF')
);
}

/**
* @test
*
* @group moomoo
*/
public function can_get_discount_with_coupon()
{
$currency = Currency::factory()->create([
'default' => true,
]);

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

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

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

$purchasableA = ProductVariant::factory()->create();

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

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

$discountA = Discount::factory()->create([
'type' => AmountOff::class,
'name' => 'Test Discount A',
'coupon' => null,
'starts_at' => now(),
'data' => [
'fixed_value' => true,
'fixed_values' => [
'GBP' => 10,
],
],
]);

$discountA->channels()->attach([
$channel->id => [
'enabled' => true,
'starts_at' => now(),
],
]);

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

$discountB = Discount::factory()->create([
'type' => AmountOff::class,
'name' => 'Test Discount B',
'coupon' => null,
'starts_at' => now(),
'data' => [
'fixed_value' => true,
'fixed_values' => [
'GBP' => 10,
],
],
]);

$discountB->channels()->attach([
$channel->id => [
'enabled' => true,
'starts_at' => now(),
],
]);

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

$this->assertCount(2, Discounts::getDiscounts($cart));

$discountA->update([
'coupon' => 'ABCD',
]);

$discountB->update([
'coupon' => 'ABCDEF',
]);

$cart->update([
'coupon_code' => 'ABCDEF',
]);

$this->assertCount(1, Discounts::getDiscounts($cart->refresh()));
}
}
Loading
Loading