Merge pull request #1984 from Haendlerbund/small-bugfix-and-tiny-refactoring

Small bugfix on showQuantityBox and tiny refactoring
This commit is contained in:
Jitendra Singh 2020-02-04 19:17:37 +05:30 committed by GitHub
commit 9d8315ce98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 468 additions and 47 deletions

View File

@ -42,4 +42,4 @@ MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
SHOP_MAIL_FROM=test@example.com
ADMIN_MAIL_TO=test@example.com
ADMIN_MAIL_TO=test@example.com

3
.gitignore vendored
View File

@ -24,6 +24,5 @@ yarn.lock
.php_cs.cache
storage/
storage/*.key
/docker-compose-collection/
/resources/themes/velocity/*

View File

@ -96,6 +96,7 @@
<label for="meta_description">{{ __('admin::app.cms.pages.meta_description') }}</label>
<textarea type="text" class="control" name="meta_description">{{ old('meta_description') }}</textarea>
</div>
</div>
</accordian>

View File

@ -104,12 +104,14 @@
<label for="meta_keywords">{{ __('admin::app.cms.pages.meta_keywords') }}</label>
<textarea type="text" class="control" name="{{$locale}}[meta_keywords]">{{ old($locale)['meta_keywords'] ?? ($page->translate($locale)['meta_keywords'] ?? '') }}</textarea>
</div>
<div class="control-group">
<label for="meta_description">{{ __('admin::app.cms.pages.meta_description') }}</label>
<textarea type="text" class="control" name="{{$locale}}[meta_description]">{{ old($locale)['meta_description'] ?? ($page->translate($locale)['meta_description'] ?? '') }}</textarea>
</div>
</div>
</accordian>

View File

@ -128,7 +128,7 @@ class Cart {
*
* @param integer $productId
* @param array $data
* @return Cart
* @return Mixed Cart on success, array with warning otherwise
*/
public function addProduct($productId, $data)
{
@ -136,8 +136,9 @@ class Cart {
$cart = $this->getCart();
if (! $cart && ! $cart = $this->create($data))
return;
if (! $cart && ! $cart = $this->create($data)) {
return ['warning' => __('shop::app.checkout.cart.item.error-add')];
}
$product = $this->productRepository->findOneByField('id', $productId);
@ -157,8 +158,9 @@ class Cart {
foreach ($cartProducts as $cartProduct) {
$cartItem = $this->getItemByProduct($cartProduct);
if (isset($cartProduct['parent_id']))
if (isset($cartProduct['parent_id'])) {
$cartProduct['parent_id'] = $parentCartItem->id;
}
if (! $cartItem) {
$cartItem = $this->cartItemRepository->create(array_merge($cartProduct, ['cart_id' => $cart->id]));
@ -166,6 +168,9 @@ class Cart {
if (isset($cartProduct['parent_id']) && $cartItem->parent_id != $parentCartItem->id) {
$cartItem = $this->cartItemRepository->create(array_merge($cartProduct, ['cart_id' => $cart->id]));
} else {
if ($product->getTypeInstance()->showQuantityBox() === false) {
return ['warning' => __('shop::app.checkout.cart.integrity.qty_impossible')];
}
$cartItem = $this->cartItemRepository->update($cartProduct, $cartItem->id);
}
}

View File

@ -123,13 +123,29 @@ class Cart extends Model implements CartContract
public function hasDownloadableItems()
{
foreach ($this->items as $item) {
if ($item->type == 'downloadable')
if (stristr($item->type,'downloadable') !== false) {
return true;
}
}
return false;
}
/**
* Returns true if cart contains one or many products with quantity box.
* (for example: simple, configurable, virtual)
* @return bool
*/
public function hasProductsWithQuantityBox(): bool
{
foreach ($this->items as $item) {
if ($item->product->getTypeInstance()->showQuantityBox() === true) {
return true;
}
}
return false;
}
/**
* Checks if cart has items that allow guest checkout
*
@ -145,4 +161,4 @@ class Cart extends Model implements CartContract
return true;
}
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace Webkul\Core\Helpers;
// here you can define custom actions
@ -7,11 +8,17 @@ namespace Webkul\Core\Helpers;
use Codeception\Module\Laravel5;
use Illuminate\Support\Facades\Event;
use Webkul\Product\Models\Product;
use Webkul\Product\Models\ProductAttributeValue;
use Webkul\Product\Models\ProductInventory;
use Webkul\Product\Models\ProductAttributeValue;
use Webkul\Product\Models\ProductDownloadableLink;
use Webkul\Product\Models\ProductDownloadableLinkTranslation;
class Laravel5Helper extends Laravel5
{
public const SIMPLE_PRODUCT = 1;
public const VIRTUAL_PRODUCT = 2;
public const DOWNLOADABLE_PRODUCT = 3;
/**
* Returns field name of given attribute.
*
@ -56,28 +63,154 @@ class Laravel5Helper extends Laravel5
}
return $attributes[$attribute];
}
/**
* @param array $attributeValueStates
* Helper function to generate products for testing
*
* @param int $productType
* @param array $configs
* @param array $productStates
*
* @return \Webkul\Product\Models\Product
* @part ORM
*/
public function haveProduct(
array $configs = [],
array $productStates = []
): Product {
public function haveProduct(int $productType, array $configs = [], array $productStates = []): Product
{
$I = $this;
/** @var Product $product */
$product = factory(Product::class)->states($productStates)->create($configs['productAttributes'] ?? []);;
$I->createAttributeValues($product->id,$configs['attributeValues'] ?? []);
$I->have(ProductInventory::class, array_merge($configs['productInventory'] ?? [], [
'product_id' => $product->id,
'inventory_source_id' => 1,
]));
Event::dispatch('catalog.product.create.after', $product);
switch ($productType) {
case self::DOWNLOADABLE_PRODUCT:
$product = $I->haveDownloadableProduct($configs, $productStates);
break;
case self::VIRTUAL_PRODUCT:
$product = $I->haveVirtualProduct($configs, $productStates);
break;
case self::SIMPLE_PRODUCT:
default:
$product = $I->haveSimpleProduct($configs, $productStates);
}
if ($product !== null) {
Event::dispatch('catalog.product.create.after', $product);
}
return $product;
}
private function createAttributeValues($id, array $attributeValues = [])
/**
* @param array $configs
* @param array $productStates
*
* @return \Webkul\Product\Models\Product
*/
private function haveSimpleProduct(array $configs = [], array $productStates = []): Product
{
$I = $this;
if (!in_array('simple', $productStates)) {
$productStates = array_merge($productStates, ['simple']);
}
/** @var Product $product */
$product = $I->createProduct($configs['productAttributes'] ?? [], $productStates);
$I->createAttributeValues($product->id, $configs['attributeValues'] ?? []);
$I->createInventory($product->id, $configs['productInventory'] ?? []);
return $product->refresh();
}
/**
* @param array $configs
* @param array $productStates
*
* @return \Webkul\Product\Models\Product
*/
private function haveVirtualProduct(array $configs = [], array $productStates = []): Product
{
$I = $this;
if (!in_array('virtual', $productStates)) {
$productStates = array_merge($productStates, ['virtual']);
}
/** @var Product $product */
$product = $I->createProduct($configs['productAttributes'] ?? [], $productStates);
$I->createAttributeValues($product->id, $configs['attributeValues'] ?? []);
$I->createInventory($product->id, $configs['productInventory'] ?? []);
return $product->refresh();
}
/**
* @param array $configs
* @param array $productStates
*
* @return \Webkul\Product\Models\Product
*/
private function haveDownloadableProduct(array $configs = [], array $productStates = []): Product
{
$I = $this;
if (!in_array('downloadable', $productStates)) {
$productStates = array_merge($productStates, ['downloadable']);
}
/** @var Product $product */
$product = $I->createProduct($configs['productAttributes'] ?? [], $productStates);
$I->createAttributeValues($product->id, $configs['attributeValues'] ?? []);
$I->createDownloadableLink($product->id);
return $product->refresh();
}
/**
* @param array $attributes
* @param array $states
*
* @return \Webkul\Product\Models\Product
*/
private function createProduct(array $attributes = [], array $states = []): Product
{
return factory(Product::class)->states($states)->create($attributes);
}
/**
* @param int $productId
* @param array $inventoryConfig
*/
private function createInventory(int $productId, array $inventoryConfig = []): void
{
$I = $this;
$I->have(ProductInventory::class, array_merge($inventoryConfig, [
'product_id' => $productId,
'inventory_source_id' => 1,
]));
}
/**
* @param int $productId
*/
private function createDownloadableLink(int $productId): void
{
$I = $this;
$link = $I->have(ProductDownloadableLink::class, [
'product_id' => $productId,
]);
$I->have(ProductDownloadableLinkTranslation::class, [
'product_downloadable_link_id' => $link->id,
]);
}
/**
* @param int $productId
* @param array $attributeValues
*/
private function createAttributeValues(int $productId, array $attributeValues = []): void
{
$I = $this;
$productAttributeValues = [
@ -100,10 +233,10 @@ class Laravel5Helper extends Laravel5
'weight',
];
foreach ($productAttributeValues as $attribute) {
$data = ['product_id' => $id];
$data = ['product_id' => $productId];
if (array_key_exists($attribute, $attributeValues)) {
$fieldName = self::getAttributeFieldName($attribute);
if (! array_key_exists($fieldName, $data)) {
if (!array_key_exists($fieldName, $data)) {
$data[$fieldName] = $attributeValues[$attribute];
} else {
$data = [$fieldName => $attributeValues[$attribute]];

View File

@ -3,6 +3,7 @@
/** @var \Illuminate\Database\Eloquent\Factory $factory */
use Faker\Generator as Faker;
use Webkul\Product\Models\Product;
use Webkul\Product\Models\ProductDownloadableLink;
use Webkul\Product\Models\ProductDownloadableLinkTranslation;
@ -17,6 +18,9 @@ $factory->define(ProductDownloadableLink::class, function (Faker $faker) {
'type' => 'file',
'price' => 0.0000,
'downloads' => $faker->randomNumber(1),
'product_id' => function () {
return factory(Product::class)->create()->id;
},
'created_at' => $now,
'updated_at' => $now,
];
@ -28,4 +32,3 @@ $factory->define(ProductDownloadableLinkTranslation::class, function (Faker $fak
'title' => $faker->word,
];
});

View File

@ -0,0 +1,17 @@
<?php
/** @var \Illuminate\Database\Eloquent\Factory $factory */
use Faker\Generator as Faker;
use Webkul\Product\Models\ProductDownloadableLink;
use Webkul\Product\Models\ProductDownloadableLinkTranslation;
$factory->define(ProductDownloadableLinkTranslation::class, function (Faker $faker) {
return [
'locale' => 'en',
'title' => $faker->word,
'product_downloadable_link_id' => function () {
return factory(ProductDownloadableLink::class)->create()->id;
},
];
});

View File

@ -18,6 +18,11 @@ $factory->define(Product::class, function (Faker $faker) {
$factory->state(Product::class, 'simple', [
'type' => 'simple',
]);
$factory->state(Product::class, 'downloadable_with_stock', [
$factory->state(Product::class, 'virtual', [
'type' => 'virtual',
]);
$factory->state(Product::class, 'downloadable', [
'type' => 'downloadable',
]);

View File

@ -55,8 +55,9 @@ class DownloadableLinkPurchasedRepository extends Repository
*/
public function saveLinks($orderItem)
{
if (stristr($orderItem->type,'downloadable') === false || ! isset($orderItem->additional['links']))
if (! $this->isValidDownloadableProduct($orderItem)) {
return;
}
foreach ($orderItem->additional['links'] as $linkId) {
if (! $productDownloadableLink = $this->productDownloadableLinkRepository->find($linkId))
@ -78,6 +79,19 @@ class DownloadableLinkPurchasedRepository extends Repository
}
}
/**
* Return true, if ordered item is valid downloadable product with links
*
* @param mixed $orderItem Webkul\Sales\Models\OrderItem;
* @return bool
*/
private function isValidDownloadableProduct($orderItem) : bool {
if (stristr($orderItem->type,'downloadable') !== false && isset($orderItem->additional['links'])) {
return true;
}
return false;
}
/**
* @param OrderItem $orderItem
* @param string $status

View File

@ -80,7 +80,12 @@ class CartController extends Controller
try {
$result = Cart::addProduct($id, request()->all());
if ($result) {
if ($this->onWarningAddingToCart($result)) {
session()->flash('warning', $result['warning']);
return redirect()->back();
}
if ($result instanceof Cart) {
session()->flash('success', trans('shop::app.checkout.cart.item.success'));
if ($customer = auth()->guard('customer')->user())
@ -88,8 +93,6 @@ class CartController extends Controller
if (request()->get('is_buy_now'))
return redirect()->route('shop.checkout.onepage.index');
} else {
session()->flash('warning', trans('shop::app.checkout.cart.item.error-add'));
}
} catch(\Exception $e) {
session()->flash('error', trans($e->getMessage()));
@ -205,4 +208,16 @@ class CartController extends Controller
'message' => trans('shop::app.checkout.total.remove-coupon')
]);
}
/**
* Returns true, if result of adding product to cart
* is an array and contains a key "warning"
*
* @param $result
*
* @return bool
*/
private function onWarningAddingToCart($result): bool {
return is_array($result) && isset($result['warning']);
}
}

View File

@ -2400,7 +2400,7 @@ section.cart {
.control-group {
font-size: 16px !important;
margin: 0px;
margin: 0 15px 0 0;
width: auto;
.wrap {
@ -2422,7 +2422,7 @@ section.cart {
.remove, .towishlist {
line-height: 35px;
margin-left: 15px;
margin-right: 15px;
}
}
}

View File

@ -426,7 +426,8 @@ return [
'missing_fields' => 'Some required fields missing for this product.',
'missing_options' => 'Options are missing for this product.',
'missing_links' => 'Downloadable links are missing for this product.',
'qty_missing' => 'Atleast one product should have more than 1 quantity.'
'qty_missing' => 'Atleast one product should have more than 1 quantity.',
'qty_impossible' => 'Cannot add more than one of these products to cart.'
],
'create-error' => 'Encountered some issue while making cart instance.',
'title' => 'Shopping Cart',

View File

@ -68,10 +68,12 @@
{!! view_render_event('bagisto.shop.checkout.cart.item.quantity.before', ['item' => $item]) !!}
<div class="misc">
<quantity-changer
:control-name="'qty[{{$item->id}}]'"
quantity="{{$item->quantity}}">
</quantity-changer>
@if ($item->product->getTypeInstance()->showQuantityBox() === true)
<quantity-changer
:control-name="'qty[{{$item->id}}]'"
quantity="{{$item->quantity}}">
</quantity-changer>
@endif
<span class="remove">
<a href="{{ route('shop.checkout.cart.remove', $item->id) }}" onclick="removeLink('{{ __('shop::app.checkout.cart.cart-remove-action') }}')">{{ __('shop::app.checkout.cart.remove-link') }}</a></span>
@ -106,9 +108,11 @@
<a href="{{ route('shop.home.index') }}" class="link">{{ __('shop::app.checkout.cart.continue-shopping') }}</a>
<div>
<button type="submit" class="btn btn-lg btn-primary">
@if ($cart->hasProductsWithQuantityBox())
<button type="submit" class="btn btn-lg btn-primary" id="update_cart_button">
{{ __('shop::app.checkout.cart.update-cart') }}
</button>
@endif
@if (! cart()->hasError())
<a href="{{ route('shop.checkout.onepage.index') }}" class="btn btn-lg btn-primary">

View File

@ -1,5 +1,6 @@
<?php
use Codeception\Stub;
/**
* Inherited Methods
@ -23,4 +24,52 @@ class UnitTester extends \Codeception\Actor
/**
* Define custom actions here
*/
/**
* execute any function of a class (also private/protected) and return its return
*
* @param string|object $className name of the class (FQCN) or an instance of it
* @param string $functionName name of the function which will be executed
* @param array $methodParams params the function will be executed with
* @param array $constructParams params which will be called in constructor. Will be ignored if $className
* is already an instance of an object.
* @param array $mocks mock/stub overrides of methods and properties. Will be ignored if $className is
* already an instance of an object.
*
* @return mixed
* @throws \Exception
*/
public function executeFunction(
$className,
string $functionName,
array $methodParams = [],
array $constructParams = [],
array $mocks = []
) {
$I = $this;
$I->comment('I execute function "'
. $functionName
. '" of class "'
. (is_object($className) ? get_class($className) : $className)
. '" with '
. count($methodParams)
. ' method-params, '
. count($constructParams)
. ' constuctor-params and '
. count($mocks)
. ' mocked class-methods/params'
);
$class = new \ReflectionClass($className);
$method = $class->getMethod($functionName);
$method->setAccessible(true);
if (is_object($className)) {
$reflectedClass = $className;
} elseif (empty($constructParams)) {
$reflectedClass = Stub::make($className, $mocks);
} else {
$reflectedClass = Stub::construct($className, $constructParams, $mocks);
}
return $method->invokeArgs($reflectedClass, $methodParams);
}
}

View File

@ -7,6 +7,7 @@ use Faker\Factory;
use Webkul\Attribute\Models\Attribute;
use Webkul\Attribute\Models\AttributeFamily;
use Webkul\Attribute\Models\AttributeOption;
use Webkul\Core\Helpers\Laravel5Helper;
use Webkul\Core\Models\Locale;
use Webkul\Product\Models\Product;
use Webkul\Product\Models\ProductAttributeValue;
@ -56,7 +57,7 @@ class ProductCest
public function testIndex(FunctionalTester $I): void
{
$product = $I->haveProduct([], ['simple']);
$product = $I->haveProduct(Laravel5Helper::SIMPLE_PRODUCT, [], ['simple']);
$I->loginAsAdmin();
$I->amOnAdminRoute('admin.dashboard.index');

View File

@ -4,6 +4,7 @@ namespace Tests\Functional\Admin\Customer;
use FunctionalTester;
use Webkul\Core\Helpers\Laravel5Helper;
use Webkul\Product\Models\ProductReview;
@ -11,7 +12,7 @@ class ReviewCest
{
public function testIndex(FunctionalTester $I): void
{
$product = $I->haveProduct([], ['simple']);
$product = $I->haveProduct(Laravel5Helper::SIMPLE_PRODUCT, [], ['simple']);
$review = $I->have(ProductReview::class, ['product_id' => $product->id]);
$I->loginAsAdmin();

View File

@ -0,0 +1,55 @@
<?php
namespace Tests\Functional\Checkout\Cart;
use FunctionalTester;
use Webkul\Core\Helpers\Laravel5Helper;
use Cart;
class CartCest
{
public $cart;
public $productWithQuantityBox;
public $productWithoutQuantityBox;
public function _before(FunctionalTester $I)
{
$productConfig = [
'productAttributes' => [],
'productInventory' => [
'qty' => 10,
],
'attributeValues' => [
'status' => 1,
],
];
$this->productWithQuantityBox = $I->haveProduct(Laravel5Helper::SIMPLE_PRODUCT, $productConfig);
$this->productWithoutQuantityBox = $I->haveProduct(Laravel5Helper::DOWNLOADABLE_PRODUCT, $productConfig);
}
public function checkCartWithQuantityBox(FunctionalTester $I)
{
Cart::addProduct($this->productWithQuantityBox->id, [
'_token' => session('_token'),
'product_id' => $this->productWithQuantityBox->id,
'quantity' => 1,
]);
$I->amOnPage('/checkout/cart');
$I->seeElement('#update_cart_button');
}
public function checkCartWithoutQuantityBox(FunctionalTester $I)
{
Cart::addProduct($this->productWithoutQuantityBox->id, [
'_token' => session('_token'),
'product_id' => $this->productWithoutQuantityBox->id,
'links' => $this->productWithoutQuantityBox->downloadable_links->pluck('id')->all(),
'quantity' => 1,
]);
$I->amOnPage('/checkout/cart');
$I->dontSeeElement('#update_cart_button');
}
}

View File

@ -6,6 +6,7 @@ use Codeception\Example;
use FunctionalTester;
use Faker\Factory;
use Cart;
use Webkul\Core\Helpers\Laravel5Helper;
class GuestCheckoutCest
{
@ -34,10 +35,10 @@ class GuestCheckoutCest
],
];
$this->productNoGuestCheckout = $I->haveProduct($pConfigDefault, ['simple']);
$this->productNoGuestCheckout = $I->haveProduct(Laravel5Helper::SIMPLE_PRODUCT, $pConfigDefault, ['simple']);
$this->productNoGuestCheckout->refresh();
$this->productGuestCheckout = $I->haveProduct($pConfigGuestCheckout, ['simple']);
$this->productGuestCheckout = $I->haveProduct(Laravel5Helper::SIMPLE_PRODUCT, $pConfigGuestCheckout, ['simple']);
$this->productGuestCheckout->refresh();
}
@ -61,7 +62,7 @@ class GuestCheckoutCest
$I->see($product->name, '//div[@class="product-information"]/div[@class="product-name"]');
$I->click(__('shop::app.products.add-to-cart'),
'//form[input[@name="product_id"][@value="' . $product->id . '"]]/button');
$I->seeInSource(__('shop::app.checkout.cart.item.success'));
$I->amOnRoute('shop.checkout.cart.index');
$I->see('Shopping Cart', '//div[@class="title"]');
$I->makeHtmlSnapshot('guestCheckout_' . $example['globalConfig'] . '_' . $product->getAttribute('guest_checkout'));

View File

@ -8,7 +8,7 @@ modules:
- Asserts
- Filesystem
- \Helper\Unit
- Laravel5:
- Webkul\Core\Helpers\Laravel5Helper:
environment_file: .env.testing
run_database_migrations: true
run_database_seeder: true

View File

@ -0,0 +1,36 @@
<?php
namespace Tests\Unit\Checkout\Cart\Controllers;
use UnitTester;
use Webkul\Checkout\Models\Cart;
use Webkul\Shop\Http\Controllers\CartController;
class CartControllerCest
{
public function _before(UnitTester $I)
{
}
public function testOnWarningAddingToCart(UnitTester $I)
{
$scenarios = [
[
'result' => ['key' => 'value', 'warning' => 'Hello World. Something went wrong.'],
'expected' => true,
],
[
'result' => ['key' => 'value'],
'expected' => false,
],
[
'result' => new Cart(),
'expected' => false,
],
];
foreach ($scenarios as $scenario) {
$I->assertEquals($scenario['expected'], $I->executeFunction(CartController::class, 'onWarningAddingToCart', [$scenario['result']]));
}
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace Tests\Unit\Checkout\Cart\Models;
use UnitTester;
use Faker\Factory;
use Webkul\Core\Helpers\Laravel5Helper;
use Cart;
class CartModelCest
{
public $cart;
public $faker;
public $sessionToken;
public $productWithQuantityBox;
public $productWithoutQuantityBox;
public function _before(UnitTester $I)
{
$this->faker = Factory::create();
$this->sessionToken = $this->faker->uuid;
session(['_token' => $this->sessionToken]);
$productConfig = [
'productAttributes' => [],
'productInventory' => [
'qty' => 10,
],
'attributeValues' => [
'status' => 1,
],
];
$this->productWithQuantityBox = $I->haveProduct(Laravel5Helper::SIMPLE_PRODUCT, $productConfig);
$this->productWithoutQuantityBox = $I->haveProduct(Laravel5Helper::DOWNLOADABLE_PRODUCT, $productConfig);
}
public function testHasProductsWithQuantityBox(UnitTester $I)
{
$I->wantTo('check function with cart, that contains a product with QuantityBox() == false');
$this->cart = Cart::addProduct($this->productWithoutQuantityBox->id, [
'_token' => session('_token'),
'product_id' => $this->productWithoutQuantityBox->id,
'links' => $this->productWithoutQuantityBox->downloadable_links->pluck('id')->all(),
'quantity' => 1,
]);
$cartItemIdOfProductWithoutQuantityBox = $this->cart->items[0]->id;
$I->assertFalse(Cart::getCart()->hasProductsWithQuantityBox());
$I->wantTo('check function with cart, that is mixed');
Cart::addProduct($this->productWithQuantityBox->id, [
'_token' => session('_token'),
'product_id' => $this->productWithQuantityBox->id,
'quantity' => 1,
]);
$I->assertTrue(Cart::getCart()->hasProductsWithQuantityBox());
$I->wantTo('check function with cart, that contains a product with QuantityBox() == true');
Cart::removeItem($cartItemIdOfProductWithoutQuantityBox);
$I->assertTrue(Cart::getCart()->hasProductsWithQuantityBox());
}
}