diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 85533a8dc..2cf59a56a 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -24,8 +24,7 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule) { - // $schedule->command('inspire') - // ->hourly(); + $schedule->command('booking:cron')->dailyAt('3:00'); } /** @@ -36,6 +35,7 @@ class Kernel extends ConsoleKernel protected function commands() { $this->load(__DIR__.'/Commands'); + $this->load(__DIR__.'/../../packages/Webkul/Core/src/Console/Commands'); require base_path('routes/console.php'); } diff --git a/packages/Webkul/Admin/webpack.mix.js b/packages/Webkul/Admin/webpack.mix.js index 10ebcb348..30c3baaf5 100755 --- a/packages/Webkul/Admin/webpack.mix.js +++ b/packages/Webkul/Admin/webpack.mix.js @@ -21,7 +21,7 @@ mix.js(__dirname + "/src/Resources/assets/js/app.js", "js/admin.js") processCssUrls: false }); -if (!mix.inProduction()) { +if (! mix.inProduction()) { mix.sourceMaps(); } diff --git a/packages/Webkul/BookingProduct/src/Database/Factories/BookingProductEventTicketFactory.php b/packages/Webkul/BookingProduct/src/Database/Factories/BookingProductEventTicketFactory.php new file mode 100644 index 000000000..09f79b311 --- /dev/null +++ b/packages/Webkul/BookingProduct/src/Database/Factories/BookingProductEventTicketFactory.php @@ -0,0 +1,19 @@ +define(BookingProductEventTicket::class, function (Faker $faker, array $attributes) { + return [ + 'price' => $faker->randomFloat(4, 3, 900), + 'qty' => $faker->randomNumber(2), + 'booking_product_id' => static function () { + return factory(BookingProduct::class)->create(['type' => 'event'])->id; + } + ]; +}); \ No newline at end of file diff --git a/packages/Webkul/BookingProduct/src/Database/Factories/BookingProductFactory.php b/packages/Webkul/BookingProduct/src/Database/Factories/BookingProductFactory.php new file mode 100644 index 000000000..3b74a8a1f --- /dev/null +++ b/packages/Webkul/BookingProduct/src/Database/Factories/BookingProductFactory.php @@ -0,0 +1,20 @@ +define(BookingProduct::class, function (Faker $faker, array $attributes) { + return [ + 'type' => array_rand(['event']), + 'qty' => $faker->randomNumber(2), + 'available_from' => Carbon::yesterday(), + 'available_to' => Carbon::tomorrow(), + 'product_id' => function () { + return factory(Product::class)->create(['type' => 'booking'])->id; + } + ]; +}); \ No newline at end of file diff --git a/packages/Webkul/BookingProduct/src/Providers/BookingProductServiceProvider.php b/packages/Webkul/BookingProduct/src/Providers/BookingProductServiceProvider.php index 92bec1ff8..b31d28d7f 100644 --- a/packages/Webkul/BookingProduct/src/Providers/BookingProductServiceProvider.php +++ b/packages/Webkul/BookingProduct/src/Providers/BookingProductServiceProvider.php @@ -2,6 +2,7 @@ namespace Webkul\BookingProduct\Providers; +use Illuminate\Database\Eloquent\Factory as EloquentFactory; use Illuminate\Support\ServiceProvider; class BookingProductServiceProvider extends ServiceProvider @@ -10,8 +11,9 @@ class BookingProductServiceProvider extends ServiceProvider * Bootstrap services. * * @return void + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ - public function boot() + public function boot(): void { $this->loadRoutesFrom(__DIR__ . '/../Http/front-routes.php'); @@ -26,6 +28,8 @@ class BookingProductServiceProvider extends ServiceProvider ], 'public'); $this->app->register(EventServiceProvider::class); + + $this->app->make(EloquentFactory::class)->load(__DIR__ . '/../Database/Factories'); } /** @@ -33,7 +37,7 @@ class BookingProductServiceProvider extends ServiceProvider * * @return void */ - public function register() + public function register(): void { $this->mergeConfigFrom( dirname(__DIR__) . '/Config/product_types.php', 'product_types' diff --git a/packages/Webkul/Core/src/Console/Commands/BookingCron.php b/packages/Webkul/Core/src/Console/Commands/BookingCron.php new file mode 100644 index 000000000..cc1a2b59e --- /dev/null +++ b/packages/Webkul/Core/src/Console/Commands/BookingCron.php @@ -0,0 +1,75 @@ +join('product_flat', 'booking_products.product_id', '=', 'product_flat.product_id') + ->where('booking_products.type', 'event') + ->where('booking_products.available_to', '<=', Carbon::now()) + ->where('product_flat.status', 1) + ->get(); + + if (count($expiredEvents) > 0) { + $attStatusId = Attribute::query()->select('id') + ->where('code', 'status') + ->first() + ->id; + + foreach ($expiredEvents as $expEvent) { + ProductAttributeValue::query()->where('product_id', $expEvent->product_id) + ->where('attribute_id', $attStatusId) + ->update(['boolean_value' => 0]); + + ProductFlat::query()->where('product_id', $expEvent->product_id) + ->update(['status' => 0]); + + Log::info('BookingCron: deactivated expired event', ['booking_product_id' => $expEvent->id, 'product_id' => $expEvent->product_id]); + } + $this->info('All expired events have been deactivated'); + } else { + Log::info('BookingCron: Did not find any expired events to be deactivated'); + $this->info('Did not find any expired events to be deactivated'); + } + } +} diff --git a/packages/Webkul/Core/src/Providers/CoreServiceProvider.php b/packages/Webkul/Core/src/Providers/CoreServiceProvider.php index b60cdd31e..cc6fe420b 100755 --- a/packages/Webkul/Core/src/Providers/CoreServiceProvider.php +++ b/packages/Webkul/Core/src/Providers/CoreServiceProvider.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Factory as EloquentFactory; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\Validator; use Illuminate\Foundation\AliasLoader; +use Webkul\Core\Console\Commands\BookingCron; use Webkul\Core\Core; use Webkul\Core\Exceptions\Handler; use Webkul\Core\Facades\Core as CoreFacade; @@ -22,6 +23,7 @@ class CoreServiceProvider extends ServiceProvider * Bootstrap services. * * @return void + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function boot() { @@ -86,7 +88,12 @@ class CoreServiceProvider extends ServiceProvider protected function registerCommands(): void { if ($this->app->runningInConsole()) { - $this->commands([BagistoVersion::class, Install::class, ExchangeRateUpdate::class]); + $this->commands([ + BagistoVersion::class, + Install::class, + ExchangeRateUpdate::class, + BookingCron::class + ]); } } @@ -96,6 +103,7 @@ class CoreServiceProvider extends ServiceProvider * @param string $path * * @return void + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ protected function registerEloquentFactoriesFrom($path): void { diff --git a/packages/Webkul/Product/src/Helpers/BundleOption.php b/packages/Webkul/Product/src/Helpers/BundleOption.php index fcfca3148..406a12d63 100644 --- a/packages/Webkul/Product/src/Helpers/BundleOption.php +++ b/packages/Webkul/Product/src/Helpers/BundleOption.php @@ -36,7 +36,13 @@ class BundleOption extends AbstractProduct $options = []; foreach ($this->product->bundle_options as $option) { - $options[$option->id] = $this->getOptionItemData($option); + $data = $this->getOptionItemData($option); + + if (! count($data['products'])) { + continue; + } + + $options[$option->id] = $data; } usort ($options, function($a, $b) { @@ -79,6 +85,10 @@ class BundleOption extends AbstractProduct $products = []; foreach ($option->bundle_option_products as $index => $bundleOptionProduct) { + if (! $bundleOptionProduct->product->getTypeInstance()->isSaleable()) { + continue; + } + $products[$bundleOptionProduct->id] = [ 'id' => $bundleOptionProduct->id, 'qty' => $bundleOptionProduct->qty, @@ -110,15 +120,15 @@ class BundleOption extends AbstractProduct { $products = []; - $products[$product->id] = [ - 'id' => $product->id, - 'qty' => $product->qty, - 'price' => $product->product->getTypeInstance()->getProductPrices(), - 'name' => $product->product->name, - 'product_id' => $product->product_id, - 'is_default' => $product->is_default, - 'sort_order' => $product->sort_order, - ]; + $products[$product->id] = [ + 'id' => $product->id, + 'qty' => $product->qty, + 'price' => $product->product->getTypeInstance()->getProductPrices(), + 'name' => $product->product->name, + 'product_id' => $product->product_id, + 'is_default' => $product->is_default, + 'sort_order' => $product->sort_order, + ]; usort ($products, function($a, $b) { if ($a['sort_order'] == $b['sort_order']) { diff --git a/packages/Webkul/Product/src/Type/Bundle.php b/packages/Webkul/Product/src/Type/Bundle.php index 3d2d78600..9ad09181c 100644 --- a/packages/Webkul/Product/src/Type/Bundle.php +++ b/packages/Webkul/Product/src/Type/Bundle.php @@ -234,6 +234,10 @@ class Bundle extends AbstractType $optionPrices = []; foreach ($option->bundle_option_products as $index => $bundleOptionProduct) { + if (! $bundleOptionProduct->product->getTypeInstance()->isSaleable()) { + continue; + } + $optionPrices[] = $bundleOptionProduct->qty * ($minPrice ? $bundleOptionProduct->product->getTypeInstance()->getMinimalPrice() @@ -271,6 +275,10 @@ class Bundle extends AbstractType foreach ($this->product->bundle_options as $option) { foreach ($option->bundle_option_products as $index => $bundleOptionProduct) { + if (! $bundleOptionProduct->product->getTypeInstance()->isSaleable()) { + continue; + } + if (in_array($option->type, ['multiselect', 'checkbox'])) { if (! isset($optionPrices[$option->id][0])) { $optionPrices[$option->id][0] = 0; @@ -304,6 +312,10 @@ class Bundle extends AbstractType foreach ($this->product->bundle_options as $option) { foreach ($option->bundle_option_products as $index => $bundleOptionProduct) { + if (! $bundleOptionProduct->product->getTypeInstance()->isSaleable()) { + continue; + } + if (in_array($option->type, ['multiselect', 'checkbox'])) { if (! isset($optionPrices[$option->id][0])) { $optionPrices[$option->id][0] = 0; @@ -427,6 +439,10 @@ class Bundle extends AbstractType foreach ($this->getCartChildProducts($data) as $productId => $data) { $product = $this->productRepository->find($productId); + if (! $product->getTypeInstance()->isSaleable()) { + continue; + } + $cartProduct = $product->getTypeInstance()->prepareForCart(array_merge($data, ['parent_id' => $this->product->id])); if (is_string($cartProduct)) { @@ -473,6 +489,10 @@ class Bundle extends AbstractType 'product_bundle_option_id' => $optionId, ]); + if (! $optionProduct->product->getTypeInstance()->isSaleable()) { + continue; + } + $qty = $data['bundle_option_qty'][$optionId] ?? $optionProduct->qty; if (! isset($products[$optionProduct->product_id])) { diff --git a/tests/_support/Helper/DataMocker.php b/tests/_support/Helper/DataMocker.php index 536e2dbef..cb351ab5a 100644 --- a/tests/_support/Helper/DataMocker.php +++ b/tests/_support/Helper/DataMocker.php @@ -15,7 +15,7 @@ class DataMocker extends Module /** * Get an instance of the faker * - * @return Generator + * @return \Faker\Generator * * @author florianbosdorff */ diff --git a/tests/unit.suite.yml b/tests/unit.suite.yml index ba6191f9a..cab8a5bc2 100644 --- a/tests/unit.suite.yml +++ b/tests/unit.suite.yml @@ -8,6 +8,7 @@ modules: - Asserts - Filesystem - \Helper\Unit + - \Helper\DataMocker - Webkul\Core\Helpers\Laravel5Helper: environment_file: .env.testing run_database_migrations: true diff --git a/tests/unit/Core/Commands/BookingCronCest.php b/tests/unit/Core/Commands/BookingCronCest.php new file mode 100644 index 000000000..455fcf2ec --- /dev/null +++ b/tests/unit/Core/Commands/BookingCronCest.php @@ -0,0 +1,55 @@ +fake()->numberBetween(2, 6); + + for ($i=0; $i<$index; $i++) { + $products[$i] = $I->haveProduct(Laravel5Helper::VIRTUAL_PRODUCT); + Product::query()->where('id', $products[$i]->id)->update(['type' => 'booking']); + + if ($I->fake()->randomDigitNotNull <= 5) { + $availableTo = Carbon::now()->subMinutes($I->fake()->numberBetween(2, 59)); + } else { + $availableTo = Carbon::now()->addMinutes($I->fake()->numberBetween(2, 59)); + } + + $bookingProducts[$i] = $I->have(BookingProduct::class, [ + 'type' => 'event', + 'available_to' => $availableTo->toDateTimeString(), + 'product_id' => $products[$i]->id, + ]); + + $I->have(BookingProductEventTicket::class, + ['booking_product_id' => $bookingProducts[$i]->id]); + + $products[$i]->refresh(); + $I->assertNotFalse($products[$i]->status); + } + + $I->callArtisan('booking:cron'); + + for ($i=0; $i<$index; $i++) { + $products[$i]->refresh(); + + if ($bookingProducts[$i]->available_to < Carbon::now()) { + $I->assertEquals(0, $products[$i]->status); + } else { + $I->assertEquals(1, $products[$i]->status); + } + } + } +} +