From 9cc76c41964b09fba0718edeef0a47cb04ef935b Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Tue, 5 Mar 2024 08:52:21 +0000 Subject: [PATCH] Chore - Merge 0.8 into 1.0 (#1601) --- .editorconfig | 15 +++++ .gitattributes | 17 ++++++ .gitignore | 20 ++++--- docs/core/reference/carts.md | 32 ++++++++++- docs/core/reference/discounts.md | 8 +++ docs/core/upgrading.md | 5 ++ packages/core/config/cart.php | 23 ++++++++ .../database/state/ConvertTaxbreakdown.php | 1 - packages/core/src/Base/Casts/Price.php | 10 ++-- .../core/src/Base/Traits/LogsActivity.php | 25 +++++++- .../core/src/Console/Commands/PruneCarts.php | 57 +++++++++++++++++++ packages/core/src/LunarServiceProvider.php | 9 +++ .../core/src/Managers/DiscountManager.php | 24 ++++++-- .../src/Managers/StorefrontSessionManager.php | 14 ++--- packages/core/src/Models/Discount.php | 21 ++++--- packages/core/src/Models/Order.php | 9 ++- packages/core/src/Models/OrderAddress.php | 3 +- packages/core/src/Models/Product.php | 3 +- .../src/Pipelines/CartPrune/PruneAfter.php | 21 +++++++ .../Pipelines/CartPrune/WhereNotMerged.php | 16 ++++++ .../src/Pipelines/CartPrune/WithoutOrders.php | 16 ++++++ 21 files changed, 309 insertions(+), 40 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 packages/core/src/Console/Commands/PruneCarts.php create mode 100644 packages/core/src/Pipelines/CartPrune/PruneAfter.php create mode 100644 packages/core/src/Pipelines/CartPrune/WhereNotMerged.php create mode 100644 packages/core/src/Pipelines/CartPrune/WithoutOrders.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..d5f28a4669 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_size = 4 +indent_style = space +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..3807fc3dfe --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +/docs export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/monorepo-builder.php export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml export-ignore +/pint.json export-ignore diff --git a/.gitignore b/.gitignore index 19d8b246d4..cdcd513659 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,15 @@ -/vendor/ -composer.lock -/vendor -.phpunit.result.cache -.phpunit.cache/ +/.DS_Store +/.fleet /.idea +/.nova +/.phpunit.cache /.vscode -node_modules/ -yarn.lock +/node_modules +/vendor + +.phpunit.result.cache +composer.lock package-lock.json -.DS_Store -.nova phpstan.neon +Thumbs.db +yarn.lock diff --git a/docs/core/reference/carts.md b/docs/core/reference/carts.md index 2074df3e1b..d45dbe17d3 100644 --- a/docs/core/reference/carts.md +++ b/docs/core/reference/carts.md @@ -279,7 +279,7 @@ CartSession::add($purchasable, $quantity); ```php CartSession::addLines([ [ - 'id' => 1, + 'purchasable' => \Lunar\Models\ProductVariant::find(123), 'quantity' => 25, 'meta' => ['foo' => 'bar'], ], @@ -466,3 +466,33 @@ return [ ``` In most cases you won't need to change this. + +## Pruning cart data + +Over time you will experience a build up of carts in your database that you may want to regularly remove. + +You can enable automatic removal of these carts using the `lunar.carts.prune_tables.enabled` config. By setting this to `true` any carts without an order associated will be removed after 90 days. + +You can change the number of days carts are retained for using the `lunar.carts.prune_tables. prune_interval` config. + +If you have specific needs around pruning you can also change the `lunar.carts.prune_tables.pipelines` array to determine what carts should be removed. + + + +```php +return [ + // ... + 'prune_tables' => [ + + 'enabled' => false, + + 'pipelines' => [ + Lunar\Pipelines\CartPrune\PruneAfter::class, + Lunar\Pipelines\CartPrune\WithoutOrders::class, + ], + + 'prune_interval' => 90, // days + + ], +]; +``` diff --git a/docs/core/reference/discounts.md b/docs/core/reference/discounts.md index 17a06e7e72..52a2782e5e 100644 --- a/docs/core/reference/discounts.md +++ b/docs/core/reference/discounts.md @@ -66,6 +66,14 @@ Discount::usable()->get(); Discount::products($productIds, $type = 'condition'); ``` +### Resetting the discount cache + +For performance reasons the applicable discounts are cached per request. If you need to reset this cache (for example after adding a discount code) you should call `resetDiscounts()`: + +```php +Discount::resetDiscounts(); +``` + ## 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`. diff --git a/docs/core/upgrading.md b/docs/core/upgrading.md index c78bc84a4d..0e4c8b64bc 100644 --- a/docs/core/upgrading.md +++ b/docs/core/upgrading.md @@ -18,6 +18,7 @@ php artisan migrate Lunar currently provides bug fixes and security updates for only the latest minor release, e.g. `0.7`. + ## 1.0 ### High Impact @@ -58,6 +59,10 @@ public Collection $tiered, public Collection $priceBreaks, ``` +## 0.8 + +No significant changes. + ## 0.7 ### High Impact diff --git a/packages/core/config/cart.php b/packages/core/config/cart.php index d444562051..00b5244ef7 100644 --- a/packages/core/config/cart.php +++ b/packages/core/config/cart.php @@ -150,4 +150,27 @@ 'lines.purchasable.product', 'lines.cart.currency', ], + + /* + |-------------------------------------------------------------------------- + | Prune carts + |-------------------------------------------------------------------------- + | + | Should the cart models be pruned to prevent data build up and + | some settings controlling how pruning should be determined + | + */ + 'prune_tables' => [ + + 'enabled' => false, + + 'pipelines' => [ + Lunar\Pipelines\CartPrune\PruneAfter::class, + Lunar\Pipelines\CartPrune\WithoutOrders::class, + Lunar\Pipelines\CartPrune\WhereNotMerged::class, + ], + + 'prune_interval' => 90, // days + + ], ]; diff --git a/packages/core/database/state/ConvertTaxbreakdown.php b/packages/core/database/state/ConvertTaxbreakdown.php index a623b8743c..dec8623f4e 100644 --- a/packages/core/database/state/ConvertTaxbreakdown.php +++ b/packages/core/database/state/ConvertTaxbreakdown.php @@ -74,7 +74,6 @@ public function run() }); }); } - }); } diff --git a/packages/core/src/Base/Casts/Price.php b/packages/core/src/Base/Casts/Price.php index 1f59878679..077edf1f5f 100644 --- a/packages/core/src/Base/Casts/Price.php +++ b/packages/core/src/Base/Casts/Price.php @@ -22,10 +22,12 @@ public function get($model, $key, $value, $attributes) { $currency = $model->currency ?: Currency::getDefault(); - /** - * Make it an integer based on currency requirements. - */ - $value = preg_replace('/[^0-9]/', '', $value); + if (! is_null($value)) { + /** + * Make it an integer based on currency requirements. + */ + $value = preg_replace('/[^0-9]/', '', $value); + } Validator::make([ $key => $value, diff --git a/packages/core/src/Base/Traits/LogsActivity.php b/packages/core/src/Base/Traits/LogsActivity.php index c43da2754f..72a81a9eb8 100644 --- a/packages/core/src/Base/Traits/LogsActivity.php +++ b/packages/core/src/Base/Traits/LogsActivity.php @@ -2,6 +2,7 @@ namespace Lunar\Base\Traits; +use Illuminate\Support\Arr; use Spatie\Activitylog\LogOptions; use Spatie\Activitylog\Traits\LogsActivity as SpatieLogsActivity; @@ -9,6 +10,8 @@ trait LogsActivity { use SpatieLogsActivity; + public static array $logExcept = []; + /** * Get the log options for the activity log. */ @@ -17,7 +20,25 @@ public function getActivitylogOptions(): LogOptions return LogOptions::defaults() ->useLogName('lunar') ->logAll() - ->dontSubmitEmptyLogs() - ->logExcept(['updated_at']); + ->logExcept(array_merge(['updated_at'], static::getActivitylogExcept())) + ->logOnlyDirty() + ->dontSubmitEmptyLogs(); + } + + public static function addActivitylogExcept(array|string $fields) + { + $fields = Arr::wrap($fields); + + static::$logExcept = array_merge(static::$logExcept, $fields); + } + + public static function getDefaultLogExcept(): array + { + return []; + } + + public static function getActivitylogExcept(): array + { + return array_merge(static::getDefaultLogExcept(), static::$logExcept); } } diff --git a/packages/core/src/Console/Commands/PruneCarts.php b/packages/core/src/Console/Commands/PruneCarts.php new file mode 100644 index 0000000000..2fdb0bdbac --- /dev/null +++ b/packages/core/src/Console/Commands/PruneCarts.php @@ -0,0 +1,57 @@ +info('Beginning prune'); + + $query = Cart::query(); + + $carts = app(Pipeline::class) + ->send($query) + ->through( + config('lunar.cart.prune_tables.pipelines', []) + )->then(function ($query) { + $query->chunk(200, function ($carts) { + $carts->each(fn ($cart) => $this->pruneCart($cart)); + }); + }); + + $this->info('Prune complete'); + } + + public function pruneCart(Cart $cart) + { + Cart::where('merged_id', $cart->id)->get()->each(fn ($merged) => $this->pruneCart($merged)); + + $cart->lines()->delete(); + $cart->addresses()->delete(); + $cart->delete(); + } +} diff --git a/packages/core/src/LunarServiceProvider.php b/packages/core/src/LunarServiceProvider.php index 77329f07ac..547a23a302 100644 --- a/packages/core/src/LunarServiceProvider.php +++ b/packages/core/src/LunarServiceProvider.php @@ -5,6 +5,7 @@ use Cartalyst\Converter\Laravel\Facades\Converter; use Illuminate\Auth\Events\Login; use Illuminate\Auth\Events\Logout; +use Illuminate\Console\Scheduling\Schedule; use Illuminate\Database\Events\MigrationsEnded; use Illuminate\Database\Events\MigrationsStarted; use Illuminate\Database\Events\NoPendingMigrations; @@ -39,6 +40,7 @@ use Lunar\Console\Commands\Import\AddressData; use Lunar\Console\Commands\MigrateGetCandy; use Lunar\Console\Commands\Orders\SyncNewCustomerOrders; +use Lunar\Console\Commands\PruneCarts; use Lunar\Console\Commands\ScoutIndexerCommand; use Lunar\Console\InstallLunar; use Lunar\Database\State\ConvertProductTypeAttributesToProducts; @@ -210,7 +212,14 @@ public function boot(): void ScoutIndexerCommand::class, MigrateGetCandy::class, SyncNewCustomerOrders::class, + PruneCarts::class, ]); + + if (config('lunar.cart.prune_tables.enabled', false)) { + $this->callAfterResolving(Schedule::class, function (Schedule $schedule) { + $schedule->command('lunar:prune:carts')->daily(); + }); + } } Arr::macro('permutate', [\Lunar\Utils\Arr::class, 'permutate']); diff --git a/packages/core/src/Managers/DiscountManager.php b/packages/core/src/Managers/DiscountManager.php index 0f629aafce..40c8a22026 100644 --- a/packages/core/src/Managers/DiscountManager.php +++ b/packages/core/src/Managers/DiscountManager.php @@ -138,18 +138,27 @@ public function getDiscounts(Cart $cart = null): Collection function ($query, $value) { return $query->where(function ($query) use ($value) { return $query->where(fn ($query) => $query->products( - $value->lines->pluck('purchasable.product_id')->filter()->values() + $value->lines->pluck('purchasable.product_id')->filter()->values(), + ['condition', 'limitation'] ) ) ->orWhere(fn ($query) => $query->productVariants( - $value->lines->pluck('purchasable.id')->filter()->values() + $value->lines->pluck('purchasable.id')->filter()->values(), + ['condition', 'limitation'] ) ); }); } - )->when( + ) + ->when( $cart?->coupon_code, - fn ($query, $value) => $query->where('coupon', '=', $value)->orWhere(fn ($query) => $query->whereNull('coupon')->orWhere('coupon', '')), + function ($query, $value) { + return $query->where(function ($query) use ($value) { + $query->where('coupon', $value) + ->orWhereNull('coupon') + ->orWhere('coupon', ''); + }); + }, fn ($query, $value) => $query->whereNull('coupon')->orWhere('coupon', '') )->orderBy('priority', 'desc') ->orderBy('id') @@ -203,6 +212,13 @@ public function apply(Cart $cart): Cart return $cart; } + public function resetDiscounts(): self + { + $this->discounts = null; + + return $this; + } + public function validateCoupon(string $coupon): bool { return app( diff --git a/packages/core/src/Managers/StorefrontSessionManager.php b/packages/core/src/Managers/StorefrontSessionManager.php index 04b8bc93db..9e3fa9e04a 100644 --- a/packages/core/src/Managers/StorefrontSessionManager.php +++ b/packages/core/src/Managers/StorefrontSessionManager.php @@ -80,7 +80,7 @@ public function initCustomerGroups() ); if ($this->customerGroups?->count()) { - if (! $groupHandles) { + if ($groupHandles->isEmpty()) { return $this->setCustomerGroups( $this->customerGroups ); @@ -89,16 +89,14 @@ public function initCustomerGroups() return $this->customerGroups; } - if (! $this->customerGroups?->count()) { - return $this->setCustomerGroups( - collect([ - CustomerGroup::getDefault(), - ]) - ); + if (! $groupHandles->isEmpty()) { + return $this->customerGroups = CustomerGroup::whereIn('handle', $groupHandles)->get(); } return $this->setCustomerGroups( - CustomerGroup::whereIn('handle', $groupHandles)->get() + collect([ + CustomerGroup::getDefault(), + ]) ); } diff --git a/packages/core/src/Models/Discount.php b/packages/core/src/Models/Discount.php index 2592fdc3a0..cbef9b384a 100644 --- a/packages/core/src/Models/Discount.php +++ b/packages/core/src/Models/Discount.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Arr; use Lunar\Base\BaseModel; use Lunar\Base\Traits\HasChannels; use Lunar\Base\Traits\HasCustomerGroups; @@ -192,20 +193,22 @@ public function scopeActive(Builder $query): Builder /** * Return the products scope. */ - public function scopeProducts(Builder $query, iterable $productIds = [], string $type = null): Builder + public function scopeProducts(Builder $query, iterable $productIds = [], array|string $types = []): Builder { if (is_array($productIds)) { $productIds = collect($productIds); } + $types = Arr::wrap($types); + return $query->where( - fn ($subQuery) => $subQuery->whereDoesntHave('purchasables') + fn ($subQuery) => $subQuery->whereDoesntHave('purchasables', fn ($query) => $query->when($types, fn ($query) => $query->whereIn('type', $types))) ->orWhereHas('purchasables', fn ($relation) => $relation->whereIn('purchasable_id', $productIds) ->wherePurchasableType(Product::class) ->when( - $type, - fn ($query) => $query->whereType($type) + $types, + fn ($query) => $query->whereIn('type', $types) ) ) ); @@ -214,20 +217,22 @@ public function scopeProducts(Builder $query, iterable $productIds = [], string /** * Return the product variants scope. */ - public function scopeProductVariants(Builder $query, iterable $variantIds = [], string $type = null): Builder + public function scopeProductVariants(Builder $query, iterable $variantIds = [], array|string $types = []): Builder { if (is_array($variantIds)) { $variantIds = collect($variantIds); } + $types = Arr::wrap($types); + return $query->where( - fn ($subQuery) => $subQuery->whereDoesntHave('purchasables') + fn ($subQuery) => $subQuery->whereDoesntHave('purchasables', fn ($query) => $query->when($types, fn ($query) => $query->whereIn('type', $types))) ->orWhereHas('purchasables', fn ($relation) => $relation->whereIn('purchasable_id', $variantIds) ->wherePurchasableType(ProductVariant::class) ->when( - $type, - fn ($query) => $query->whereType($type) + $types, + fn ($query) => $query->whereIn('type', $types) ) ) ); diff --git a/packages/core/src/Models/Order.php b/packages/core/src/Models/Order.php index 7b859bf1a0..6addde1d74 100644 --- a/packages/core/src/Models/Order.php +++ b/packages/core/src/Models/Order.php @@ -35,7 +35,7 @@ * @property int $tax_total * @property int $total * @property ?string $notes - * @property string $currency + * @property string $currency_code * @property ?string $compare_currency_code * @property float $exchange_rate * @property ?\Illuminate\Support\Carbon $placed_at @@ -244,4 +244,11 @@ public function isPlaced(): bool { return ! blank($this->placed_at); } + + public static function getDefaultLogExcept(): array + { + return [ + 'status', + ]; + } } diff --git a/packages/core/src/Models/OrderAddress.php b/packages/core/src/Models/OrderAddress.php index e5e285e5a4..59bb73908f 100644 --- a/packages/core/src/Models/OrderAddress.php +++ b/packages/core/src/Models/OrderAddress.php @@ -72,8 +72,9 @@ protected static function newFactory(): OrderAddressFactory 'delivery_instructions', 'contact_email', 'contact_phone', - 'meta', 'type', + 'shipping_option', + 'meta', ]; /** diff --git a/packages/core/src/Models/Product.php b/packages/core/src/Models/Product.php index 0133f37a9b..f313c3a152 100644 --- a/packages/core/src/Models/Product.php +++ b/packages/core/src/Models/Product.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -129,7 +130,7 @@ public function variants(): HasMany public function collections(): BelongsToMany { return $this->belongsToMany( - Collection::class, + \Lunar\Models\Collection::class, config('lunar.database.table_prefix').'collection_product' )->withPivot(['position'])->withTimestamps(); } diff --git a/packages/core/src/Pipelines/CartPrune/PruneAfter.php b/packages/core/src/Pipelines/CartPrune/PruneAfter.php new file mode 100644 index 0000000000..67abcb8da7 --- /dev/null +++ b/packages/core/src/Pipelines/CartPrune/PruneAfter.php @@ -0,0 +1,21 @@ +where('updated_at', '<=', now()->subDays($days)) + ->whereDoesntHave('lines', function ($query) use ($days) { + $query->where('updated_at', '>', now()->subDays($days)); + }); + + return $next($query); + } +} diff --git a/packages/core/src/Pipelines/CartPrune/WhereNotMerged.php b/packages/core/src/Pipelines/CartPrune/WhereNotMerged.php new file mode 100644 index 0000000000..e497736afe --- /dev/null +++ b/packages/core/src/Pipelines/CartPrune/WhereNotMerged.php @@ -0,0 +1,16 @@ +unmerged(); + + return $next($query); + } +} diff --git a/packages/core/src/Pipelines/CartPrune/WithoutOrders.php b/packages/core/src/Pipelines/CartPrune/WithoutOrders.php new file mode 100644 index 0000000000..1b2de9c63f --- /dev/null +++ b/packages/core/src/Pipelines/CartPrune/WithoutOrders.php @@ -0,0 +1,16 @@ +whereDoesntHave('orders'); + + return $next($query); + } +}