diff --git a/docs/core/reference/carts.md b/docs/core/reference/carts.md index 2074df3e1b..837e79cb5a 100644 --- a/docs/core/reference/carts.md +++ b/docs/core/reference/carts.md @@ -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/packages/core/config/cart.php b/packages/core/config/cart.php index d444562051..d6e4abfe82 100644 --- a/packages/core/config/cart.php +++ b/packages/core/config/cart.php @@ -150,4 +150,26 @@ '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, + ], + + 'prune_interval' => 90, // days + + ], ]; diff --git a/packages/core/src/Console/Commands/PruneCarts.php b/packages/core/src/Console/Commands/PruneCarts.php new file mode 100644 index 0000000000..a950777e13 --- /dev/null +++ b/packages/core/src/Console/Commands/PruneCarts.php @@ -0,0 +1,54 @@ +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(function ($cart) { + Cart::where('merged_id', $cart->id)->update(['merged_id' => null]); + + $cart->lines()->delete(); + $cart->addresses()->delete(); + $cart->delete(); + }); + }); + }); + + $this->info('Prune complete'); + } +} diff --git a/packages/core/src/LunarServiceProvider.php b/packages/core/src/LunarServiceProvider.php index e74400fca4..b651a7c8a7 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; @@ -204,7 +206,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/Pipelines/CartPrune/PruneAfter.php b/packages/core/src/Pipelines/CartPrune/PruneAfter.php new file mode 100644 index 0000000000..9efa40aca3 --- /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/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); + } +} diff --git a/packages/core/tests/Unit/Console/CartPruneTest.php b/packages/core/tests/Unit/Console/CartPruneTest.php new file mode 100644 index 0000000000..0d278afb6f --- /dev/null +++ b/packages/core/tests/Unit/Console/CartPruneTest.php @@ -0,0 +1,45 @@ +create(); + $channel = Channel::factory()->create(); + + $cart = Cart::create([ + 'currency_id' => $currency->id, + 'channel_id' => $channel->id, + 'meta' => ['foo' => 'bar'], + 'updated_at' => Carbon::now()->subDay(120), + ]); + + $cart = Cart::create([ + 'currency_id' => $currency->id, + 'channel_id' => $channel->id, + 'meta' => ['foo' => 'bar'], + 'updated_at' => Carbon::now()->subDay(20), + ]); + + $this->assertCount(2, Cart::query()->get()); + + $this->artisan('lunar:prune:carts'); + + $this->assertCount(1, Cart::query()->get()); + } +}