From 1b6fee90ef11b0a20574955eb04b17582b7775e0 Mon Sep 17 00:00:00 2001 From: hexu Date: Fri, 27 Mar 2020 12:03:18 +0800 Subject: [PATCH 01/54] Add repository cacheable trait self::findByField does not work for repository cache, so I change it to $this->findByField --- packages/Webkul/Core/src/Eloquent/Repository.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/Webkul/Core/src/Eloquent/Repository.php b/packages/Webkul/Core/src/Eloquent/Repository.php index a843ec976..0249dd76f 100755 --- a/packages/Webkul/Core/src/Eloquent/Repository.php +++ b/packages/Webkul/Core/src/Eloquent/Repository.php @@ -2,11 +2,16 @@ namespace Webkul\Core\Eloquent; +use Prettus\Repository\Contracts\CacheableInterface; use Prettus\Repository\Eloquent\BaseRepository; use Illuminate\Database\Eloquent\Model; use Illuminate\Container\Container as App; +use Prettus\Repository\Traits\CacheableRepository; -abstract class Repository extends BaseRepository { + +abstract class Repository extends BaseRepository implements CacheableInterface { + + use CacheableRepository; /** * Find data by field and value @@ -18,7 +23,7 @@ abstract class Repository extends BaseRepository { */ public function findOneByField($field, $value = null, $columns = ['*']) { - $model = parent::findByField($field, $value, $columns = ['*']); + $model = $this->findByField($field, $value, $columns = ['*']); return $model->first(); } @@ -33,7 +38,7 @@ abstract class Repository extends BaseRepository { */ public function findOneWhere(array $where, $columns = ['*']) { - $model = parent::findWhere($where, $columns); + $model = $this->findWhere($where, $columns); return $model->first(); } @@ -132,4 +137,4 @@ abstract class Repository extends BaseRepository { { return $this->model; } -} \ No newline at end of file +} From 7a9f099b7bc26324836339a9952c61ae50d2fb64 Mon Sep 17 00:00:00 2001 From: Annika Wolff Date: Thu, 16 Apr 2020 10:25:45 +0200 Subject: [PATCH 02/54] remove channel from tax category --- .../views/tax/tax-categories/create.blade.php | 18 +--------- .../views/tax/tax-categories/edit.blade.php | 16 --------- .../Database/Factories/TaxCategoryFactory.php | 3 -- ...30351_remove_channel_from_tax_category.php | 34 +++++++++++++++++++ .../Controllers/TaxCategoryController.php | 2 -- .../Webkul/Tax/src/Models/TaxCategory.php | 1 - public/mix-manifest.json | 4 +++ 7 files changed, 39 insertions(+), 39 deletions(-) create mode 100644 packages/Webkul/Tax/src/Database/Migrations/2020_04_16_130351_remove_channel_from_tax_category.php create mode 100644 public/mix-manifest.json diff --git a/packages/Webkul/Admin/src/Resources/views/tax/tax-categories/create.blade.php b/packages/Webkul/Admin/src/Resources/views/tax/tax-categories/create.blade.php index 336dbcabe..a09ec4cfb 100755 --- a/packages/Webkul/Admin/src/Resources/views/tax/tax-categories/create.blade.php +++ b/packages/Webkul/Admin/src/Resources/views/tax/tax-categories/create.blade.php @@ -26,22 +26,6 @@
@csrf() -
- - - - - @{{ errors.first('channel') }} -
-
@@ -67,7 +51,7 @@
- +
diff --git a/packages/Webkul/Admin/src/Resources/views/tax/tax-categories/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/tax/tax-categories/edit.blade.php index 178246b22..3a30a0425 100755 --- a/packages/Webkul/Admin/src/Resources/views/tax/tax-categories/edit.blade.php +++ b/packages/Webkul/Admin/src/Resources/views/tax/tax-categories/edit.blade.php @@ -27,22 +27,6 @@
@csrf() @method('PUT') -
- - - - - @{{ errors.first('channel') }} -
-
diff --git a/packages/Webkul/Tax/src/Database/Factories/TaxCategoryFactory.php b/packages/Webkul/Tax/src/Database/Factories/TaxCategoryFactory.php index b704d1d84..a0aab3cab 100644 --- a/packages/Webkul/Tax/src/Database/Factories/TaxCategoryFactory.php +++ b/packages/Webkul/Tax/src/Database/Factories/TaxCategoryFactory.php @@ -7,9 +7,6 @@ use Webkul\Tax\Models\TaxCategory; $factory->define(TaxCategory::class, function (Faker $faker) { return [ - 'channel_id' => function () { - return core()->getCurrentChannel()->id; - }, 'code' => $faker->uuid, 'name' => $faker->words(2, true), 'description' => $faker->sentence(10), diff --git a/packages/Webkul/Tax/src/Database/Migrations/2020_04_16_130351_remove_channel_from_tax_category.php b/packages/Webkul/Tax/src/Database/Migrations/2020_04_16_130351_remove_channel_from_tax_category.php new file mode 100644 index 000000000..cc5f08768 --- /dev/null +++ b/packages/Webkul/Tax/src/Database/Migrations/2020_04_16_130351_remove_channel_from_tax_category.php @@ -0,0 +1,34 @@ +dropForeign('tax_categories_channel_id_foreign'); + $table->dropColumn('channel_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('tax_categories', function (Blueprint $table) { + $table->integer('channel_id')->unsigned()->after('id'); + $table->foreign('channel_id')->references('id')->on('channels')->onDelete('cascade'); + }); + } +} diff --git a/packages/Webkul/Tax/src/Http/Controllers/TaxCategoryController.php b/packages/Webkul/Tax/src/Http/Controllers/TaxCategoryController.php index 82a3101c2..4c1f6cf11 100755 --- a/packages/Webkul/Tax/src/Http/Controllers/TaxCategoryController.php +++ b/packages/Webkul/Tax/src/Http/Controllers/TaxCategoryController.php @@ -68,7 +68,6 @@ class TaxCategoryController extends Controller $data = request()->input(); $this->validate(request(), [ - 'channel_id' => 'required|numeric', 'code' => 'required|string|unique:tax_categories,code', 'name' => 'required|string', 'description' => 'required|string', @@ -111,7 +110,6 @@ class TaxCategoryController extends Controller public function update($id) { $this->validate(request(), [ - 'channel_id' => 'required|numeric', 'code' => 'required|string|unique:tax_categories,code,' . $id, 'name' => 'required|string', 'description' => 'required|string', diff --git a/packages/Webkul/Tax/src/Models/TaxCategory.php b/packages/Webkul/Tax/src/Models/TaxCategory.php index e2322f758..011e37106 100755 --- a/packages/Webkul/Tax/src/Models/TaxCategory.php +++ b/packages/Webkul/Tax/src/Models/TaxCategory.php @@ -16,7 +16,6 @@ class TaxCategory extends Model implements TaxCategoryContract protected $table = 'tax_categories'; protected $fillable = [ - 'channel_id', 'code', 'name', 'description', diff --git a/public/mix-manifest.json b/public/mix-manifest.json new file mode 100644 index 000000000..2d6011713 --- /dev/null +++ b/public/mix-manifest.json @@ -0,0 +1,4 @@ +{ + "/js/app.js": "/js/app.js", + "/css/app.css": "/css/app.css" +} From 3729b9daafabe454584916f46b7a43cd0d686165 Mon Sep 17 00:00:00 2001 From: Annika Wolff Date: Thu, 16 Apr 2020 14:05:42 +0200 Subject: [PATCH 03/54] introduce channel and channel inventory source factories --- .../src/Database/Factories/ChannelFactory.php | 39 +++++++++++++++++++ .../ChannelInventorySourceFactory.php | 10 +++++ 2 files changed, 49 insertions(+) create mode 100644 packages/Webkul/Core/src/Database/Factories/ChannelFactory.php create mode 100644 packages/Webkul/Core/src/Database/Factories/ChannelInventorySourceFactory.php diff --git a/packages/Webkul/Core/src/Database/Factories/ChannelFactory.php b/packages/Webkul/Core/src/Database/Factories/ChannelFactory.php new file mode 100644 index 000000000..80f04d976 --- /dev/null +++ b/packages/Webkul/Core/src/Database/Factories/ChannelFactory.php @@ -0,0 +1,39 @@ +define(Channel::class, function (Faker $faker, array $attributes) { + + $seoTitle = $attributes['seo_title'] ?? $faker->word; + $seoDescription = $attributes['seo_description'] ?? $faker->words(10, true); + $seoKeywords = $attributes['seo_keywords'] ?? $faker->words(3, true); + + $seoData = [ + 'meta_title' => $seoTitle, + 'meta_description' => $seoDescription, + 'meta_keywords' => $seoKeywords, + ]; + + unset($attributes['seo_title'], $attributes['seo_description'], $attributes['seo_keywords']); + + + return [ + 'code' => $faker->unique()->word, + 'name' => $faker->word, + 'default_locale_id' => function () { + return factory(Locale::class)->create()->id; + }, + 'base_currency_id' => function () { + return factory(Currency::class)->create()->id; + }, + 'root_category_id' => function () { + return factory(Category::class)->create()->id; + }, + 'home_seo' => json_encode($seoData), + ]; +}); diff --git a/packages/Webkul/Core/src/Database/Factories/ChannelInventorySourceFactory.php b/packages/Webkul/Core/src/Database/Factories/ChannelInventorySourceFactory.php new file mode 100644 index 000000000..4b8c272c5 --- /dev/null +++ b/packages/Webkul/Core/src/Database/Factories/ChannelInventorySourceFactory.php @@ -0,0 +1,10 @@ +define('channel_inventory_sources', function () { + return [ + 'channel_id' => core()->getCurrentChannel()->id, + 'inventory_source_id' => 1, + ]; +}); \ No newline at end of file From 5a9dfd85614af9d67b6cb6593b2f66e1e57cda02 Mon Sep 17 00:00:00 2001 From: Florian Bosdorff Date: Thu, 16 Apr 2020 21:13:19 +0200 Subject: [PATCH 04/54] add unified address-table and handling for models --- packages/Webkul/Checkout/src/Models/Cart.php | 4 +- .../Checkout/src/Models/CartAddress.php | 44 ++-- .../2020_04_16_185147_add_table_addresses.php | 215 ++++++++++++++++++ packages/Webkul/Core/src/Models/Address.php | 52 +++++ .../Customer/src/Models/CustomerAddress.php | 33 +-- packages/Webkul/Sales/src/Models/Order.php | 4 +- .../Webkul/Sales/src/Models/OrderAddress.php | 48 ++-- 7 files changed, 322 insertions(+), 78 deletions(-) create mode 100644 packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php create mode 100644 packages/Webkul/Core/src/Models/Address.php diff --git a/packages/Webkul/Checkout/src/Models/Cart.php b/packages/Webkul/Checkout/src/Models/Cart.php index 05ff14562..dbc924c63 100755 --- a/packages/Webkul/Checkout/src/Models/Cart.php +++ b/packages/Webkul/Checkout/src/Models/Cart.php @@ -48,7 +48,7 @@ class Cart extends Model implements CartContract */ public function billing_address() { - return $this->addresses()->where('address_type', 'billing'); + return $this->addresses()->where('address_type', CartAddress::ADDRESS_TYPE_BILLING); } /** @@ -64,7 +64,7 @@ class Cart extends Model implements CartContract */ public function shipping_address() { - return $this->addresses()->where('address_type', 'shipping'); + return $this->addresses()->where('address_type', CartAddress::ADDRESS_TYPE_SHIPPING); } /** diff --git a/packages/Webkul/Checkout/src/Models/CartAddress.php b/packages/Webkul/Checkout/src/Models/CartAddress.php index 307526c9a..bcc4cb7f1 100755 --- a/packages/Webkul/Checkout/src/Models/CartAddress.php +++ b/packages/Webkul/Checkout/src/Models/CartAddress.php @@ -2,29 +2,29 @@ namespace Webkul\Checkout\Models; -use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Builder; use Webkul\Checkout\Contracts\CartAddress as CartAddressContract; +use Webkul\Core\Models\Address; -class CartAddress extends Model implements CartAddressContract +class CartAddress extends Address implements CartAddressContract { - protected $table = 'cart_address'; + public const ADDRESS_TYPE_SHIPPING = 'cart_address_shipping'; + public const ADDRESS_TYPE_BILLING = 'cart_address_billing'; - protected $fillable = [ - 'first_name', - 'last_name', - 'email', - 'company_name', - 'vat_id', - 'address1', - 'city', - 'state', - 'postcode', - 'country', - 'phone', - 'address_type', - 'cart_id', - 'customer_id', - ]; + /** + * The "booted" method of the model. + * + * @return void + */ + protected static function booted() + { + static::addGlobalScope('address_type', function (Builder $builder) { + $builder->whereIn('address_type', [ + self::ADDRESS_TYPE_BILLING, + self::ADDRESS_TYPE_SHIPPING + ]); + }); + } /** * Get the shipping rates for the cart address. @@ -35,10 +35,10 @@ class CartAddress extends Model implements CartAddressContract } /** - * Get all of the attributes for the attribute groups. + * Get the cart record associated with the address. */ - public function getNameAttribute() + public function cart() { - return $this->first_name . ' ' . $this->last_name; + return $this->belongsTo(Cart::class); } } \ No newline at end of file diff --git a/packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php b/packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php new file mode 100644 index 000000000..feb7cc934 --- /dev/null +++ b/packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php @@ -0,0 +1,215 @@ +increments('id'); + $table->string('address_type'); + $table->unsignedInteger('customer_id')->nullable()->comment('null if guest checkout'); + $table->unsignedInteger('cart_id')->nullable()->comment('only for cart_addresses'); + $table->unsignedInteger('order_id')->nullable()->comment('only for order_addresses'); + + $table->string('first_name'); + $table->string('last_name'); + $table->string('gender')->nullable(); + $table->string('company_name')->nullable(); + $table->string('address1'); + $table->string('address2')->nullable(); + $table->string('postcode'); + $table->string('city'); + $table->string('state'); + $table->string('country'); + $table->string('email')->nullable(); + $table->string('phone')->nullable(); + + $table->string('vat_id')->nullable(); + $table->boolean('default_address') + ->default(false) + ->comment('only for customer_addresses'); + + $table->timestamps(); + + $table->foreign(['customer_id'])->references('id')->on('customers'); + $table->foreign(['cart_id'])->references('id')->on('cart'); + $table->foreign(['order_id'])->references('id')->on('orders'); + }); + + $this->insertCustomerAddresses(); + $this->insertCartAddresses(); + $this->insertOrderAddresses(); + + Schema::drop('customer_addresses'); + Schema::drop('cart_address'); + Schema::drop('order_address'); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + throw new Exception('you cannot revert this migration: data would be lost'); + } + + private function insertCustomerAddresses(): void + { + $dbPrefix = DB::getTablePrefix(); + + $insertCustomerAddresses = <<< SQL + INSERT INTO ${dbPrefix}addresses( + address_type, + customer_id, + first_name, + last_name, + gender, + company_name, + address1, + address2, + postcode, + city, + state, + country, + email, + phone, + default_address, + created_at, + updated_at + ) + SELECT + "customer", + customer_id, + first_name, + last_name, + (SELECT gender FROM customers c WHERE c.id=ca.customer_id), + company_name, + address1, + address2, + postcode, + city, + state, + country, + null, + phone, + default_address, + created_at, + updated_at + FROM customer_addresses ca; +SQL; + + DB::unprepared($insertCustomerAddresses); + } + + private function insertCartAddresses(): void + { + $dbPrefix = DB::getTablePrefix(); + + $insertCustomerAddresses = <<< SQL + INSERT INTO ${dbPrefix}addresses( + address_type, + customer_id, + cart_id, + first_name, + last_name, + gender, + company_name, + address1, + address2, + postcode, + city, + state, + country, + email, + phone, + default_address, + created_at, + updated_at + ) + SELECT + (CASE ca.address_type='billing' THEN "cart_address_billing" ELSE "cart_address_shipping" END), + customer_id, + cart_id, + first_name, + last_name, + (SELECT gender FROM customers c WHERE c.id=ca.customer_id), + company_name, + address1, + address2, + postcode, + city, + state, + country, + email, + phone, + default_address, + created_at, + updated_at + FROM cart_address ca; +SQL; + + DB::unprepared($insertCustomerAddresses); + } + + private function insertOrderAddresses(): void + { + $dbPrefix = DB::getTablePrefix(); + + $insertCustomerAddresses = <<< SQL + INSERT INTO ${dbPrefix}addresses( + address_type, + customer_id, + order_id, + first_name, + last_name, + gender, + company_name, + address1, + address2, + postcode, + city, + state, + country, + email, + phone, + default_address, + created_at, + updated_at + ) + SELECT + (CASE ca.address_type='billing' THEN "order_address_billing" ELSE "order_address_shipping" END), + customer_id, + order_id, + first_name, + last_name, + (SELECT gender FROM customers c WHERE c.id=os.customer_id), + company_name, + address1, + address2, + postcode, + city, + state, + country, + email, + phone, + default_address, + created_at, + updated_at + FROM order_address oa; +SQL; + + DB::unprepared($insertCustomerAddresses); + } +} diff --git a/packages/Webkul/Core/src/Models/Address.php b/packages/Webkul/Core/src/Models/Address.php new file mode 100644 index 000000000..b882d2eec --- /dev/null +++ b/packages/Webkul/Core/src/Models/Address.php @@ -0,0 +1,52 @@ +first_name . ' ' . $this->last_name; + } + + /** + * Get the customer record associated with the address. + */ + public function customer() + { + return $this->belongsTo(Customer::class); + } +} diff --git a/packages/Webkul/Customer/src/Models/CustomerAddress.php b/packages/Webkul/Customer/src/Models/CustomerAddress.php index 389857a53..28cb96b35 100755 --- a/packages/Webkul/Customer/src/Models/CustomerAddress.php +++ b/packages/Webkul/Customer/src/Models/CustomerAddress.php @@ -2,34 +2,23 @@ namespace Webkul\Customer\Models; -use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Builder; +use Webkul\Core\Models\Address; use Webkul\Customer\Contracts\CustomerAddress as CustomerAddressContract; -class CustomerAddress extends Model implements CustomerAddressContract +class CustomerAddress extends Address implements CustomerAddressContract { - protected $table = 'customer_addresses'; - - protected $fillable = [ - 'customer_id', - 'company_name', - 'vat_id', - 'address1', - 'address2', - 'country', - 'state', - 'city', - 'postcode', - 'phone', - 'default_address', - 'first_name', - 'last_name', - ]; + public const ADDRESS_TYPE = 'customer'; /** - * Get the customer address full name. + * The "booted" method of the model. + * + * @return void */ - public function getNameAttribute() + protected static function booted() { - return $this->first_name . ' ' . $this->last_name; + static::addGlobalScope('address_type', function (Builder $builder) { + $builder->where('address_type', self::ADDRESS_TYPE); + }); } } diff --git a/packages/Webkul/Sales/src/Models/Order.php b/packages/Webkul/Sales/src/Models/Order.php index 48d5725de..dfb9ab4d1 100755 --- a/packages/Webkul/Sales/src/Models/Order.php +++ b/packages/Webkul/Sales/src/Models/Order.php @@ -140,7 +140,7 @@ class Order extends Model implements OrderContract */ public function billing_address() { - return $this->addresses()->where('address_type', 'billing'); + return $this->addresses()->where('address_type', OrderAddress::ADDRESS_TYPE_BILLING); } /** @@ -156,7 +156,7 @@ class Order extends Model implements OrderContract */ public function shipping_address() { - return $this->addresses()->where('address_type', 'shipping'); + return $this->addresses()->where('address_type', OrderAddress::ADDRESS_TYPE_SHIPPING); } /** diff --git a/packages/Webkul/Sales/src/Models/OrderAddress.php b/packages/Webkul/Sales/src/Models/OrderAddress.php index d1bcddc62..a964e8ead 100755 --- a/packages/Webkul/Sales/src/Models/OrderAddress.php +++ b/packages/Webkul/Sales/src/Models/OrderAddress.php @@ -2,47 +2,35 @@ namespace Webkul\Sales\Models; -use Illuminate\Database\Eloquent\Model; -use Webkul\Customer\Models\Customer; +use Webkul\Core\Models\Address; use Webkul\Sales\Contracts\OrderAddress as OrderAddressContract; +use Illuminate\Database\Eloquent\Builder; -class OrderAddress extends Model implements OrderAddressContract +class OrderAddress extends Address implements OrderAddressContract { - protected $table = 'order_address'; - - protected $guarded = ['id', 'created_at', 'updated_at']; - - protected $fillable = [ - 'first_name', - 'last_name', - 'email', - 'company_name', - 'vat_id', - 'address1', - 'address2', - 'city', - 'state', - 'postcode', - 'country', - 'phone', - 'address_type', - 'cart_id', - 'customer_id', - ]; + public const ADDRESS_TYPE_SHIPPING = 'order_address_shipping'; + public const ADDRESS_TYPE_BILLING = 'order_address_billing'; /** - * Get of the customer fullname. + * The "booted" method of the model. + * + * @return void */ - public function getNameAttribute() + protected static function booted() { - return $this->first_name . ' ' . $this->last_name; + static::addGlobalScope('address_type', function (Builder $builder) { + $builder->whereIn('address_type', [ + self::ADDRESS_TYPE_BILLING, + self::ADDRESS_TYPE_SHIPPING + ]); + }); } /** - * Get the customer record associated with the order. + * Get the order record associated with the address. */ - public function customer() + public function order() { - return $this->belongsTo(Customer::class); + return $this->belongsTo(Order::class); } } \ No newline at end of file From 01d4cb026f13fbc7e77899623fb9568ec6324a04 Mon Sep 17 00:00:00 2001 From: Florian Bosdorff Date: Fri, 17 Apr 2020 11:09:59 +0200 Subject: [PATCH 05/54] fix address_type at many places; add tests for address relations --- .../Admin/src/DataGrids/AddressDataGrid.php | 4 +- .../Admin/src/DataGrids/OrderDataGrid.php | 9 +- .../src/DataGrids/OrderRefundDataGrid.php | 5 +- .../src/DataGrids/OrderShipmentsDataGrid.php | 5 +- packages/Webkul/Checkout/src/Cart.php | 12 +-- .../Database/Factories/CartAddressFactory.php | 6 +- .../Checkout/src/Models/CartAddress.php | 9 +- .../Checkout/src/Models/CartShippingRate.php | 3 +- .../2020_04_16_185147_add_table_addresses.php | 86 +++++++++++++++--- packages/Webkul/Core/src/Models/Address.php | 13 ++- .../Factories/CustomerAddressFactory.php | 5 +- .../Customer/src/Models/CustomerAddress.php | 9 +- .../Factories/OrderAddressFactory.php | 4 +- packages/Webkul/Sales/src/Models/Invoice.php | 6 +- packages/Webkul/Sales/src/Models/Order.php | 48 ++++++---- .../Webkul/Sales/src/Models/OrderAddress.php | 9 +- packages/Webkul/Sales/src/Models/Shipment.php | 3 +- .../Webkul/Shipping/src/Carriers/FlatRate.php | 2 +- .../Webkul/Shipping/src/Carriers/Free.php | 2 +- tests/functional/Checkout/Order/OrderCest.php | 8 +- tests/unit/Core/AddressCest.php | 89 +++++++++++++++++++ 21 files changed, 267 insertions(+), 70 deletions(-) create mode 100644 tests/unit/Core/AddressCest.php diff --git a/packages/Webkul/Admin/src/DataGrids/AddressDataGrid.php b/packages/Webkul/Admin/src/DataGrids/AddressDataGrid.php index 59997b828..ede09c495 100644 --- a/packages/Webkul/Admin/src/DataGrids/AddressDataGrid.php +++ b/packages/Webkul/Admin/src/DataGrids/AddressDataGrid.php @@ -3,6 +3,7 @@ namespace Webkul\Admin\DataGrids; use Illuminate\Support\Facades\DB; +use Webkul\Customer\Models\CustomerAddress; use Webkul\Ui\DataGrid\DataGrid; use Webkul\Customer\Repositories\CustomerRepository; @@ -43,10 +44,11 @@ class AddressDataGrid extends DataGrid { $customer = $this->customerRepository->find(request('id')); - $queryBuilder = DB::table('customer_addresses as ca') + $queryBuilder = DB::table('addresses as ca') ->leftJoin('countries', 'ca.country', '=', 'countries.code') ->leftJoin('customers as c', 'ca.customer_id', '=', 'c.id') ->addSelect('ca.id as address_id', 'ca.company_name', 'ca.address1', 'ca.country', DB::raw('' . DB::getTablePrefix() . 'countries.name as country_name'), 'ca.state', 'ca.city', 'ca.postcode', 'ca.phone', 'ca.default_address') + ->where('ca.address_type', CustomerAddress::ADDRESS_TYPE) ->where('c.id', $customer->id); $queryBuilder = $queryBuilder->leftJoin('country_states', function($qb) { diff --git a/packages/Webkul/Admin/src/DataGrids/OrderDataGrid.php b/packages/Webkul/Admin/src/DataGrids/OrderDataGrid.php index 718cb4071..50673e5fa 100755 --- a/packages/Webkul/Admin/src/DataGrids/OrderDataGrid.php +++ b/packages/Webkul/Admin/src/DataGrids/OrderDataGrid.php @@ -3,6 +3,7 @@ namespace Webkul\Admin\DataGrids; use Illuminate\Support\Facades\DB; +use Webkul\Sales\Models\OrderAddress; use Webkul\Ui\DataGrid\DataGrid; class OrderDataGrid extends DataGrid @@ -14,13 +15,13 @@ class OrderDataGrid extends DataGrid public function prepareQueryBuilder() { $queryBuilder = DB::table('orders') - ->leftJoin('order_address as order_address_shipping', function($leftJoin) { + ->leftJoin('addresses as order_address_shipping', function($leftJoin) { $leftJoin->on('order_address_shipping.order_id', '=', 'orders.id') - ->where('order_address_shipping.address_type', 'shipping'); + ->where('order_address_shipping.address_type', OrderAddress::ADDRESS_TYPE_SHIPPING); }) - ->leftJoin('order_address as order_address_billing', function($leftJoin) { + ->leftJoin('addresses as order_address_billing', function($leftJoin) { $leftJoin->on('order_address_billing.order_id', '=', 'orders.id') - ->where('order_address_billing.address_type', 'billing'); + ->where('order_address_billing.address_type', OrderAddress::ADDRESS_TYPE_BILLING); }) ->addSelect('orders.id','orders.increment_id', 'orders.base_sub_total', 'orders.base_grand_total', 'orders.created_at', 'channel_name', 'status') ->addSelect(DB::raw('CONCAT(' . DB::getTablePrefix() . 'order_address_billing.first_name, " ", ' . DB::getTablePrefix() . 'order_address_billing.last_name) as billed_to')) diff --git a/packages/Webkul/Admin/src/DataGrids/OrderRefundDataGrid.php b/packages/Webkul/Admin/src/DataGrids/OrderRefundDataGrid.php index 320182382..53fe381e8 100755 --- a/packages/Webkul/Admin/src/DataGrids/OrderRefundDataGrid.php +++ b/packages/Webkul/Admin/src/DataGrids/OrderRefundDataGrid.php @@ -3,6 +3,7 @@ namespace Webkul\Admin\DataGrids; use Illuminate\Support\Facades\DB; +use Webkul\Sales\Models\OrderAddress; use Webkul\Ui\DataGrid\DataGrid; class OrderRefundDataGrid extends DataGrid @@ -16,9 +17,9 @@ class OrderRefundDataGrid extends DataGrid $queryBuilder = DB::table('refunds') ->select('refunds.id', 'orders.increment_id', 'refunds.state', 'refunds.base_grand_total', 'refunds.created_at') ->leftJoin('orders', 'refunds.order_id', '=', 'orders.id') - ->leftJoin('order_address as order_address_billing', function($leftJoin) { + ->leftJoin('addresses as order_address_billing', function($leftJoin) { $leftJoin->on('order_address_billing.order_id', '=', 'orders.id') - ->where('order_address_billing.address_type', 'billing'); + ->where('order_address_billing.address_type', OrderAddress::ADDRESS_TYPE_BILLING); }) ->addSelect(DB::raw('CONCAT(' . DB::getTablePrefix() . 'order_address_billing.first_name, " ", ' . DB::getTablePrefix() . 'order_address_billing.last_name) as billed_to')); diff --git a/packages/Webkul/Admin/src/DataGrids/OrderShipmentsDataGrid.php b/packages/Webkul/Admin/src/DataGrids/OrderShipmentsDataGrid.php index d126c7f2a..33df62eb9 100755 --- a/packages/Webkul/Admin/src/DataGrids/OrderShipmentsDataGrid.php +++ b/packages/Webkul/Admin/src/DataGrids/OrderShipmentsDataGrid.php @@ -3,6 +3,7 @@ namespace Webkul\Admin\DataGrids; use Illuminate\Support\Facades\DB; +use Webkul\Sales\Models\OrderAddress; use Webkul\Ui\DataGrid\DataGrid; class OrderShipmentsDataGrid extends DataGrid @@ -14,9 +15,9 @@ class OrderShipmentsDataGrid extends DataGrid public function prepareQueryBuilder() { $queryBuilder = DB::table('shipments') - ->leftJoin('order_address as order_address_shipping', function($leftJoin) { + ->leftJoin('addresses as order_address_shipping', function($leftJoin) { $leftJoin->on('order_address_shipping.order_id', '=', 'shipments.order_id') - ->where('order_address_shipping.address_type', 'shipping'); + ->where('order_address_shipping.address_type', OrderAddress::ADDRESS_TYPE_SHIPPING); }) ->leftJoin('orders as ors', 'shipments.order_id', '=', 'ors.id') ->leftJoin('inventory_sources as is', 'shipments.inventory_source_id', '=', 'is.id') diff --git a/packages/Webkul/Checkout/src/Cart.php b/packages/Webkul/Checkout/src/Cart.php index 6032735d7..4a4ddd407 100755 --- a/packages/Webkul/Checkout/src/Cart.php +++ b/packages/Webkul/Checkout/src/Cart.php @@ -658,7 +658,7 @@ class Cart if (! $cart = $this->getCart()) { return; } - + Event::dispatch('checkout.cart.calculate.items.tax.before', $cart); foreach ($cart->items()->get() as $item) { @@ -1199,21 +1199,21 @@ class Cart } else { if (isset($billingAddressData['use_for_shipping']) && $billingAddressData['use_for_shipping']) { $this->cartAddressRepository->create(array_merge($billingAddressData, - ['address_type' => 'shipping'])); + ['address_type' => CartAddress::ADDRESS_TYPE_SHIPPING])); } else { $this->cartAddressRepository->create(array_merge($shippingAddressData, - ['address_type' => 'shipping'])); + ['address_type' => CartAddress::ADDRESS_TYPE_SHIPPING])); } } } } else { - $this->cartAddressRepository->create(array_merge($billingAddressData, ['address_type' => 'billing'])); + $this->cartAddressRepository->create(array_merge($billingAddressData, ['address_type' => CartAddress::ADDRESS_TYPE_BILLING])); if ($cart->haveStockableItems()) { if (isset($billingAddressData['use_for_shipping']) && $billingAddressData['use_for_shipping']) { - $this->cartAddressRepository->create(array_merge($billingAddressData, ['address_type' => 'shipping'])); + $this->cartAddressRepository->create(array_merge($billingAddressData, ['address_type' => CartAddress::ADDRESS_TYPE_SHIPPING])); } else { - $this->cartAddressRepository->create(array_merge($shippingAddressData, ['address_type' => 'shipping'])); + $this->cartAddressRepository->create(array_merge($shippingAddressData, ['address_type' => CartAddress::ADDRESS_TYPE_SHIPPING])); } } } diff --git a/packages/Webkul/Checkout/src/Database/Factories/CartAddressFactory.php b/packages/Webkul/Checkout/src/Database/Factories/CartAddressFactory.php index 53e389de0..c78b813e7 100644 --- a/packages/Webkul/Checkout/src/Database/Factories/CartAddressFactory.php +++ b/packages/Webkul/Checkout/src/Database/Factories/CartAddressFactory.php @@ -6,14 +6,10 @@ use Faker\Generator as Faker; use Webkul\Checkout\Models\CartAddress; $factory->define(CartAddress::class, function (Faker $faker) { - $now = date("Y-m-d H:i:s"); - return [ 'first_name' => $faker->firstName(), 'last_name' => $faker->lastName, 'email' => $faker->email, - 'created_at' => $now, - 'updated_at' => $now, - 'address_type' => 'billing', + 'address_type' => CartAddress::ADDRESS_TYPE_BILLING, ]; }); diff --git a/packages/Webkul/Checkout/src/Models/CartAddress.php b/packages/Webkul/Checkout/src/Models/CartAddress.php index bcc4cb7f1..7b59b0983 100755 --- a/packages/Webkul/Checkout/src/Models/CartAddress.php +++ b/packages/Webkul/Checkout/src/Models/CartAddress.php @@ -11,6 +11,13 @@ class CartAddress extends Address implements CartAddressContract public const ADDRESS_TYPE_SHIPPING = 'cart_address_shipping'; public const ADDRESS_TYPE_BILLING = 'cart_address_billing'; + /** + * @var array default values + */ + protected $attributes = [ + 'address_type' => self::ADDRESS_TYPE_BILLING, + ]; + /** * The "booted" method of the model. * @@ -18,7 +25,7 @@ class CartAddress extends Address implements CartAddressContract */ protected static function booted() { - static::addGlobalScope('address_type', function (Builder $builder) { + static::addGlobalScope('address_type', static function (Builder $builder) { $builder->whereIn('address_type', [ self::ADDRESS_TYPE_BILLING, self::ADDRESS_TYPE_SHIPPING diff --git a/packages/Webkul/Checkout/src/Models/CartShippingRate.php b/packages/Webkul/Checkout/src/Models/CartShippingRate.php index e3c8d41a3..a4b4bbe17 100755 --- a/packages/Webkul/Checkout/src/Models/CartShippingRate.php +++ b/packages/Webkul/Checkout/src/Models/CartShippingRate.php @@ -24,6 +24,7 @@ class CartShippingRate extends Model implements CartShippingRateContract */ public function shipping_address() { - return $this->belongsTo(CartAddressProxy::modelClass()); + return $this->belongsTo(CartAddressProxy::modelClass(), 'cart_address_id') + ->where('address_type', CartAddress::ADDRESS_TYPE_SHIPPING); } } \ No newline at end of file diff --git a/packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php b/packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php index feb7cc934..f1a62ab1f 100644 --- a/packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php +++ b/packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php @@ -4,6 +4,11 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; +use Webkul\Checkout\Models\CartAddress; +use Webkul\Checkout\Models\CartShippingRate; +use Webkul\Sales\Models\Invoice; +use Webkul\Sales\Models\OrderAddress; +use Webkul\Sales\Models\Shipment; class AddTableAddresses extends Migration { @@ -14,6 +19,8 @@ class AddTableAddresses extends Migration */ public function up() { + DB::beginTransaction(); + Schema::create('addresses', function (Blueprint $table) { $table->increments('id'); $table->string('address_type'); @@ -39,6 +46,8 @@ class AddTableAddresses extends Migration ->default(false) ->comment('only for customer_addresses'); + $table->json('additional')->nullable(); + $table->timestamps(); $table->foreign(['customer_id'])->references('id')->on('customers'); @@ -46,13 +55,17 @@ class AddTableAddresses extends Migration $table->foreign(['order_id'])->references('id')->on('orders'); }); - $this->insertCustomerAddresses(); - $this->insertCartAddresses(); - $this->insertOrderAddresses(); + $this->migrateCustomerAddresses(); + $this->migrateCartAddresses(); + $this->migrateOrderAddresses(); + + $this->migrateForeignKeys(); Schema::drop('customer_addresses'); Schema::drop('cart_address'); Schema::drop('order_address'); + + DB::commit(); } /** @@ -65,7 +78,7 @@ class AddTableAddresses extends Migration throw new Exception('you cannot revert this migration: data would be lost'); } - private function insertCustomerAddresses(): void + private function migrateCustomerAddresses(): void { $dbPrefix = DB::getTablePrefix(); @@ -86,6 +99,7 @@ class AddTableAddresses extends Migration email, phone, default_address, + additional, created_at, updated_at ) @@ -105,6 +119,7 @@ class AddTableAddresses extends Migration null, phone, default_address, + JSON_INSERT('{}', '$.old_customer_address_id', id), created_at, updated_at FROM customer_addresses ca; @@ -113,7 +128,7 @@ SQL; DB::unprepared($insertCustomerAddresses); } - private function insertCartAddresses(): void + private function migrateCartAddresses(): void { $dbPrefix = DB::getTablePrefix(); @@ -134,12 +149,12 @@ SQL; country, email, phone, - default_address, + additional, created_at, updated_at ) SELECT - (CASE ca.address_type='billing' THEN "cart_address_billing" ELSE "cart_address_shipping" END), + (CASE WHEN ca.address_type='billing' THEN "cart_address_billing" ELSE "cart_address_shipping" END), customer_id, cart_id, first_name, @@ -154,7 +169,7 @@ SQL; country, email, phone, - default_address, + JSON_INSERT('{}', '$.old_cart_address_id', id), created_at, updated_at FROM cart_address ca; @@ -163,7 +178,7 @@ SQL; DB::unprepared($insertCustomerAddresses); } - private function insertOrderAddresses(): void + private function migrateOrderAddresses(): void { $dbPrefix = DB::getTablePrefix(); @@ -184,17 +199,17 @@ SQL; country, email, phone, - default_address, + additional, created_at, updated_at ) SELECT - (CASE ca.address_type='billing' THEN "order_address_billing" ELSE "order_address_shipping" END), + (CASE WHEN oa.address_type='billing' THEN "order_address_billing" ELSE "order_address_shipping" END), customer_id, order_id, first_name, last_name, - (SELECT gender FROM customers c WHERE c.id=os.customer_id), + (SELECT gender FROM customers c WHERE c.id=oa.customer_id), company_name, address1, address2, @@ -204,7 +219,7 @@ SQL; country, email, phone, - default_address, + JSON_INSERT('{}', '$.old_Order_address_id', id), created_at, updated_at FROM order_address oa; @@ -212,4 +227,49 @@ SQL; DB::unprepared($insertCustomerAddresses); } + + private function migrateForeignKeys(): void + { + Schema::table('cart_shipping_rates', static function (Blueprint $table) { + $table->dropForeign(['cart_address_id']); + + CartAddress::query() + ->orderBy('id', 'asc') // for some reason each() needs an orderBy in before + ->each(static function ($row) { + CartShippingRate::query() + ->where('cart_address_id', $row->additional['old_cart_address_id']) + ->update(['cart_address_id' => $row->id]); + }); + + $table->foreign(['cart_address_id'])->references('id')->on('addresses'); + }); + + Schema::table('invoices', static function (Blueprint $table) { + $table->dropForeign(['order_address_id']); + + OrderAddress::query() + ->orderBy('id', 'asc') // for some reason each() needs an orderBy in before + ->each(static function ($row) { + Invoice::query() + ->where('order_address_id', $row->additional['old_order_address_id']) + ->update(['order_address_id' => $row->id]); + }); + + $table->foreign(['order_address_id'])->references('id')->on('addresses'); + }); + + Schema::table('shipments', static function (Blueprint $table) { + $table->dropForeign(['order_address_id']); + + OrderAddress::query() + ->orderBy('id', 'asc') // for some reason each() needs an orderBy in before + ->each(static function ($row) { + Shipment::query() + ->where('order_address_id', $row->additional['old_order_address_id']) + ->update(['order_address_id' => $row->id]); + }); + + $table->foreign(['order_address_id'])->references('id')->on('addresses'); + }); + } } diff --git a/packages/Webkul/Core/src/Models/Address.php b/packages/Webkul/Core/src/Models/Address.php index b882d2eec..05bf0bd1e 100644 --- a/packages/Webkul/Core/src/Models/Address.php +++ b/packages/Webkul/Core/src/Models/Address.php @@ -5,10 +5,13 @@ namespace Webkul\Core\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Webkul\Customer\Models\Customer; abstract class Address extends Model { + protected $table = 'addresses'; + protected $guarded = [ 'id', 'created_at', @@ -16,6 +19,7 @@ abstract class Address extends Model ]; protected $fillable = [ + 'address_type', 'customer_id', 'cart_id', 'order_id', @@ -32,12 +36,17 @@ abstract class Address extends Model 'email', 'phone', 'default_address', + 'additional', + ]; + + protected $casts = [ + 'additional' => 'array', ]; /** * Get all of the attributes for the attribute groups. */ - public function getNameAttribute() + public function getNameAttribute(): string { return $this->first_name . ' ' . $this->last_name; } @@ -45,7 +54,7 @@ abstract class Address extends Model /** * Get the customer record associated with the address. */ - public function customer() + public function customer(): BelongsTo { return $this->belongsTo(Customer::class); } diff --git a/packages/Webkul/Customer/src/Database/Factories/CustomerAddressFactory.php b/packages/Webkul/Customer/src/Database/Factories/CustomerAddressFactory.php index e60f5fe88..877df08e2 100644 --- a/packages/Webkul/Customer/src/Database/Factories/CustomerAddressFactory.php +++ b/packages/Webkul/Customer/src/Database/Factories/CustomerAddressFactory.php @@ -7,8 +7,6 @@ use Webkul\Customer\Models\Customer; use Webkul\Customer\Models\CustomerAddress; $factory->define(CustomerAddress::class, function (Faker $faker) { - $now = date("Y-m-d H:i:s"); - // use an locale from a country in europe so the vat id can be generated $fakerIt = \Faker\Factory::create('it_IT'); @@ -27,8 +25,7 @@ $factory->define(CustomerAddress::class, function (Faker $faker) { 'postcode' => $faker->postcode, 'phone' => $faker->e164PhoneNumber, 'default_address' => array_random([0, 1]), - 'created_at' => $now, - 'updated_at' => $now, + 'address_type' => CustomerAddress::ADDRESS_TYPE, ]; }); diff --git a/packages/Webkul/Customer/src/Models/CustomerAddress.php b/packages/Webkul/Customer/src/Models/CustomerAddress.php index 28cb96b35..2ce4770f1 100755 --- a/packages/Webkul/Customer/src/Models/CustomerAddress.php +++ b/packages/Webkul/Customer/src/Models/CustomerAddress.php @@ -10,6 +10,13 @@ class CustomerAddress extends Address implements CustomerAddressContract { public const ADDRESS_TYPE = 'customer'; + /** + * @var array default values + */ + protected $attributes = [ + 'address_type' => self::ADDRESS_TYPE, + ]; + /** * The "booted" method of the model. * @@ -17,7 +24,7 @@ class CustomerAddress extends Address implements CustomerAddressContract */ protected static function booted() { - static::addGlobalScope('address_type', function (Builder $builder) { + static::addGlobalScope('address_type', static function (Builder $builder) { $builder->where('address_type', self::ADDRESS_TYPE); }); } diff --git a/packages/Webkul/Sales/src/Database/Factories/OrderAddressFactory.php b/packages/Webkul/Sales/src/Database/Factories/OrderAddressFactory.php index 12f1004f6..cf5a1ffcd 100644 --- a/packages/Webkul/Sales/src/Database/Factories/OrderAddressFactory.php +++ b/packages/Webkul/Sales/src/Database/Factories/OrderAddressFactory.php @@ -22,7 +22,7 @@ $factory->define(OrderAddress::class, function (Faker $faker) { 'city' => $customerAddress->city, 'postcode' => $customerAddress->postcode, 'phone' => $customerAddress->phone, - 'address_type' => 'billing', + 'address_type' => OrderAddress::ADDRESS_TYPE_BILLING, 'order_id' => function () { return factory(Order::class)->create()->id; }, @@ -30,5 +30,5 @@ $factory->define(OrderAddress::class, function (Faker $faker) { }); $factory->state(OrderAddress::class, 'shipping', [ - 'address_type' => 'shipping', + 'address_type' => OrderAddress::ADDRESS_TYPE_SHIPPING, ]); \ No newline at end of file diff --git a/packages/Webkul/Sales/src/Models/Invoice.php b/packages/Webkul/Sales/src/Models/Invoice.php index 127ddfaf4..a6e20c654 100755 --- a/packages/Webkul/Sales/src/Models/Invoice.php +++ b/packages/Webkul/Sales/src/Models/Invoice.php @@ -34,7 +34,8 @@ class Invoice extends Model implements InvoiceContract /** * Get the invoice items record associated with the invoice. */ - public function items() { + public function items() + { return $this->hasMany(InvoiceItemProxy::modelClass())->whereNull('parent_id'); } @@ -59,6 +60,7 @@ class Invoice extends Model implements InvoiceContract */ public function address() { - return $this->belongsTo(OrderAddressProxy::modelClass(), 'order_address_id'); + return $this->belongsTo(OrderAddressProxy::modelClass(), 'order_address_id') + ->where('address_type', OrderAddress::ADDRESS_TYPE_BILLING); } } \ No newline at end of file diff --git a/packages/Webkul/Sales/src/Models/Order.php b/packages/Webkul/Sales/src/Models/Order.php index dfb9ab4d1..bd6eaf01b 100755 --- a/packages/Webkul/Sales/src/Models/Order.php +++ b/packages/Webkul/Sales/src/Models/Order.php @@ -8,6 +8,14 @@ use Webkul\Sales\Contracts\Order as OrderContract; class Order extends Model implements OrderContract { + public const STATUS_PENDING = 'pending'; + public const STATUS_PENDING_PAYMENT = 'pending_payment'; + public const STATUS_PROCESSING = 'processing'; + public const STATUS_COMPLETED = 'completed'; + public const STATUS_CANCELED = 'canceled'; + public const STATUS_CLOSED = 'closed'; + public const STATUS_FRAUD = 'fraud'; + protected $guarded = [ 'id', 'items', @@ -21,13 +29,13 @@ class Order extends Model implements OrderContract ]; protected $statusLabel = [ - 'pending' => 'Pending', - 'pending_payment' => 'Pending Payment', - 'processing' => 'Processing', - 'completed' => 'Completed', - 'canceled' => 'Canceled', - 'closed' => 'Closed', - 'fraud' => 'Fraud', + self::STATUS_PENDING => 'Pending', + self::STATUS_PENDING_PAYMENT => 'Pending Payment', + self::STATUS_PROCESSING => 'Processing', + self::STATUS_COMPLETED => 'Completed', + self::STATUS_CANCELED => 'Canceled', + self::STATUS_CLOSED => 'Closed', + self::STATUS_FRAUD => 'Fraud', ]; /** @@ -180,7 +188,7 @@ class Order extends Model implements OrderContract * * @return boolean */ - public function haveStockableItems() + public function haveStockableItems(): bool { foreach ($this->items as $item) { if ($item->getTypeInstance()->isStockable()) { @@ -193,10 +201,12 @@ class Order extends Model implements OrderContract /** * Checks if new shipment is allow or not + * + * @return bool */ - public function canShip() + public function canShip(): bool { - if ($this->status == 'fraud') { + if ($this->status === self::STATUS_FRAUD) { return false; } @@ -211,10 +221,12 @@ class Order extends Model implements OrderContract /** * Checks if new invoice is allow or not + * + * @return bool */ - public function canInvoice() + public function canInvoice(): bool { - if ($this->status == 'fraud') { + if ($this->status === self::STATUS_FRAUD) { return false; } @@ -229,10 +241,12 @@ class Order extends Model implements OrderContract /** * Checks if order can be canceled or not + * + * @return bool */ - public function canCancel() + public function canCancel(): bool { - if ($this->status == 'fraud') { + if ($this->status === self::STATUS_FRAUD) { return false; } @@ -247,10 +261,12 @@ class Order extends Model implements OrderContract /** * Checks if order can be refunded or not + * + * @return bool */ - public function canRefund() + public function canRefund(): bool { - if ($this->status == 'fraud') { + if ($this->status === self::STATUS_FRAUD) { return false; } diff --git a/packages/Webkul/Sales/src/Models/OrderAddress.php b/packages/Webkul/Sales/src/Models/OrderAddress.php index a964e8ead..2bb571bb1 100755 --- a/packages/Webkul/Sales/src/Models/OrderAddress.php +++ b/packages/Webkul/Sales/src/Models/OrderAddress.php @@ -11,6 +11,13 @@ class OrderAddress extends Address implements OrderAddressContract public const ADDRESS_TYPE_SHIPPING = 'order_address_shipping'; public const ADDRESS_TYPE_BILLING = 'order_address_billing'; + /** + * @var array default values + */ + protected $attributes = [ + 'address_type' => self::ADDRESS_TYPE_BILLING, + ]; + /** * The "booted" method of the model. * @@ -18,7 +25,7 @@ class OrderAddress extends Address implements OrderAddressContract */ protected static function booted() { - static::addGlobalScope('address_type', function (Builder $builder) { + static::addGlobalScope('address_type', static function (Builder $builder) { $builder->whereIn('address_type', [ self::ADDRESS_TYPE_BILLING, self::ADDRESS_TYPE_SHIPPING diff --git a/packages/Webkul/Sales/src/Models/Shipment.php b/packages/Webkul/Sales/src/Models/Shipment.php index 1627100a5..a5983f091 100755 --- a/packages/Webkul/Sales/src/Models/Shipment.php +++ b/packages/Webkul/Sales/src/Models/Shipment.php @@ -51,6 +51,7 @@ class Shipment extends Model implements ShipmentContract */ public function address() { - return $this->belongsTo(OrderAddressProxy::modelClass(), 'order_address_id'); + return $this->belongsTo(OrderAddressProxy::modelClass(), 'order_address_id') + ->where('address_type', OrderAddress::ADDRESS_TYPE_SHIPPING); } } \ No newline at end of file diff --git a/packages/Webkul/Shipping/src/Carriers/FlatRate.php b/packages/Webkul/Shipping/src/Carriers/FlatRate.php index 5cbf59083..6170bce6b 100755 --- a/packages/Webkul/Shipping/src/Carriers/FlatRate.php +++ b/packages/Webkul/Shipping/src/Carriers/FlatRate.php @@ -23,7 +23,7 @@ class FlatRate extends AbstractShipping /** * Returns rate for flatrate * - * @return array + * @return CartShippingRate|false */ public function calculate() { diff --git a/packages/Webkul/Shipping/src/Carriers/Free.php b/packages/Webkul/Shipping/src/Carriers/Free.php index f03247c94..c3c0e0ec5 100755 --- a/packages/Webkul/Shipping/src/Carriers/Free.php +++ b/packages/Webkul/Shipping/src/Carriers/Free.php @@ -22,7 +22,7 @@ class Free extends AbstractShipping /** * Returns rate for flatrate * - * @return array + * @return CartShippingRate|false */ public function calculate() { diff --git a/tests/functional/Checkout/Order/OrderCest.php b/tests/functional/Checkout/Order/OrderCest.php index 2cf903895..8d7e47f13 100644 --- a/tests/functional/Checkout/Order/OrderCest.php +++ b/tests/functional/Checkout/Order/OrderCest.php @@ -67,13 +67,13 @@ class OrderCest $I->seeResponseCodeIsSuccessful(); $I->seeRecord(CartAddress::class, array_merge($addressData, [ - 'address_type' => 'shipping', + 'address_type' => CartAddress::ADDRESS_TYPE_SHIPPING, 'cart_id' => $mocks['cart']->id, 'customer_id' => $mocks['customer']->id, ])); $I->seeRecord(CartAddress::class, array_merge($addressData, [ - 'address_type' => 'billing', + 'address_type' => CartAddress::ADDRESS_TYPE_BILLING, 'cart_id' => $mocks['cart']->id, 'customer_id' => $mocks['customer']->id, ])); @@ -128,13 +128,13 @@ class OrderCest $I->seeRecord(OrderAddress::class, array_merge($addressData, [ 'order_id' => $order->id, - 'address_type' => 'shipping', + 'address_type' => OrderAddress::ADDRESS_TYPE_SHIPPING, 'customer_id' => $mocks['customer']->id, ])); $I->seeRecord(OrderAddress::class, array_merge($addressData, [ 'order_id' => $order->id, - 'address_type' => 'billing', + 'address_type' => OrderAddress::ADDRESS_TYPE_BILLING, 'customer_id' => $mocks['customer']->id, ])); diff --git a/tests/unit/Core/AddressCest.php b/tests/unit/Core/AddressCest.php new file mode 100644 index 000000000..b307d38e1 --- /dev/null +++ b/tests/unit/Core/AddressCest.php @@ -0,0 +1,89 @@ +have(Customer::class); + CustomerAddress::create(['customer_id' => $customer1->id]); + + /** @var Customer $customer2 */ + $customer2 = $I->have(Customer::class); + $address1 = CustomerAddress::create(['customer_id' => $customer2->id]); + + $customer2->refresh(); + $I->assertCount(1, $customer2->addresses); + $I->assertEquals($address1->id, $customer2->addresses[0]->id); + $I->assertNull($customer2->default_address); + + $address2 = CustomerAddress::create(['customer_id' => $customer2->id, 'default_address' => true]); + $customer2->refresh(); + $I->assertCount(2, $customer2->addresses); + $I->assertEquals($address2->id, $customer2->default_address->id); + } + + public function testCartAddressRelations(UnitTester $I): void + { + /** @var Cart $cart */ + $cart = $I->have(Cart::class); + $address1 = CartAddress::create(['cart_id' => $cart->id]); + $address2 = CartAddress::create([ + 'cart_id' => $cart->id, + 'address_type' => CartAddress::ADDRESS_TYPE_SHIPPING + ]); + $cart->refresh(); + + $I->assertNotNull($address1->address_type); + $I->assertEquals($address1->id, $cart->billing_address->id); + $I->assertEquals($address2->id, $cart->shipping_address->id); + + /** @var CartShippingRate $freeShipping */ + $freeShipping = (new Free())->calculate(); + $freeShipping->cart_address_id = $address2->id; + $freeShipping->saveOrFail(); + + $freeShipping->refresh(); + $I->assertEquals($address2->id, $freeShipping->shipping_address->id); + } + + public function testOrderAddressRelations(UnitTester $I): void + { + /** @var Order $order */ + $order = $I->have(Order::class); + $address1 = OrderAddress::create(['order_id' => $order->id]); + $address2 = OrderAddress::create([ + 'order_id' => $order->id, + 'address_type' => OrderAddress::ADDRESS_TYPE_SHIPPING + ]); + $order->refresh(); + + $I->assertNotNull($address1->address_type); + $I->assertEquals($address1->id, $order->billing_address->id); + $I->assertEquals($address2->id, $order->shipping_address->id); + + /** @var Shipment $shipment */ + $shipment = Shipment::create(['order_address_id' => $address2->id]); + $I->assertEquals($address2->id, $shipment->address->id); + + /** @var Invoice $invoice */ + $invoice = Invoice::create(['order_id' => $order->id, 'order_address_id' => $address1->id]); + $I->assertEquals($address1->id, $invoice->address->id); + } +} \ No newline at end of file From 910f76952ff38e88c6d68829a97132778945b332 Mon Sep 17 00:00:00 2001 From: Florian Bosdorff Date: Fri, 17 Apr 2020 15:54:02 +0200 Subject: [PATCH 06/54] fix address-constrains; fix address global scopes --- .../Checkout/src/Models/CartAddress.php | 12 +++++++- .../2020_04_16_185147_add_table_addresses.php | 21 +++++++++---- packages/Webkul/Core/src/Models/Address.php | 30 +++++++++++++++++++ .../Customer/src/Models/CustomerAddress.php | 4 ++- .../Factories/OrderAddressFactory.php | 2 +- packages/Webkul/Sales/src/Models/Invoice.php | 2 +- .../Webkul/Sales/src/Models/OrderAddress.php | 26 ++++++++++++++-- packages/Webkul/Sales/src/Models/Shipment.php | 2 +- tests/unit/Core/AddressCest.php | 19 ++++++++++++ 9 files changed, 105 insertions(+), 13 deletions(-) diff --git a/packages/Webkul/Checkout/src/Models/CartAddress.php b/packages/Webkul/Checkout/src/Models/CartAddress.php index 7b59b0983..8e81039e7 100755 --- a/packages/Webkul/Checkout/src/Models/CartAddress.php +++ b/packages/Webkul/Checkout/src/Models/CartAddress.php @@ -6,6 +6,14 @@ use Illuminate\Database\Eloquent\Builder; use Webkul\Checkout\Contracts\CartAddress as CartAddressContract; use Webkul\Core\Models\Address; +/** + * Class CartAddress + * @package Webkul\Checkout\Models + * + * @property integer $cart_id + * @property Cart $cart + * + */ class CartAddress extends Address implements CartAddressContract { public const ADDRESS_TYPE_SHIPPING = 'cart_address_shipping'; @@ -23,7 +31,7 @@ class CartAddress extends Address implements CartAddressContract * * @return void */ - protected static function booted() + protected static function boot() { static::addGlobalScope('address_type', static function (Builder $builder) { $builder->whereIn('address_type', [ @@ -31,6 +39,8 @@ class CartAddress extends Address implements CartAddressContract self::ADDRESS_TYPE_SHIPPING ]); }); + + parent::boot(); } /** diff --git a/packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php b/packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php index f1a62ab1f..0e98f344f 100644 --- a/packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php +++ b/packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php @@ -50,9 +50,9 @@ class AddTableAddresses extends Migration $table->timestamps(); - $table->foreign(['customer_id'])->references('id')->on('customers'); - $table->foreign(['cart_id'])->references('id')->on('cart'); - $table->foreign(['order_id'])->references('id')->on('orders'); + $table->foreign(['customer_id'])->references('id')->on('customers')->onDelete('cascade'); + $table->foreign(['cart_id'])->references('id')->on('cart')->onDelete('cascade'); + $table->foreign(['order_id'])->references('id')->on('orders')->onDelete('cascade'); }); $this->migrateCustomerAddresses(); @@ -241,7 +241,10 @@ SQL; ->update(['cart_address_id' => $row->id]); }); - $table->foreign(['cart_address_id'])->references('id')->on('addresses'); + $table->foreign(['cart_address_id']) + ->references('id') + ->on('addresses') + ->onDelete('cascade'); }); Schema::table('invoices', static function (Blueprint $table) { @@ -255,7 +258,10 @@ SQL; ->update(['order_address_id' => $row->id]); }); - $table->foreign(['order_address_id'])->references('id')->on('addresses'); + $table->foreign(['order_address_id']) + ->references('id') + ->on('addresses') + ->onDelete('cascade'); }); Schema::table('shipments', static function (Blueprint $table) { @@ -269,7 +275,10 @@ SQL; ->update(['order_address_id' => $row->id]); }); - $table->foreign(['order_address_id'])->references('id')->on('addresses'); + $table->foreign(['order_address_id']) + ->references('id') + ->on('addresses') + ->onDelete('cascade'); }); } } diff --git a/packages/Webkul/Core/src/Models/Address.php b/packages/Webkul/Core/src/Models/Address.php index 05bf0bd1e..9b0d68e97 100644 --- a/packages/Webkul/Core/src/Models/Address.php +++ b/packages/Webkul/Core/src/Models/Address.php @@ -4,10 +4,39 @@ namespace Webkul\Core\Models; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Webkul\Customer\Models\Customer; +/** + * Class Address + * @package Webkul\Core\Models + * + * @property string $address_type + * @property integer $customer_id + * @property Customer $customer + * @property string $first_name + * @property string $last_name + * @property string $gender + * @property string $company_name + * @property string $address1 + * @property string $address2 + * @property string $postcode + * @property string $city + * @property string $state + * @property string $country + * @property string $email + * @property string $phone + * @property boolean $default_address + * @property array $additional + * + * @property-read integer $id + * @property-read string $name + * @property-read Carbon $created_at + * @property-read Carbon $updated_at + * + */ abstract class Address extends Model { protected $table = 'addresses'; @@ -36,6 +65,7 @@ abstract class Address extends Model 'email', 'phone', 'default_address', + 'vat_id', 'additional', ]; diff --git a/packages/Webkul/Customer/src/Models/CustomerAddress.php b/packages/Webkul/Customer/src/Models/CustomerAddress.php index 2ce4770f1..89cecb4cb 100755 --- a/packages/Webkul/Customer/src/Models/CustomerAddress.php +++ b/packages/Webkul/Customer/src/Models/CustomerAddress.php @@ -22,10 +22,12 @@ class CustomerAddress extends Address implements CustomerAddressContract * * @return void */ - protected static function booted() + protected static function boot() { static::addGlobalScope('address_type', static function (Builder $builder) { $builder->where('address_type', self::ADDRESS_TYPE); }); + + parent::boot(); } } diff --git a/packages/Webkul/Sales/src/Database/Factories/OrderAddressFactory.php b/packages/Webkul/Sales/src/Database/Factories/OrderAddressFactory.php index cf5a1ffcd..d8b44c4bc 100644 --- a/packages/Webkul/Sales/src/Database/Factories/OrderAddressFactory.php +++ b/packages/Webkul/Sales/src/Database/Factories/OrderAddressFactory.php @@ -10,7 +10,7 @@ use Webkul\Sales\Models\OrderAddress; $factory->define(OrderAddress::class, function (Faker $faker) { $customer = factory(Customer::class)->create(); - $customerAddress = factory(CustomerAddress::class)->create(); + $customerAddress = factory(CustomerAddress::class)->make(); return [ 'first_name' => $customer->first_name, diff --git a/packages/Webkul/Sales/src/Models/Invoice.php b/packages/Webkul/Sales/src/Models/Invoice.php index a6e20c654..65d9a850a 100755 --- a/packages/Webkul/Sales/src/Models/Invoice.php +++ b/packages/Webkul/Sales/src/Models/Invoice.php @@ -56,7 +56,7 @@ class Invoice extends Model implements InvoiceContract } /** - * Get the addresses for the shipment. + * Get the address for the invoice. */ public function address() { diff --git a/packages/Webkul/Sales/src/Models/OrderAddress.php b/packages/Webkul/Sales/src/Models/OrderAddress.php index 2bb571bb1..051899a44 100755 --- a/packages/Webkul/Sales/src/Models/OrderAddress.php +++ b/packages/Webkul/Sales/src/Models/OrderAddress.php @@ -2,10 +2,19 @@ namespace Webkul\Sales\Models; +use Webkul\Checkout\Models\CartAddress; use Webkul\Core\Models\Address; use Webkul\Sales\Contracts\OrderAddress as OrderAddressContract; use Illuminate\Database\Eloquent\Builder; +/** + * Class OrderAddress + * @package Webkul\Sales\Models + * + * @property integer $order_id + * @property Order $order + * + */ class OrderAddress extends Address implements OrderAddressContract { public const ADDRESS_TYPE_SHIPPING = 'order_address_shipping'; @@ -23,14 +32,27 @@ class OrderAddress extends Address implements OrderAddressContract * * @return void */ - protected static function booted() + protected static function boot() { - static::addGlobalScope('address_type', static function (Builder $builder) { + static::addGlobalScope('address_type', function (Builder $builder) { $builder->whereIn('address_type', [ self::ADDRESS_TYPE_BILLING, self::ADDRESS_TYPE_SHIPPING ]); }); + + static::creating(static function ($address) { + switch ($address->address_type) { + case CartAddress::ADDRESS_TYPE_BILLING: + $address->address_type = self::ADDRESS_TYPE_BILLING; + break; + case CartAddress::ADDRESS_TYPE_SHIPPING: + $address->address_type = self::ADDRESS_TYPE_SHIPPING; + break; + } + }); + + parent::boot(); } /** diff --git a/packages/Webkul/Sales/src/Models/Shipment.php b/packages/Webkul/Sales/src/Models/Shipment.php index a5983f091..8dc2cbb87 100755 --- a/packages/Webkul/Sales/src/Models/Shipment.php +++ b/packages/Webkul/Sales/src/Models/Shipment.php @@ -47,7 +47,7 @@ class Shipment extends Model implements ShipmentContract } /** - * Get the addresses for the shipment. + * Get the address for the shipment. */ public function address() { diff --git a/tests/unit/Core/AddressCest.php b/tests/unit/Core/AddressCest.php index b307d38e1..ecc0bd8be 100644 --- a/tests/unit/Core/AddressCest.php +++ b/tests/unit/Core/AddressCest.php @@ -18,6 +18,25 @@ use Webkul\Shipping\Carriers\Free; class AddressCest { + public function testAddressFilters(UnitTester $I): void + { + $cartAddress = $I->have(CartAddress::class); + $customerAddress = $I->have(CustomerAddress::class); + $orderAddress = $I->have(OrderAddress::class); + + $cartAddresses = CartAddress::all(); + $I->assertCount(1, $cartAddresses); + $I->assertEquals($cartAddress->id, $cartAddresses[0]->id); + + $customerAddresses = CustomerAddress::all(); + $I->assertCount(1, $customerAddresses); + $I->assertEquals($customerAddress->id, $customerAddresses[0]->id); + + $orderAddresses = OrderAddress::all(); + $I->assertCount(1, $orderAddresses); + $I->assertEquals($orderAddress->id, $orderAddresses[0]->id); + } + public function testCustomerAddressRelations(UnitTester $I): void { /** @var Customer $customer1 */ From 754c52e5568abfc525496d8e2a6417b6c9baf6d9 Mon Sep 17 00:00:00 2001 From: Florian Bosdorff Date: Mon, 20 Apr 2020 08:22:31 +0200 Subject: [PATCH 07/54] rename address types --- packages/Webkul/Checkout/src/Models/CartAddress.php | 4 ++-- .../Migrations/2020_04_16_185147_add_table_addresses.php | 4 ++-- packages/Webkul/Sales/src/Models/OrderAddress.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/Webkul/Checkout/src/Models/CartAddress.php b/packages/Webkul/Checkout/src/Models/CartAddress.php index 8e81039e7..88cb4ec29 100755 --- a/packages/Webkul/Checkout/src/Models/CartAddress.php +++ b/packages/Webkul/Checkout/src/Models/CartAddress.php @@ -16,8 +16,8 @@ use Webkul\Core\Models\Address; */ class CartAddress extends Address implements CartAddressContract { - public const ADDRESS_TYPE_SHIPPING = 'cart_address_shipping'; - public const ADDRESS_TYPE_BILLING = 'cart_address_billing'; + public const ADDRESS_TYPE_SHIPPING = 'cart_shipping'; + public const ADDRESS_TYPE_BILLING = 'cart_billing'; /** * @var array default values diff --git a/packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php b/packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php index 0e98f344f..d44082d29 100644 --- a/packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php +++ b/packages/Webkul/Core/src/Database/Migrations/2020_04_16_185147_add_table_addresses.php @@ -154,7 +154,7 @@ SQL; updated_at ) SELECT - (CASE WHEN ca.address_type='billing' THEN "cart_address_billing" ELSE "cart_address_shipping" END), + (CASE WHEN ca.address_type='billing' THEN "cart_billing" ELSE "cart_shipping" END), customer_id, cart_id, first_name, @@ -204,7 +204,7 @@ SQL; updated_at ) SELECT - (CASE WHEN oa.address_type='billing' THEN "order_address_billing" ELSE "order_address_shipping" END), + (CASE WHEN oa.address_type='billing' THEN "order_billing" ELSE "order_shipping" END), customer_id, order_id, first_name, diff --git a/packages/Webkul/Sales/src/Models/OrderAddress.php b/packages/Webkul/Sales/src/Models/OrderAddress.php index 051899a44..a8a63f700 100755 --- a/packages/Webkul/Sales/src/Models/OrderAddress.php +++ b/packages/Webkul/Sales/src/Models/OrderAddress.php @@ -17,8 +17,8 @@ use Illuminate\Database\Eloquent\Builder; */ class OrderAddress extends Address implements OrderAddressContract { - public const ADDRESS_TYPE_SHIPPING = 'order_address_shipping'; - public const ADDRESS_TYPE_BILLING = 'order_address_billing'; + public const ADDRESS_TYPE_SHIPPING = 'order_shipping'; + public const ADDRESS_TYPE_BILLING = 'order_billing'; /** * @var array default values From 1403364942d84d694ed7cd19a92050b632c68a02 Mon Sep 17 00:00:00 2001 From: Shubham Mehrotra Date: Mon, 20 Apr 2020 12:19:15 +0530 Subject: [PATCH 08/54] responsive cart count update --- .../Resources/views/shop/UI/header.blade.php | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/Webkul/Velocity/src/Resources/views/shop/UI/header.blade.php b/packages/Webkul/Velocity/src/Resources/views/shop/UI/header.blade.php index 65d9b7df4..5f9c1f9da 100644 --- a/packages/Webkul/Velocity/src/Resources/views/shop/UI/header.blade.php +++ b/packages/Webkul/Velocity/src/Resources/views/shop/UI/header.blade.php @@ -217,7 +217,7 @@ v-for="(thirdLevelCategory, index) in nestedSubCategory.children"> + :href="`${$root.baseUrl}/${subCategory.slug}/${nestedSubCategory.slug}/${thirdLevelCategory.slug}`"> +
+ + + + + + @{{ errors.first('address-form.billing[country]') }} + +
+
-
- - - - - - @{{ errors.first('address-form.billing[country]') }} - -
-
+
+ + + + + + @{{ errors.first('address-form.shipping[country]') }} + +
+
-
- - - - - - @{{ errors.first('address-form.shipping[country]') }} - -
-
@@ -28,4 +28,4 @@ {!! view_render_event('bagisto.admin.catalog.attributes.list.after') !!}
-@stop \ No newline at end of file +@stop From d973ec94c01a30e60577173ea4018b0cd0b1d642 Mon Sep 17 00:00:00 2001 From: Steffen Mahler Date: Tue, 28 Apr 2020 18:15:40 +0200 Subject: [PATCH 30/54] add CartRuleCest --- .../Webkul/CartRule/src/Helpers/CartRule.php | 32 +- .../Webkul/CartRule/src/Listeners/Order.php | 8 +- .../src/Repositories/CartRuleRepository.php | 2 +- packages/Webkul/Checkout/src/Cart.php | 37 +- .../Factories/CartRuleCouponFactory.php | 20 + .../Database/Factories/CartRuleFactory.php | 30 + packages/Webkul/Tax/src/Helpers/Tax.php | 16 +- tests/unit/CartRule/CartRuleCest.php | 1079 +++++++++++++++++ 8 files changed, 1187 insertions(+), 37 deletions(-) create mode 100644 packages/Webkul/Core/src/Database/Factories/CartRuleCouponFactory.php create mode 100644 packages/Webkul/Core/src/Database/Factories/CartRuleFactory.php create mode 100644 tests/unit/CartRule/CartRuleCest.php diff --git a/packages/Webkul/CartRule/src/Helpers/CartRule.php b/packages/Webkul/CartRule/src/Helpers/CartRule.php index 08c6b05b4..2ea493156 100644 --- a/packages/Webkul/CartRule/src/Helpers/CartRule.php +++ b/packages/Webkul/CartRule/src/Helpers/CartRule.php @@ -134,11 +134,14 @@ class CartRule */ public function getCartRules() { - static $cartRules; - - if ($cartRules) { - return $cartRules; + $staticCartRules = new class() { + public static $cartRules; + public static $cartID; + }; + if ($staticCartRules::$cartID === cart()->getCart()->id && $staticCartRules::$cartRules) { + return $staticCartRules::$cartRules; } + $staticCartRules::$cartID = cart()->getCart()->id; $customerGroupId = null; @@ -166,6 +169,7 @@ class CartRule ->orderBy('sort_order', 'asc'); })->findWhere(['status' => 1]); + $staticCartRules::$cartRules = $cartRules; return $cartRules; } @@ -246,6 +250,10 @@ class CartRule continue; } + if ($rule->coupon_code) { + $item->coupon_code = $rule->coupon_code; + } + $quantity = $rule->discount_quantity ? min($item->quantity, $rule->discount_quantity) : $item->quantity; $discountAmount = $baseDiscountAmount = 0; @@ -254,9 +262,9 @@ class CartRule case 'by_percent': $rulePercent = min(100, $rule->discount_amount); - $discountAmount = ($quantity * $item->price - $item->discount_amount) * ($rulePercent / 100); + $discountAmount = ($quantity * $item->price + $item->tax_amount - $item->discount_amount) * ($rulePercent / 100); - $baseDiscountAmount = ($quantity * $item->base_price - $item->base_discount_amount) * ($rulePercent / 100); + $baseDiscountAmount = ($quantity * $item->base_price + $item->base_tax_amount - $item->base_discount_amount) * ($rulePercent / 100); if (! $rule->discount_quantity || $rule->discount_quantity > $quantity) { $discountPercent = min(100, $item->discount_percent + $rulePercent); @@ -316,8 +324,16 @@ class CartRule break; } - $item->discount_amount = min($item->discount_amount + $discountAmount, $item->price * $quantity + $item->tax_amount); - $item->base_discount_amount = min($item->base_discount_amount + $baseDiscountAmount, $item->base_price * $quantity + $item->base_tax_amount); + $item->discount_amount = round( + min( + $item->discount_amount + $discountAmount, + $item->price * $quantity + $item->tax_amount + ),2); + $item->base_discount_amount = round( + min( + $item->base_discount_amount + $baseDiscountAmount, + $item->base_price * $quantity + $item->base_tax_amount + ), 2); $appliedRuleIds[$rule->id] = $rule->id; diff --git a/packages/Webkul/CartRule/src/Listeners/Order.php b/packages/Webkul/CartRule/src/Listeners/Order.php index e51b8e0fa..b52e1e8b0 100755 --- a/packages/Webkul/CartRule/src/Listeners/Order.php +++ b/packages/Webkul/CartRule/src/Listeners/Order.php @@ -19,14 +19,14 @@ class Order /** * CartRuleCustomerRepository object * - * @var Webkul\CartRule\Repositories\\CartRuleCustomerRepository + * @var \Webkul\CartRule\Repositories\CartRuleCustomerRepository */ protected $cartRuleCustomerRepository; /** * CartRuleCouponRepository object * - * @var Webkul\CartRule\Repositories\\CartRuleCouponRepository + * @var \Webkul\CartRule\Repositories\CartRuleCouponRepository */ protected $cartRuleCouponRepository; @@ -64,7 +64,7 @@ class Order /** * Save cart rule and cart rule coupon properties after place order - * + * * @param \Webkul\Sales\Contracts\Order $order * @return void */ @@ -112,7 +112,7 @@ class Order } $coupon = $this->cartRuleCouponRepository->findOneByField('code', $order->coupon_code); - + if ($coupon) { $this->cartRuleCouponRepository->update(['times_used' => $coupon->times_used + 1], $coupon->id); diff --git a/packages/Webkul/CartRule/src/Repositories/CartRuleRepository.php b/packages/Webkul/CartRule/src/Repositories/CartRuleRepository.php index 0f277afcb..cd7507a5d 100755 --- a/packages/Webkul/CartRule/src/Repositories/CartRuleRepository.php +++ b/packages/Webkul/CartRule/src/Repositories/CartRuleRepository.php @@ -37,7 +37,7 @@ class CartRuleRepository extends Repository /** * CartRuleCouponRepository object * - * @var Webkul\CartRule\Repositories\CartRuleCouponRepository + * @var \Webkul\CartRule\Repositories\CartRuleCouponRepository */ protected $cartRuleCouponRepository; diff --git a/packages/Webkul/Checkout/src/Cart.php b/packages/Webkul/Checkout/src/Cart.php index 05a46f6c1..62be3d063 100755 --- a/packages/Webkul/Checkout/src/Cart.php +++ b/packages/Webkul/Checkout/src/Cart.php @@ -430,22 +430,21 @@ class Cart /** * Returns cart * - * @return \Webkul\Checkout\Contracts\Cart|null + * @return \Webkul\Checkout\Models\Cart|null */ - public function getCart() + public function getCart(): ?\Webkul\Checkout\Models\Cart { - $cart = null; - if ($this->getCurrentCustomer()->check()) { - $cart = $this->cartRepository->findOneWhere([ + return $this->cartRepository->findOneWhere([ 'customer_id' => $this->getCurrentCustomer()->user()->id, 'is_active' => 1, ]); + } elseif (session()->has('cart')) { - $cart = $this->cartRepository->find(session()->get('cart')->id); + return $this->cartRepository->find(session()->get('cart')->id); } - return $cart && $cart->is_active ? $cart : null; + return null; } /** @@ -479,7 +478,8 @@ class Cart * * @param array $data * - * @return void is the cart valid + * @return bool + * @throws \Prettus\Validator\Exceptions\ValidatorException */ public function saveCustomerAddress($data): bool { @@ -629,6 +629,7 @@ class Cart } else { foreach ($cart->items as $item) { $response = $item->product->getTypeInstance()->validateCartItem($item); + // ToDo: refactoring of all validateCartItem functions, at the moment they return nothing if ($response) { return; @@ -658,7 +659,7 @@ class Cart if (! $cart = $this->getCart()) { return; } - + Event::dispatch('checkout.cart.calculate.items.tax.before', $cart); foreach ($cart->items()->get() as $item) { @@ -737,7 +738,7 @@ class Cart * @param \Webkul\Checkout\Contracts\CartItem $item * @return \Webkul\Checkout\Contracts\CartItem */ - protected function setItemTaxToZero(CartItem $item): CartItem + protected function setItemTaxToZero(\Webkul\Checkout\Contracts\CartItem $item): \Webkul\Checkout\Contracts\CartItem { $item->tax_percent = 0; $item->tax_amount = 0; @@ -751,7 +752,7 @@ class Cart * * @return bool */ - public function hasError() + public function hasError(): bool { if (! $this->getCart()) { return true; @@ -769,7 +770,7 @@ class Cart * * @return bool */ - public function isItemsHaveSufficientQuantity() + public function isItemsHaveSufficientQuantity(): bool { foreach ($this->getCart()->items as $item) { if (! $this->isItemHaveQuantity($item)) { @@ -786,7 +787,7 @@ class Cart * @param \Webkul\Checkout\Contracts\CartItem $item * @return bool */ - public function isItemHaveQuantity($item) + public function isItemHaveQuantity($item): bool { return $item->product->getTypeInstance()->isItemHaveQuantity($item); } @@ -796,7 +797,7 @@ class Cart * * @return void */ - public function deActivateCart() + public function deActivateCart(): void { if ($cart = $this->getCart()) { $this->cartRepository->update(['is_active' => false], $cart->id); @@ -812,7 +813,7 @@ class Cart * * @return array */ - public function prepareDataForOrder() + public function prepareDataForOrder(): array { $data = $this->toArray(); @@ -871,7 +872,7 @@ class Cart * @param array $data * @return array */ - public function prepareDataForOrderItem($data) + public function prepareDataForOrderItem($data): array { $finalData = [ 'product' => $this->productRepository->find($data['product_id']), @@ -1029,9 +1030,9 @@ class Cart * When logged in as guest or the customer profile is not complete, we use the * billing address to fill the order customer_ data. * - * @param \Webkul\Checkout\Models\Cart $cart + * @param \Webkul\Checkout\Contracts\Cart $cart */ - private function assignCustomerFields(\Webkul\Checkout\Models\Cart $cart): void + private function assignCustomerFields(\Webkul\Checkout\Contracts\Cart $cart): void { if ($this->getCurrentCustomer()->check() && ($user = $this->getCurrentCustomer()->user()) diff --git a/packages/Webkul/Core/src/Database/Factories/CartRuleCouponFactory.php b/packages/Webkul/Core/src/Database/Factories/CartRuleCouponFactory.php new file mode 100644 index 000000000..db5d1d65b --- /dev/null +++ b/packages/Webkul/Core/src/Database/Factories/CartRuleCouponFactory.php @@ -0,0 +1,20 @@ +define(CartRuleCoupon::class, function (Faker $faker) { + return [ + 'code' => $faker->uuid(), + 'usage_limit' => 100, + 'usage_per_customer' => 100, + 'type' => 0, + 'is_primary' => 1, + 'cart_rule_id' => static function () { + return factory(CartRule::class)->create()->id; + }, + ]; +}); \ No newline at end of file diff --git a/packages/Webkul/Core/src/Database/Factories/CartRuleFactory.php b/packages/Webkul/Core/src/Database/Factories/CartRuleFactory.php new file mode 100644 index 000000000..330c3caa6 --- /dev/null +++ b/packages/Webkul/Core/src/Database/Factories/CartRuleFactory.php @@ -0,0 +1,30 @@ +define(CartRule::class, function (Faker $faker) { + return [ + 'name' => $faker->uuid, + 'description' => $faker->sentence(), + 'starts_from' => null, + 'ends_till' => null, + 'coupon_type' => '1', + 'use_auto_generation' => '0', + 'usage_per_customer' => '100', + 'uses_per_coupon' => '100', + 'times_used' => '0', + 'condition_type' => '2', + 'end_other_rules' => '0', + 'uses_attribute_conditions' => '0', + 'discount_quantity' => '0', + 'discount_step' => '0', + 'apply_to_shipping' => '0', + 'free_shipping' => '0', + 'sort_order' => '0', + 'status' => '1', + 'conditions' => null, + ]; +}); \ No newline at end of file diff --git a/packages/Webkul/Tax/src/Helpers/Tax.php b/packages/Webkul/Tax/src/Helpers/Tax.php index b6734a090..7f4952f81 100644 --- a/packages/Webkul/Tax/src/Helpers/Tax.php +++ b/packages/Webkul/Tax/src/Helpers/Tax.php @@ -11,11 +11,13 @@ class Tax /** * Returns an array with tax rates and tax amount - * @param object $that - * @param bool $asBase + * + * @param \Webkul\Checkout\Contracts\Cart $that + * @param bool $asBase + * * @return array */ - public static function getTaxRatesWithAmount(object $that, bool $asBase = false): array + public static function getTaxRatesWithAmount(\Webkul\Checkout\Models\Cart $that, bool $asBase = false): array { $taxes = []; @@ -34,11 +36,13 @@ class Tax /** * Returns the total tax amount - * @param object $that - * @param bool $asBase + * + * @param \Webkul\Checkout\Contracts\Cart $that + * @param bool $asBase + * * @return float */ - public static function getTaxTotal(object $that, bool $asBase = false): float + public static function getTaxTotal(\Webkul\Checkout\Models\Cart $that, bool $asBase = false): float { $taxes = self::getTaxRatesWithAmount($that, $asBase); diff --git a/tests/unit/CartRule/CartRuleCest.php b/tests/unit/CartRule/CartRuleCest.php new file mode 100644 index 000000000..af420af02 --- /dev/null +++ b/tests/unit/CartRule/CartRuleCest.php @@ -0,0 +1,1079 @@ +cartRule = $cartRule; + $this->coupon = $coupon; + } +} + +class expectedCartItem +{ + public const ITEM_DISCOUNT_AMOUNT_PRECISION = 2; + public const ITEM_TAX_AMOUNT_PRECISION = 4; + + public $cart_id; + public $product_id; + public $quantity = 1; + public $price = 0.0; + public $base_price = 0.0; + public $total = 0.0; + public $base_total = 0.0; + public $tax_percent = 0.0; + public $tax_amount = 0.0; + public $base_tax_amount = 0.0; + public $coupon_code = null; + public $discount_percent = 0.0; + public $discount_amount = 0.0; + public $base_discount_amount = 0.0; + public $applied_cart_rule_ids = ''; + + public function __construct(int $cartId, int $productId) + { + $this->cart_id = $cartId; + $this->product_id = $productId; + } + + public function calcTotals(): void + { + $this->total = $this->quantity * $this->price; + $this->base_total = $this->quantity * $this->price; + } + + public function calcTaxAmounts(): void + { + $this->tax_amount = round( + $this->quantity * $this->price * $this->tax_percent / 100, + self::ITEM_TAX_AMOUNT_PRECISION + ); + $this->base_tax_amount = round( + $this->quantity * $this->price * $this->tax_percent / 100, + self::ITEM_TAX_AMOUNT_PRECISION + ); + } + + public function calcFixedDiscountAmounts(float $discount, float $baseDiscount, string $code, int $cartRuleId): void + { + $this->discount_amount = $this->quantity * $discount; + $this->base_discount_amount = $this->quantity * $baseDiscount; + $this->coupon_code = $code; + $this->applied_cart_rule_ids = (string)$cartRuleId; + } + + public function calcPercentageDiscountAmounts(float $discount, string $code, int $cartRuleId): void + { + $this->discount_percent = $discount; + $this->discount_amount = round( + ($this->total + $this->tax_amount) * $this->discount_percent / 100, + self::ITEM_DISCOUNT_AMOUNT_PRECISION + ); + $this->base_discount_amount = round( + ($this->base_total + $this->base_tax_amount) * $this->discount_percent / 100, + self::ITEM_DISCOUNT_AMOUNT_PRECISION + ); + $this->coupon_code = $code; + $this->applied_cart_rule_ids = (string)$cartRuleId; + } +} + +class expectedCart +{ + public const CART_TOTAL_PRECISION = 2; + + public $customer_id; + public $id; + public $items_count = 0; + public $items_qty = 0.0; + public $sub_total = 0.0; + public $tax_total = 0.0; + public $discount_amount = 0.0; + public $grand_total = 0.0; + public $base_sub_total = 0.0; + public $base_tax_total = 0.0; + public $base_discount_amount = 0.0; + public $base_grand_total = 0.0; + public $coupon_code = null; + public $applied_cart_rule_ids = ''; + + public function __construct(int $cartId, int $customerId) + { + $this->id = $cartId; + $this->customer_id = $customerId; + } + + public function applyCoupon(int $cartRuleId, string $couponCode): void + { + $this->coupon_code = $couponCode; + $this->applied_cart_rule_ids = (string)$cartRuleId; + } + + public function finalizeTotals(): void + { + $this->sub_total = round($this->sub_total, self::CART_TOTAL_PRECISION); + $this->tax_total = round($this->tax_total, self::CART_TOTAL_PRECISION); + $this->grand_total = $this->sub_total + $this->tax_total - $this->discount_amount; + + $this->base_sub_total = round($this->base_sub_total, self::CART_TOTAL_PRECISION); + $this->base_tax_total = round($this->base_tax_total, self::CART_TOTAL_PRECISION); + $this->base_grand_total = $this->base_sub_total + $this->base_tax_total - $this->base_discount_amount; + } + + public function toArray(): array + { + return (array)$this; + } +} + +class expectedOrder +{ + public $status; + public $customer_email; + public $customer_first_name; + public $customer_vat_id; + public $coupon_code; + public $total_item_count; + public $total_qty_ordered; + public $grand_total; + public $base_grand_total; + public $sub_total; + public $base_sub_total; + public $discount_amount; + public $base_discount_amount; + public $tax_amount; + public $base_tax_amount; + public $customer_id; + public $cart_id; + public $applied_cart_rule_ids; + public $shipping_method; + public $shipping_amount; + public $base_shipping_amount; + public $shipping_discount_amount; + + public function __construct(expectedCart $expectedCart, Customer $customer, int $cartId) + { + $this->status = 'pending'; + $this->customer_email = $customer->email; + $this->customer_first_name = $customer->first_name; + $this->customer_vat_id = $customer->vat_id; + $this->coupon_code = $expectedCart->coupon_code; + $this->total_item_count = $expectedCart->items_count; + $this->total_qty_ordered = $expectedCart->items_qty; + $this->grand_total = $expectedCart->grand_total; + $this->base_grand_total = $expectedCart->base_grand_total; + $this->sub_total = $expectedCart->sub_total; + $this->base_sub_total = $expectedCart->base_sub_total; + $this->discount_amount = $expectedCart->discount_amount; + $this->base_discount_amount = $expectedCart->base_discount_amount; + $this->tax_amount = $expectedCart->tax_total; + $this->base_tax_amount = $expectedCart->base_tax_total; + $this->customer_id = $customer->id; + $this->cart_id = $cartId; + $this->applied_cart_rule_ids = $expectedCart->applied_cart_rule_ids; + $this->shipping_method = null; + $this->shipping_amount = null; + $this->base_shipping_amount = null; + $this->shipping_discount_amount = null; + } +} + +class CartRuleCest +{ + private $products; + private $sessionToken; + + public const PRODUCT_PRICE = 13.57; + public const REDUCED_PRODUCT_PRICE = 7.21; + public const TAX_RATE = 18.5; + public const REDUCED_TAX_RATE = 5.5; + + public const DISCOUNT_AMOUNT_FIX = 3.37; + public const DISCOUNT_AMOUNT_PERCENT = 7.5; + public const DISCOUNT_AMOUNT_FIX_FULL = 999999.99; + public const DISCOUNT_AMOUNT_CART = 8.33; + + public const ACTION_TYPE_FIXED = "by_fixed"; + public const ACTION_TYPE_PERCENTAGE = "by_percent"; + public const ACTION_TYPE_CART_FIXED = "cart_fixed"; + + public const PRODUCT_FREE = 0; + public const PRODUCT_NOT_FREE = 1; + public const PRODUCT_NOT_FREE_REDUCED_TAX = 2; + + public const TAX_CATEGORY = 0; + public const TAX_REDUCED_CATEGORY = 1; + + public const COUPON_FIXED = 0; + public const COUPON_FIXED_FULL = 1; + public const COUPON_PERCENTAGE = 2; + public const COUPON_PERCENTAGE_FULL = 3; + public const COUPON_FIXED_CART = 4; + + + protected function getCartWithCouponScenarios(): array + { + return [ + [ + 'name' => 'check cart coupon', + 'productSequence' => [ + self::PRODUCT_NOT_FREE, + self::PRODUCT_NOT_FREE_REDUCED_TAX, + self::PRODUCT_NOT_FREE, + ], + 'withCoupon' => true, + 'couponScenario' => [ + 'scenario' => self::COUPON_FIXED_CART, + 'products' => [ + ], + ], + 'checkOrder' => true, + ], + // ohne coupon + [ + 'name' => 'PRODUCT_FREE no coupon', + 'productSequence' => [ + self::PRODUCT_FREE, + ], + 'withCoupon' => false, + 'checkOrder' => false, + ], + [ + 'name' => 'PRODUCT_NOT_FREE no coupon', + 'productSequence' => [ + self::PRODUCT_NOT_FREE, + ], + 'withCoupon' => false, + 'checkOrder' => false, + ], + // fixer Coupon für ein Produkt (Warenkorb wird nicht 0) + [ + 'name' => 'PRODUCT_NOT_FREE fix coupon', + 'productSequence' => [ + self::PRODUCT_NOT_FREE, + ], + 'withCoupon' => true, + 'couponScenario' => [ + 'scenario' => self::COUPON_FIXED, + 'products' => [ + self::PRODUCT_NOT_FREE, + ], + ], + 'checkOrder' => false, + ], + [ + 'name' => 'check fix coupon on product with quantity=2', + 'productSequence' => [ + self::PRODUCT_NOT_FREE, + self::PRODUCT_NOT_FREE_REDUCED_TAX, + self::PRODUCT_NOT_FREE, + ], + 'withCoupon' => true, + 'couponScenario' => [ + 'scenario' => self::COUPON_FIXED, + 'products' => [ + self::PRODUCT_NOT_FREE, + ], + ], + 'checkOrder' => false, + ], + [ + 'name' => 'check fix coupon applied to two products', + 'productSequence' => [ + self::PRODUCT_NOT_FREE, + self::PRODUCT_NOT_FREE_REDUCED_TAX, + self::PRODUCT_NOT_FREE, + ], + 'withCoupon' => true, + 'couponScenario' => [ + 'scenario' => self::COUPON_FIXED, + 'products' => [ + self::PRODUCT_NOT_FREE, + self::PRODUCT_NOT_FREE_REDUCED_TAX, + ], + ], + 'checkOrder' => true, + ], + // prozenturaler Coupon für ein Produkt (Warenkorb wird nicht 0) + [ + 'name' => 'PRODUCT_NOT_FREE percentage coupon', + 'productSequence' => [ + self::PRODUCT_NOT_FREE, + ], + 'withCoupon' => true, + 'couponScenario' => [ + 'scenario' => self::COUPON_PERCENTAGE, + 'products' => [ + self::PRODUCT_NOT_FREE, + ], + ], + 'checkOrder' => false, + ], + [ + 'name' => 'check percentage coupon on product with quantity=2', + 'productSequence' => [ + self::PRODUCT_NOT_FREE, + self::PRODUCT_NOT_FREE_REDUCED_TAX, + self::PRODUCT_NOT_FREE, + ], + 'withCoupon' => true, + 'couponScenario' => [ + 'scenario' => self::COUPON_PERCENTAGE, + 'products' => [ + self::PRODUCT_NOT_FREE, + ], + ], + 'checkOrder' => false, + ], + [ + 'name' => 'check percentage coupon applied to two products', + 'productSequence' => [ + self::PRODUCT_NOT_FREE, + self::PRODUCT_NOT_FREE_REDUCED_TAX, + self::PRODUCT_NOT_FREE, + ], + 'withCoupon' => true, + 'couponScenario' => [ + 'scenario' => self::COUPON_PERCENTAGE, + 'products' => [ + self::PRODUCT_NOT_FREE, + self::PRODUCT_NOT_FREE_REDUCED_TAX, + ], + ], + 'checkOrder' => true, + ], + // fixer Coupon für ein Produkt (Warenkorb wird 0) + [ + 'name' => 'PRODUCT_NON_SUB_NOT_FREE fix coupon to zero', + 'productSequence' => [ + self::PRODUCT_NOT_FREE, + ], + 'withCoupon' => true, + 'couponScenario' => [ + 'scenario' => self::COUPON_FIXED_FULL, + 'products' => [ + self::PRODUCT_NOT_FREE, + ], + ], + 'checkOrder' => false, + ], + [ + 'name' => 'check fix coupon to zero on product with quantity=2', + 'productSequence' => [ + self::PRODUCT_NOT_FREE, + self::PRODUCT_NOT_FREE_REDUCED_TAX, + self::PRODUCT_NOT_FREE, + ], + 'withCoupon' => true, + 'couponScenario' => [ + 'scenario' => self::COUPON_FIXED_FULL, + 'products' => [ + self::PRODUCT_NOT_FREE, + ], + ], + 'checkOrder' => false, + ], + [ + 'name' => 'check fix coupon to zero applied to two products', + 'productSequence' => [ + self::PRODUCT_NOT_FREE, + self::PRODUCT_NOT_FREE_REDUCED_TAX, + self::PRODUCT_NOT_FREE, + ], + 'withCoupon' => true, + 'couponScenario' => [ + 'scenario' => self::COUPON_FIXED_FULL, + 'products' => [ + self::PRODUCT_NOT_FREE, + self::PRODUCT_NOT_FREE_REDUCED_TAX, + ], + ], + 'checkOrder' => true, + ], + // prozenturaler Coupon für ein Produkt (Warenkorb wird 0) + [ + 'name' => 'PRODUCT_NOT_FREE percentage coupon to zero', + 'productSequence' => [ + self::PRODUCT_NOT_FREE, + ], + 'withCoupon' => true, + 'couponScenario' => [ + 'scenario' => self::COUPON_PERCENTAGE_FULL, + 'products' => [ + self::PRODUCT_NOT_FREE, + ], + ], + 'checkOrder' => false, + ], + [ + 'name' => 'check percentage coupon to zero on product with quantity=2', + 'productSequence' => [ + self::PRODUCT_NOT_FREE, + self::PRODUCT_NOT_FREE_REDUCED_TAX, + self::PRODUCT_NOT_FREE, + ], + 'withCoupon' => true, + 'couponScenario' => [ + 'scenario' => self::COUPON_PERCENTAGE_FULL, + 'products' => [ + self::PRODUCT_NOT_FREE, + ], + ], + 'checkOrder' => false, + ], + [ + 'name' => 'check percentage coupon to zero applied to two products', + 'productSequence' => [ + self::PRODUCT_NOT_FREE, + self::PRODUCT_NOT_FREE_REDUCED_TAX, + self::PRODUCT_NOT_FREE, + ], + 'withCoupon' => true, + 'couponScenario' => [ + 'scenario' => self::COUPON_PERCENTAGE_FULL, + 'products' => [ + self::PRODUCT_NOT_FREE, + self::PRODUCT_NOT_FREE_REDUCED_TAX, + ], + ], + 'checkOrder' => true, + ], + ]; + } + + /** + * @param \UnitTester $I + * @param \Codeception\Example $scenario + * + * @dataProvider getCartWithCouponScenarios + * @group slow_unit + * @throws \Exception + */ + public function checkCartWithCoupon(UnitTester $I, Example $scenario): void + { + $faker = Factory::create(); + + config(['app.default_country' => 'DE']); + + $customer = $I->have(Customer::class); + auth()->guard('customer')->loginUsingId($customer->id); + Event::dispatch('customer.after.login', $customer['email']); + + $this->sessionToken = $faker->uuid; + session(['_token' => $this->sessionToken]); + + $taxCategories = $this->generateTaxCategories($I); + $this->products = $this->generateProducts($I, $scenario['productSequence'], $taxCategories); + + $cartRuleWithCoupon = null; + if ($scenario['withCoupon']) { + $cartRuleWithCoupon = $this->generateCartRuleWithCoupon($I, $scenario['couponScenario']); + } + + foreach ($scenario['productSequence'] as $productIndex) { + $data = [ + '_token' => session('_token'), + 'product_id' => $this->products[$productIndex]->id, + 'quantity' => 1, + ]; + + cart()->addProduct($this->products[$productIndex]->id, $data); + } + + if ($scenario['withCoupon']) { + $expectedCartCoupon = $cartRuleWithCoupon->coupon->code; + $I->comment('I try to use coupon code ' . $expectedCartCoupon); + cart()->setCouponCode($expectedCartCoupon)->collectTotals(); + } else { + $I->comment('I have no coupon'); + $expectedCartCoupon = null; + } + + $cart = cart()->getCart(); + $I->assertEquals($expectedCartCoupon, $cart->coupon_code); + + $expectedCartItems = $this->getExpectedCartItems($scenario, $cartRuleWithCoupon, $cart->id); + $expectedCartItems = $this->checkMaxDiscount($expectedCartItems); + + foreach ($expectedCartItems as $expectedCartItem) { + $I->seeRecord('cart_items', $expectedCartItem); + } + + $expectedCart = $this->getExpectedCart($cart->id, $expectedCartItems, $cartRuleWithCoupon); + $I->seeRecord(\Webkul\Checkout\Models\Cart::class, $expectedCart->toArray()); + + if ($scenario['checkOrder']) { + $I->wantTo('create and check order from cart'); + + $customerAddress = $I->have(CustomerAddress::class, [ + 'first_name' => $customer->first_name, + 'last_name' => $customer->last_name, + 'country' => 'DE', + ]); + + $billing = [ + 'address1' => $customerAddress->address1, + 'use_for_shipping' => true, + 'first_name' => $customerAddress->first_name, + 'last_name' => $customerAddress->last_name, + 'email' => $customer->email, + 'company_name' => $customerAddress->company_name, + 'city' => $customerAddress->city, + 'postcode' => $customerAddress->postcode, + 'country' => $customerAddress->country, + 'state' => $customerAddress->state, + 'phone' => $customerAddress->phone, + ]; + + $shipping = [ + 'address1' => '', + 'first_name' => $customerAddress->first_name, + 'last_name' => $customerAddress->last_name, + 'email' => $customer->email, + ]; + + cart()->saveCustomerAddress([ + 'billing' => $billing, + 'shipping' => $shipping, + ]); + + cart()->saveShippingMethod('free_free'); + cart()->savePaymentMethod(['method' => 'mollie_creditcard']); + $I->assertFalse(cart()->hasError()); + $orderItemRepository = new OrderItemRepository(app()); + $downloadableLinkRepository = new ProductDownloadableLinkRepository(app()); + $downloadableLinkPurchasedRepository = + new DownloadableLinkPurchasedRepository($downloadableLinkRepository, app()); + $orderRepository = new OrderRepository($orderItemRepository, $downloadableLinkPurchasedRepository, app()); + + $orderRepository->create(cart()->prepareDataForOrder()); + $expectedOrder = new expectedOrder($expectedCart, $customer, $cart->id); + $I->seeRecord('orders', $expectedOrder); + + auth()->guard('customer')->logout(); + } + } + + /** + * @param \Codeception\Example $scenario + * @param \Tests\Unit\Category\cartRuleWithCoupon $cartRuleWithCoupon + * @param int $cartID + * + * @return array + */ + private function getExpectedCartItems( + Example $scenario, + ?cartRuleWithCoupon $cartRuleWithCoupon, + int $cartID + ): array { + $cartItems = []; + + foreach ($scenario['productSequence'] as $key => $item) { + $pos = $this->array_find( + 'product_id', + $this->products[$scenario['productSequence'][$key]]->id, + $cartItems, + true + ); + + if ($pos === null) { + $cartItem = new expectedCartItem( + $cartID, + $this->products[$scenario['productSequence'][$key]]->id + ); + + } else { + $cartItem = $cartItems[$pos]; + $cartItem->quantity++; + } + + switch ($item) { + case self::PRODUCT_FREE: + $cartItem->tax_percent = self::TAX_RATE; + break; + + case self::PRODUCT_NOT_FREE: + $cartItem->price = self::PRODUCT_PRICE; + $cartItem->base_price = self::PRODUCT_PRICE; + $cartItem->tax_percent = self::TAX_RATE; + + $cartItem->calcTotals(); + $cartItem->calcTaxAmounts(); + break; + + case self::PRODUCT_NOT_FREE_REDUCED_TAX: + $cartItem->price = self::REDUCED_PRODUCT_PRICE; + $cartItem->base_price = self::REDUCED_PRODUCT_PRICE; + $cartItem->tax_percent = self::REDUCED_TAX_RATE; + + $cartItem->calcTotals(); + $cartItem->calcTaxAmounts(); + break; + } + + if ($scenario['withCoupon']) { + switch ($scenario['couponScenario']['scenario']) { + case self::COUPON_FIXED: + foreach ($scenario['couponScenario']['products'] as $couponItem) { + if ($item === $couponItem) { + $cartItem->calcFixedDiscountAmounts( + self::DISCOUNT_AMOUNT_FIX, + self::DISCOUNT_AMOUNT_FIX, + $cartRuleWithCoupon->coupon->code, + $cartRuleWithCoupon->cartRule->id + ); + continue; + } + } + break; + + case self::COUPON_FIXED_FULL: + foreach ($scenario['couponScenario']['products'] as $couponItem) { + if ($item === $couponItem) { + $cartItem->calcFixedDiscountAmounts( + self::DISCOUNT_AMOUNT_FIX_FULL, + self::DISCOUNT_AMOUNT_FIX_FULL, + $cartRuleWithCoupon->coupon->code, + $cartRuleWithCoupon->cartRule->id + ); + continue; + } + } + break; + + case self::COUPON_PERCENTAGE: + foreach ($scenario['couponScenario']['products'] as $couponItem) { + if ($item === $couponItem) { + $cartItem->calcPercentageDiscountAmounts( + self::DISCOUNT_AMOUNT_PERCENT, + $cartRuleWithCoupon->coupon->code, + $cartRuleWithCoupon->cartRule->id + ); + continue; + } + } + break; + + case self::COUPON_PERCENTAGE_FULL: + foreach ($scenario['couponScenario']['products'] as $couponItem) { + if ($item === $couponItem) { + $cartItem->calcPercentageDiscountAmounts( + 100.0, + $cartRuleWithCoupon->coupon->code, + $cartRuleWithCoupon->cartRule->id + ); + continue; + } + } + break; + } + } + + if ($pos === null) { + $cartItems[] = $cartItem; + + } else { + $cartItems[$pos] = $cartItem; + } + } + + if ($scenario['withCoupon'] && $scenario['couponScenario']['scenario'] === self::COUPON_FIXED_CART) { + $totals = $this->calcTotals($cartItems); + $cartItems = $this->splitDiscountToItems($cartItems, $cartRuleWithCoupon, $totals); + } + + return $cartItems; + } + + private function calcTotals(array $cartItems): array + { + $result = [ + 'subTotal' => 0.0, + 'baseSubTotal' => 0.0, + ]; + foreach ($cartItems as $expectedCartItem) { + $result['subTotal'] += $expectedCartItem->total; + $result['baseSubTotal'] += $expectedCartItem->base_total; + } + $result['subTotal'] = round($result['subTotal'], expectedCart::CART_TOTAL_PRECISION); + $result['baseSubTotal'] = round($result['baseSubTotal'], expectedCart::CART_TOTAL_PRECISION); + + return $result; + } + + private function splitDiscountToItems( + array $cartItems, + cartRuleWithCoupon $cartRuleWithCoupon, + array $totals + ): array { + $discountAmount = self::DISCOUNT_AMOUNT_CART; + $baseDiscountAmount = self::DISCOUNT_AMOUNT_CART; + // split coupon amount to cart items + $length = count($cartItems) - 1; + for ($i = 0; $i < $length; $i++) { + $cartItems[$i]->discount_amount = round( + self::DISCOUNT_AMOUNT_CART * $cartItems[$i]->total / $totals['subTotal'], + expectedCartItem::ITEM_DISCOUNT_AMOUNT_PRECISION + ); + $discountAmount -= $cartItems[$i]->discount_amount; + + $cartItems[$i]->base_discount_amount = round( + self::DISCOUNT_AMOUNT_CART * $cartItems[$i]->base_total / $totals['baseSubTotal'], + expectedCartItem::ITEM_DISCOUNT_AMOUNT_PRECISION + ); + $baseDiscountAmount -= $cartItems[$i]->discount_amount; + + $cartItems[$i]->coupon_code = $cartRuleWithCoupon->coupon->code; + $cartItems[$i]->applied_cart_rule_ids = (string)$cartRuleWithCoupon->cartRule->id; + } + + $cartItems[$length]->discount_amount = $discountAmount; + $cartItems[$length]->base_discount_amount = $baseDiscountAmount; + + $cartItems[$length]->coupon_code = $cartRuleWithCoupon->coupon->code; + $cartItems[$length]->applied_cart_rule_ids = (string)$cartRuleWithCoupon->cartRule->id; + + return $cartItems; + } + + /** + * @param array $expectedCartItems + * + * @return array + */ + private function checkMaxDiscount(array $expectedCartItems): array + { + foreach ($expectedCartItems as $key => $cartItem) { + $itemGrandTotal = round($cartItem->total + $cartItem->tax_amount, + expectedCartItem::ITEM_DISCOUNT_AMOUNT_PRECISION); + if ($cartItem->discount_amount > $itemGrandTotal) { + $expectedCartItems[$key]->discount_amount = $itemGrandTotal; + } + + $itemBaseGrandTotal = round($cartItem->base_total + $cartItem->base_tax_amount, + expectedCartItem::ITEM_DISCOUNT_AMOUNT_PRECISION); + if ($cartItem->base_discount_amount > $itemBaseGrandTotal) { + $expectedCartItems[$key]->base_discount_amount = $itemBaseGrandTotal; + } + } + + return $expectedCartItems; + } + + /** + * @param int $cartId + * @param array $expectedCartItems + * + * @param \Tests\Unit\Category\cartRuleWithCoupon $cartRuleWithCoupon + * + * @return \Tests\Unit\Category\expectedCart + */ + private function getExpectedCart( + int $cartId, + array $expectedCartItems, + ?cartRuleWithCoupon $cartRuleWithCoupon + ): expectedCart { + $cart = new expectedCart( + $cartId, + auth()->guard('customer')->user()->id + ); + + if ($cartRuleWithCoupon) { + $cart->applyCoupon( + $cartRuleWithCoupon->cartRule->id, + $cartRuleWithCoupon->coupon->code + ); + } + + foreach ($expectedCartItems as $cartItem) { + $cart->items_count++; + $cart->items_qty += $cartItem->quantity; + + $cart->sub_total += $cartItem->total; + $cart->tax_total += $cartItem->tax_amount; + $cart->discount_amount += $cartItem->discount_amount; + + $cart->base_sub_total += $cartItem->base_total; + $cart->base_tax_total += $cartItem->base_tax_amount; + $cart->base_discount_amount += $cartItem->base_discount_amount; + } + + $cart->finalizeTotals(); + + return $cart; + } + + /** + * @param \UnitTester $I + * + * @return array + */ + private function generateTaxCategories(UnitTester $I): array + { + $result = []; + $country = strtoupper(Config::get('app.default_country')) ?? 'DE'; + foreach ($this->getTaxRateSpecifications() as $taxSpec => $taxRate) { + $tax = $I->have(TaxRate::class, [ + 'country' => $country, + 'tax_rate' => $taxRate, + ]); + + $taxCategorie = $I->have(TaxCategory::class); + + $I->have(TaxMap::class, [ + 'tax_rate_id' => $tax->id, + 'tax_category_id' => $taxCategorie->id, + ]); + + $result[$taxSpec] = $taxCategorie->id; + } + + return $result; + } + + /** + * @param \UnitTester $I + * @param array $scenario + * @param array $taxCategories + * + * @return array + * @throws \Exception + */ + private function generateProducts(UnitTester $I, array $scenario, array $taxCategories): array + { + $products = []; + $productSpecs = $this->getProductSpecifications(); + + foreach ($scenario as $item) { + $productConfig = $this->makeProductConfig($productSpecs[$item], $taxCategories); + $products[$item] = $I->haveProduct($productSpecs[$item]['productType'], $productConfig); + } + + return $products; + } + + /** + * @param \UnitTester $I + * @param array $couponConfig + * + * @return \Tests\Unit\Category\cartRuleWithCoupon + */ + private function generateCartRuleWithCoupon(UnitTester $I, array $couponConfig): cartRuleWithCoupon + { + $faker = Factory::create(); + + $couponSpecifications = $this->getCouponSpecifications(); + $ruleConfig = $this->makeRuleConfig( + $couponSpecifications[$couponConfig['scenario']], + $this->products, + $couponConfig['products'] + ); + $cartRule = $I->have(CartRule::class, $ruleConfig); + + DB::table('cart_rule_channels')->insert([ + 'cart_rule_id' => $cartRule->id, + 'channel_id' => core()->getCurrentChannel()->id, + ]); + + $guestCustomerGroup = $I->grabRecord('customer_groups', ['code' => 'guest']); + DB::table('cart_rule_customer_groups')->insert([ + 'cart_rule_id' => $cartRule->id, + 'customer_group_id' => $guestCustomerGroup['id'], + ]); + + $generalCustomerGroup = $I->grabRecord('customer_groups', ['code' => 'general']); + DB::table('cart_rule_customer_groups')->insert([ + 'cart_rule_id' => $cartRule->id, + 'customer_group_id' => $generalCustomerGroup['id'], + ]); + + $coupon = $I->have(CartRuleCoupon::class, [ + 'cart_rule_id' => $cartRule->id, + ]); + + return new cartRuleWithCoupon( + $cartRule, + $coupon + ); + + } + + /** + * @param array $productSpec + * @param array $taxCategories + * + * @return array + */ + private function makeProductConfig(array $productSpec, array $taxCategories): array + { + $result = [ + 'productInventory' => [ + 'qty' => 100, + ], + 'attributeValues' => [ + 'price' => 0.0, + 'tax_category_id' => $taxCategories[self::TAX_CATEGORY], + ], + ]; + + if ($productSpec['reducedTax']) { + if (array_key_exists(self::TAX_REDUCED_CATEGORY, $taxCategories)) { + $result['attributeValues']['tax_category_id'] = $taxCategories[self::TAX_REDUCED_CATEGORY]; + } + } + + if (!$productSpec['freeOfCharge']) { + if ($productSpec['reducedTax']) { + $result['attributeValues']['price'] = self::REDUCED_PRODUCT_PRICE; + } else { + $result['attributeValues']['price'] = self::PRODUCT_PRICE; + } + } + + return $result; + } + + /** + * @param array $ruleSpec + * @param array $products + * @param array $couponableProducts + * + * @return array + */ + private function makeRuleConfig(array $ruleSpec, array $products, array $couponableProducts): array + { + foreach ($couponableProducts as $item) { + $conditions[] = [ + 'value' => $products[$item]->sku, + 'operator' => '==', + 'attribute' => 'product|sku', + 'attribute_type' => 'text', + ]; + } + + $result = [ + 'action_type' => $ruleSpec['actionType'], + 'discount_amount' => $ruleSpec['discountAmount'], + 'conditions' => $conditions ?? null, + ]; + + return $result; + } + + /** + * @return array + */ + private function getProductSpecifications(): array + { + return [ + [ + 'productScenario' => self::PRODUCT_FREE, + 'productType' => Laravel5Helper::SIMPLE_PRODUCT, + 'freeOfCharge' => true, + 'reducedTax' => false, + ], + [ + 'productScenario' => self::PRODUCT_NOT_FREE, + 'productType' => Laravel5Helper::SIMPLE_PRODUCT, + 'freeOfCharge' => false, + 'reducedTax' => false, + ], + [ + 'productScenario' => self::PRODUCT_NOT_FREE_REDUCED_TAX, + 'productType' => Laravel5Helper::SIMPLE_PRODUCT, + 'freeOfCharge' => false, + 'reducedTax' => true, + ], + ]; + } + + /** + * @return array + */ + private function getCouponSpecifications(): array + { + return [ + [ + 'couponScenario' => self::COUPON_FIXED, + 'actionType' => self::ACTION_TYPE_FIXED, + 'discountAmount' => self::DISCOUNT_AMOUNT_FIX, + ], + [ + 'couponScenario' => self::COUPON_FIXED_FULL, + 'actionType' => self::ACTION_TYPE_FIXED, + 'discountAmount' => self::DISCOUNT_AMOUNT_FIX_FULL, + ], + [ + 'couponScenario' => self::COUPON_PERCENTAGE, + 'actionType' => self::ACTION_TYPE_PERCENTAGE, + 'discountAmount' => self::DISCOUNT_AMOUNT_PERCENT, + ], + [ + 'couponScenario' => self::COUPON_PERCENTAGE_FULL, + 'actionType' => self::ACTION_TYPE_PERCENTAGE, + 'discountAmount' => 100.0, + ], + [ + 'couponScenario' => self::COUPON_FIXED_CART, + 'actionType' => self::ACTION_TYPE_CART_FIXED, + 'discountAmount' => self::DISCOUNT_AMOUNT_CART, + ], + ]; + } + + /** + * @return array + */ + private function getTaxRateSpecifications(): array + { + return [ + self::TAX_CATEGORY => self::TAX_RATE, + self::TAX_REDUCED_CATEGORY => self::REDUCED_TAX_RATE, + ]; + } + + /** + * @param string $param + * @param $needleValue + * @param array $data + * + * @return int|null + */ + private function array_find(string $param, $needleValue, array $data): ?int + { + foreach ($data as $pos => $object) { + if ($object->$param === $needleValue) { + return $pos; + } + } + + return null; + } + + +} \ No newline at end of file From 3154d27f52f88619109eb7ea18901e0866b2b82a Mon Sep 17 00:00:00 2001 From: Herbert Maschke Date: Wed, 29 Apr 2020 09:19:47 +0200 Subject: [PATCH 31/54] introduce channel and customer group filter for cart rule grid --- .../Admin/src/DataGrids/CartRuleDataGrid.php | 41 +++- .../Admin/src/DataGrids/ProductDataGrid.php | 8 +- .../Admin/src/Resources/lang/en/app.php | 5 +- .../views/catalog/products/index.blade.php | 3 +- .../promotions/cart-rules/index.blade.php | 48 ++++- packages/Webkul/Core/src/Core.php | 178 +++++++++++------- .../{order => Order}/OrderRepositoryCest.php | 0 7 files changed, 205 insertions(+), 78 deletions(-) rename tests/unit/Sales/{order => Order}/OrderRepositoryCest.php (100%) diff --git a/packages/Webkul/Admin/src/DataGrids/CartRuleDataGrid.php b/packages/Webkul/Admin/src/DataGrids/CartRuleDataGrid.php index ad7e167bb..71ecf723c 100644 --- a/packages/Webkul/Admin/src/DataGrids/CartRuleDataGrid.php +++ b/packages/Webkul/Admin/src/DataGrids/CartRuleDataGrid.php @@ -11,18 +11,51 @@ class CartRuleDataGrid extends DataGrid protected $sortOrder = 'desc'; + protected $customer_group = 'all'; + + protected $channel = 'all'; + + public function __construct() + { + parent::__construct(); + + $this->customer_group = request()->get('customer_group') ?? 'all'; + + $this->channel = request()->get('channel') ?? 'all'; + } + public function prepareQueryBuilder() { $queryBuilder = DB::table('cart_rules') - ->leftJoin('cart_rule_coupons', function($leftJoin) { + ->leftJoin('cart_rule_coupons', function ($leftJoin) { $leftJoin->on('cart_rule_coupons.cart_rule_id', '=', 'cart_rules.id') - ->where('cart_rule_coupons.is_primary', 1); + ->where('cart_rule_coupons.is_primary', 1); }) - ->addSelect('cart_rules.id', 'name', 'cart_rule_coupons.code as coupon_code', 'status', 'starts_from', 'ends_till', 'sort_order'); + ->addSelect('cart_rules.id', 'name', 'cart_rule_coupons.code as coupon_code', + 'status', 'starts_from', 'ends_till', 'sort_order'); $this->addFilter('id', 'cart_rules.id'); $this->addFilter('coupon_code', 'cart_rule_coupons.code'); + if ($this->customer_group !== 'all') { + $queryBuilder->leftJoin( + 'cart_rule_customer_groups', + 'cart_rule_customer_groups.cart_rule_id', + '=', + 'cart_rules.id' + ); + $queryBuilder->where('cart_rule_customer_groups.customer_group_id', $this->customer_group); + } + + if ($this->channel !== 'all') { + $queryBuilder->leftJoin( + 'cart_rule_channels', + 'cart_rule_channels.cart_rule_id', + '=', + 'cart_rules.id'); + $queryBuilder->where('cart_rule_channels.channel_id', $this->channel); + } + $this->setQueryBuilder($queryBuilder); } @@ -80,7 +113,7 @@ class CartRuleDataGrid extends DataGrid 'searchable' => true, 'sortable' => true, 'filterable' => true, - 'wrapper' => function($value) { + 'wrapper' => function ($value) { if ($value->status == 1) { return trans('admin::app.datagrid.active'); } else { diff --git a/packages/Webkul/Admin/src/DataGrids/ProductDataGrid.php b/packages/Webkul/Admin/src/DataGrids/ProductDataGrid.php index 3a35601e9..8f3664cf9 100644 --- a/packages/Webkul/Admin/src/DataGrids/ProductDataGrid.php +++ b/packages/Webkul/Admin/src/DataGrids/ProductDataGrid.php @@ -22,7 +22,7 @@ class ProductDataGrid extends DataGrid parent::__construct(); $this->locale = request()->get('locale') ?? 'all'; - + $this->channel = request()->get('channel') ?? 'all'; } @@ -119,7 +119,7 @@ class ProductDataGrid extends DataGrid 'sortable' => true, 'searchable' => false, 'filterable' => true, - 'wrapper' => function($value) { + 'wrapper' => function ($value) { if ($value->status == 1) { return trans('admin::app.datagrid.active'); } else { @@ -144,7 +144,7 @@ class ProductDataGrid extends DataGrid 'sortable' => true, 'searchable' => false, 'filterable' => false, - 'wrapper' => function($value) { + 'wrapper' => function ($value) { if (is_null($value->quantity)) { return 0; } else { @@ -161,7 +161,7 @@ class ProductDataGrid extends DataGrid 'method' => 'GET', 'route' => 'admin.catalog.products.edit', 'icon' => 'icon pencil-lg-icon', - 'condition' => function() { + 'condition' => function () { return true; }, ]); diff --git a/packages/Webkul/Admin/src/Resources/lang/en/app.php b/packages/Webkul/Admin/src/Resources/lang/en/app.php index db4d43661..92072943d 100755 --- a/packages/Webkul/Admin/src/Resources/lang/en/app.php +++ b/packages/Webkul/Admin/src/Resources/lang/en/app.php @@ -1306,8 +1306,9 @@ return [ 'order-number-suffix' => 'Order Number Suffix', 'default' => 'Default', 'sandbox' => 'Sandbox', - 'all-channels' => 'All', - 'all-locales' => 'All', + 'all-channels' => 'All Channels', + 'all-locales' => 'All Locales', + 'all-customer-groups' => 'All Customer groups', 'invoice-slip-design' => 'Invoice Slip Design', 'logo' => 'logo' ] diff --git a/packages/Webkul/Admin/src/Resources/views/catalog/products/index.blade.php b/packages/Webkul/Admin/src/Resources/views/catalog/products/index.blade.php index f01b54c2b..7832be207 100755 --- a/packages/Webkul/Admin/src/Resources/views/catalog/products/index.blade.php +++ b/packages/Webkul/Admin/src/Resources/views/catalog/products/index.blade.php @@ -19,7 +19,6 @@ @foreach (core()->getAllChannels() as $channelModel) -