From c4695f6fe0b67ac795a366ff89236e054ad745b4 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Wed, 18 Oct 2023 09:59:43 +0200 Subject: [PATCH 01/28] Implement tax display values from Magento settings (#303) --- resources/js/components/Product/AddToCart.vue | 23 ---- resources/js/components/Product/Price.vue | 130 ++++++++++++++++++ resources/js/mixins.js | 10 ++ resources/js/vue-components.js | 3 + resources/views/cart/overview.blade.php | 8 +- resources/views/components/price.blade.php | 30 ++++ .../listing/partials/item/addtocart.blade.php | 5 +- .../product/partials/addtocart.blade.php | 21 +-- .../views/product/partials/grouped.blade.php | 5 +- src/Facades/Rapidez.php | 3 + src/Http/Controllers/ProductController.php | 2 +- src/Http/ViewComposers/ConfigComposer.php | 32 +++++ src/Models/Product.php | 8 +- .../Product/WithProductAttributesScope.php | 1 + src/Rapidez.php | 34 +++++ 15 files changed, 270 insertions(+), 45 deletions(-) create mode 100644 resources/js/components/Product/Price.vue create mode 100644 resources/views/components/price.blade.php diff --git a/resources/js/components/Product/AddToCart.vue b/resources/js/components/Product/AddToCart.vue index 2ec11c0de..bdf50e7d4 100644 --- a/resources/js/components/Product/AddToCart.vue +++ b/resources/js/components/Product/AddToCart.vue @@ -147,29 +147,6 @@ export default { reader.onload = () => Vue.set(this.customOptions, optionId, 'FILE;' + file.name + ';' + reader.result) reader.readAsDataURL(file) }, - - priceAddition: function (basePrice) { - let addition = 0 - - Object.entries(this.customOptions).forEach(([key, val]) => { - if (!val) { - return - } - - let option = this.product.options.find((option) => option.option_id == key) - let optionPrice = ['drop_down', 'radio'].includes(option.type) - ? option.values.find((value) => value.option_type_id == val).price - : option.price - - if (optionPrice.price_type == 'fixed') { - addition += parseFloat(optionPrice.price) - } else { - addition += (parseFloat(basePrice) * parseFloat(optionPrice.price)) / 100 - } - }) - - return addition - }, }, computed: { diff --git a/resources/js/components/Product/Price.vue b/resources/js/components/Product/Price.vue new file mode 100644 index 000000000..3879b7426 --- /dev/null +++ b/resources/js/components/Product/Price.vue @@ -0,0 +1,130 @@ + diff --git a/resources/js/mixins.js b/resources/js/mixins.js index df6e2f78d..7a6511db7 100644 --- a/resources/js/mixins.js +++ b/resources/js/mixins.js @@ -13,5 +13,15 @@ Vue.mixin({ return await magento[method]('guest-carts/' + localStorage.mask + '/' + endpoint, data) } }, + + includeTaxAt(location) { + return location === true || location === false + ? location + : (window.config.tax.display[location] ?? 0) >= 1 + }, + + decideTax(including, excluding, location) { + return this.includeTaxAt(location) ? including : excluding + }, }, }) diff --git a/resources/js/vue-components.js b/resources/js/vue-components.js index 8507ab70a..0d76af3e4 100644 --- a/resources/js/vue-components.js +++ b/resources/js/vue-components.js @@ -24,6 +24,9 @@ Vue.component('notifications', notifications) import images from './components/Product/Images.vue' Vue.component('images', images) +import price from './components/Product/Price.vue' +Vue.component('price', price) + Vue.component('autocomplete', () => import('./components/Search/Autocomplete.vue')) Vue.component('login', () => import('./components/Checkout/Login.vue')) Vue.component('listing', () => import('./components/Listing/Listing.vue')) diff --git a/resources/views/cart/overview.blade.php b/resources/views/cart/overview.blade.php index 5e6bcbc3b..ef9658cee 100644 --- a/resources/views/cart/overview.blade.php +++ b/resources/views/cart/overview.blade.php @@ -30,14 +30,14 @@
- @{{ item.price | price }} + @{{ $root.decideTax(item.price, item.price_excl_tax, 'cart_price') | price }}
- @{{ item.total | price }} + @{{ $root.decideTax(item.total, item.total_excl_tax, 'cart_price') | price }}
@@ -48,11 +48,11 @@
@lang('Subtotal')
-
@{{ cart.subtotal | price }}
+
@{{ $root.decideTax(cart.subtotal, cart.subtotal_excl_tax, 'cart_subtotal') | price }}
@lang('Tax')
@{{ cart.tax | price }}
@lang('Shipping')
@{{ cart.shipping_description }}
-
@{{ cart.shipping_amount | price }}
+
@{{ $root.decideTax(cart.shipping_amount, cart.shipping_amount_excl_tax, 'cart_shipping') | price }}
@lang('Discount'): @{{ cart.discount_name }}
@lang('Discount')
@{{ cart.discount_amount | price }}
diff --git a/resources/views/components/price.blade.php b/resources/views/components/price.blade.php new file mode 100644 index 000000000..6386e725c --- /dev/null +++ b/resources/views/components/price.blade.php @@ -0,0 +1,30 @@ +@props(['product' => 'product', 'type' => 'catalog', 'placeholder' => '']) + + +
class('mt-1 flex items-center gap-2 text-14 font-semibold text-primary') }} + slot-scope="{ price, specialPrice, isDiscounted, range }" + > + + +
+
diff --git a/resources/views/listing/partials/item/addtocart.blade.php b/resources/views/listing/partials/item/addtocart.blade.php index f4db6d543..02c2ab506 100644 --- a/resources/views/listing/partials/item/addtocart.blade.php +++ b/resources/views/listing/partials/item/addtocart.blade.php @@ -1,9 +1,6 @@
-
-
@{{ (simpleProduct.special_price || simpleProduct.price) | price }}
-
@{{ simpleProduct.price | price }}
-
+

@lang('Sorry! This product is currently out of stock.')

diff --git a/resources/views/product/partials/addtocart.blade.php b/resources/views/product/partials/addtocart.blade.php index 53666736c..15b4a5409 100644 --- a/resources/views/product/partials/addtocart.blade.php +++ b/resources/views/product/partials/addtocart.blade.php @@ -1,5 +1,5 @@ -
+

{{ $product->name }}

@if (!$product->in_stock)

@lang('Sorry! This product is currently out of stock.')

@@ -19,14 +19,19 @@ @include('rapidez::product.partials.options')
-
-
- {{ price($product->special_price ?: $product->price) }} -
-
- {{ $product->special_price ? price($product->price) : '' }} + +
+
+ {{ price($product->special_price ?: $product->price) }} +
+
+ {{ $product->special_price ? price($product->price) : '' }} +
-
+
@{{ simpleProduct.name }} -
-
@{{ (simpleProduct.special_price || simpleProduct.price) | price }}
-
@{{ simpleProduct.price | price }}
-
+

diff --git a/src/Facades/Rapidez.php b/src/Facades/Rapidez.php index 1576a2dff..1f0fa3a55 100644 --- a/src/Facades/Rapidez.php +++ b/src/Facades/Rapidez.php @@ -14,6 +14,9 @@ * @method static array getStores($storeId = null) * @method static array getStore($storeId) * @method static void setStore($store) + * @method static array getTaxTable() + * @method static array getTaxGroups() + * @method static array getTaxRates($tax_class_id) * * @see \Rapidez\Core\Rapidez */ diff --git a/src/Http/Controllers/ProductController.php b/src/Http/Controllers/ProductController.php index 461dc4c42..fbdd2b3d6 100644 --- a/src/Http/Controllers/ProductController.php +++ b/src/Http/Controllers/ProductController.php @@ -15,7 +15,7 @@ public function show(int $productId) ->with('options') ->findOrFail($productId); - $attributes = ['entity_id', 'name', 'sku', 'super_attributes', 'children', 'grouped', 'options', 'price', 'special_price', 'images', 'url', 'min_sale_qty']; + $attributes = ['entity_id', 'name', 'sku', 'super_attributes', 'children', 'grouped', 'options', 'price', 'special_price', 'tax_class_id', 'tax_rates', 'images', 'url', 'min_sale_qty']; $attributes = Eventy::filter('productpage.frontend.attributes', $attributes); foreach ($product->super_attributes ?: [] as $superAttribute) { diff --git a/src/Http/ViewComposers/ConfigComposer.php b/src/Http/ViewComposers/ConfigComposer.php index 331d5223e..98b6524cc 100644 --- a/src/Http/ViewComposers/ConfigComposer.php +++ b/src/Http/ViewComposers/ConfigComposer.php @@ -42,6 +42,7 @@ public function compose(View $view) Config::set('frontend.customer_fields_show', $this->getCustomerFields()); Config::set('frontend.grid_per_page', Rapidez::config('catalog/frontend/grid_per_page', 12)); Config::set('frontend.grid_per_page_values', explode(',', Rapidez::config('catalog/frontend/grid_per_page_values', '12,24,36'))); + Config::set('frontend.tax', $this->getTaxConfiguration()); } public function getCustomerFields() @@ -60,4 +61,35 @@ public function getCustomerFields() 'company' => Rapidez::config('customer/address/company_show', 'opt'), ]; } + + public function getTaxConfiguration() + { + return [ + 'values' => (object) Rapidez::getTaxTable(), + 'groups' => (object) Rapidez::getTaxGroups(), + 'calculation' => [ + 'price_includes_tax' => boolval(Rapidez::config('tax/calculation/price_includes_tax', 0)), + 'base_subtotal_should_include_tax' => boolval(Rapidez::config('tax/calculation/base_subtotal_should_include_tax', 1)), + 'algorithm' => Rapidez::config('tax/calculation/algorithm', 'TOTAL_BASE_CALCULATION'), + 'apply_after_discount' => boolval(Rapidez::config('tax/calculation/apply_after_discount', 1)), + 'apply_tax_on' => Rapidez::config('tax/calculation/apply_tax_on', 0), + 'based_on' => Rapidez::config('tax/calculation/based_on', 'shipping'), + 'cross_border_trade_enabled' => boolval(Rapidez::config('tax/calculation/cross_border_trade_enabled', 0)), + 'discount_tax' => boolval(Rapidez::config('tax/calculation/discount_tax', 0)), + 'shipping_includes_tax' => boolval(Rapidez::config('tax/calculation/shipping_includes_tax', 0)), + ], + 'display' => [ + 'catalog' => Rapidez::config('tax/display/type', 1), + 'shipping' => Rapidez::config('tax/display/shipping', 1), + 'cart_price' => Rapidez::config('tax/cart_display/price', 1), + 'cart_shipping' => Rapidez::config('tax/cart_display/shipping', 1), + 'cart_subtotal' => Rapidez::config('tax/cart_display/subtotal', 1), + ], + 'defaults' => [ + 'country' => Rapidez::config('tax/defaults/country', 'US'), + 'postcode' => Rapidez::config('tax/defaults/postcode', null), + 'region' => Rapidez::config('tax/defaults/region', 0), + ], + ]; + } } diff --git a/src/Models/Product.php b/src/Models/Product.php index a4fc372bd..9a110e812 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -10,6 +10,7 @@ use Rapidez\Core\Casts\CommaSeparatedToArray; use Rapidez\Core\Casts\CommaSeparatedToIntegerArray; use Rapidez\Core\Casts\DecodeHtmlEntities; +use Rapidez\Core\Facades\Rapidez; use Rapidez\Core\Models\Scopes\Product\WithProductAttributesScope; use Rapidez\Core\Models\Scopes\Product\WithProductCategoryInfoScope; use Rapidez\Core\Models\Scopes\Product\WithProductChildrenScope; @@ -34,7 +35,7 @@ class Product extends Model protected $primaryKey = 'entity_id'; - protected $appends = ['url']; + protected $appends = ['url', 'tax_rates']; protected static function booting(): void { @@ -165,6 +166,11 @@ public function getSpecialPriceAttribute($specialPrice) })->min->special_price; } + public function getTaxRatesAttribute(): object + { + return (object) Rapidez::getTaxRates($this->tax_class_id); + } + public function getUrlAttribute(): string { $configModel = config('rapidez.models.config'); diff --git a/src/Models/Scopes/Product/WithProductAttributesScope.php b/src/Models/Scopes/Product/WithProductAttributesScope.php index 8187d79a6..5cb2bd2b5 100644 --- a/src/Models/Scopes/Product/WithProductAttributesScope.php +++ b/src/Models/Scopes/Product/WithProductAttributesScope.php @@ -27,6 +27,7 @@ public function apply(Builder $builder, Model $model) $model->qualifyColumn('sku'), $model->qualifyColumn('visibility'), $model->qualifyColumn('type_id'), + $model->qualifyColumn('tax_class_id'), $model->getQualifiedCreatedAtColumn(), ]); diff --git a/src/Rapidez.php b/src/Rapidez.php index e3de123d8..c5c0b3192 100644 --- a/src/Rapidez.php +++ b/src/Rapidez.php @@ -5,6 +5,8 @@ use Illuminate\Routing\RouteAction; use Illuminate\Support\Arr; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\DB; use Rapidez\Core\Models\Store; class Rapidez @@ -100,6 +102,38 @@ public function setStore(Store|array|callable|int|string $store): void config()->set('rapidez.root_category_id', $store['root_category_id']); } + public function getTaxTable(): array + { + return Cache::rememberForever('tax_' . config('rapidez.store'), function () { + $table = []; + $rates = DB::table('tax_class') + ->join('tax_calculation', 'tax_calculation.product_tax_class_id', '=', 'tax_class.class_id') + ->join('tax_calculation_rate', 'tax_calculation_rate.tax_calculation_rate_id', '=', 'tax_calculation.tax_calculation_rate_id') + ->where('tax_country_id', Rapidez::config('tax/defaults/country', 'US')) + ->where('tax_region_id', Rapidez::config('tax/defaults/region', 0)) + ->whereIn('tax_postcode', [null, '*', Rapidez::config('tax/default/postcode', null)]) + ->get(); + + foreach ($rates as $rate) { + $table[$rate->class_id][$rate->customer_tax_class_id] = $rate->rate; + } + + return $table; + }); + } + + public function getTaxGroups(): array + { + return Cache::rememberForever('tax_groups', function () { + return DB::table('customer_group')->pluck('tax_class_id', 'customer_group_id')->toArray(); + }); + } + + public function getTaxRates($tax_class_id): array + { + return $this->getTaxTable()[$tax_class_id] ?? []; + } + public function withStore(Store|array|callable|int|string $store, callable $callback, ...$args) { $initialStore = config('rapidez.store'); From ca4152ebd5ebfeca4b42748d4341b9d9c0af4309 Mon Sep 17 00:00:00 2001 From: royduin Date: Wed, 18 Oct 2023 08:00:21 +0000 Subject: [PATCH 02/28] Apply fixes from Prettier --- resources/js/components/Product/Price.vue | 30 ++++++++++------------- resources/js/mixins.js | 4 +-- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/resources/js/components/Product/Price.vue b/resources/js/components/Product/Price.vue index 3879b7426..970cd70ee 100644 --- a/resources/js/components/Product/Price.vue +++ b/resources/js/components/Product/Price.vue @@ -39,13 +39,11 @@ export default { calculatePrice(product, location, options = {}) { let special_price = options.special_price ?? false - let displayTax = this.$root.includeTaxAt(location); + let displayTax = this.$root.includeTaxAt(location) - let price = special_price - ? product.special_price ?? product.price ?? 0 - : product.price ?? 0 + let price = special_price ? product.special_price ?? product.price ?? 0 : product.price ?? 0 - if(options.product_options) { + if (options.product_options) { price += this.calculateOptionsValue(price, product, options.product_options) } @@ -55,9 +53,7 @@ export default { return price } - return displayTax - ? price * taxMultiplier - : price / taxMultiplier + return displayTax ? price * taxMultiplier : price / taxMultiplier }, calculateOptionsValue(basePrice, product, customOptions) { @@ -81,27 +77,27 @@ export default { }) return addition - } + }, }, computed: { specialPrice() { - if(!this.mounted) { - return; + if (!this.mounted) { + return } return this.calculatePrice(this.product, this.type, Object.assign(this.options, { special_price: true })) }, price() { - if(!this.mounted) { - return; + if (!this.mounted) { + return } return this.calculatePrice(this.product, this.type, this.options) }, isDiscounted() { - if(!this.mounted) { - return false; + if (!this.mounted) { + return false } return this.specialPrice != this.price @@ -112,8 +108,8 @@ export default { return null } - let prices = Object.values(this.product.children).map( - (child) => this.calculatePrice(child, this.type, Object.assign(this.options, { special_price: true })) + let prices = Object.values(this.product.children).map((child) => + this.calculatePrice(child, this.type, Object.assign(this.options, { special_price: true })), ) return { diff --git a/resources/js/mixins.js b/resources/js/mixins.js index 7a6511db7..3f8ab2c67 100644 --- a/resources/js/mixins.js +++ b/resources/js/mixins.js @@ -15,9 +15,7 @@ Vue.mixin({ }, includeTaxAt(location) { - return location === true || location === false - ? location - : (window.config.tax.display[location] ?? 0) >= 1 + return location === true || location === false ? location : (window.config.tax.display[location] ?? 0) >= 1 }, decideTax(including, excluding, location) { From e7a6d115ff7ed0eb7142ce82785be58ce493d18d Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Wed, 18 Oct 2023 10:32:15 +0200 Subject: [PATCH 03/28] Fix merge & implement latent feedback --- resources/js/components/Product/Price.vue | 32 +++++-------------- .../product/partials/addtocart.blade.php | 4 +-- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/resources/js/components/Product/Price.vue b/resources/js/components/Product/Price.vue index 970cd70ee..b491ce2a0 100644 --- a/resources/js/components/Product/Price.vue +++ b/resources/js/components/Product/Price.vue @@ -5,7 +5,7 @@ export default { type: Object, default: () => config.product || {}, }, - type: { + location: { type: String, default: 'catalog', }, @@ -14,16 +14,9 @@ export default { default: {}, }, }, - render() { - return this.$scopedSlots.default(Object.assign(this, { self: this })) - }, - data: () => ({ - mounted: false, - }), - - mounted() { - this.mounted = true + render() { + return this.$scopedSlots.default(this) }, methods: { @@ -31,6 +24,7 @@ export default { let taxClass = product.tax_class_id ?? product let taxValues = product.tax_values ?? window.config.tax.values[taxClass] ?? {} + // TODO: Figure out where to get the tax rate calculation from let groupId = this.$root.user?.group_id ?? 0 // 0 is always the NOT_LOGGED_IN group let customerTaxClass = window.config.tax.groups[groupId] ?? 0 @@ -82,24 +76,14 @@ export default { computed: { specialPrice() { - if (!this.mounted) { - return - } - return this.calculatePrice(this.product, this.type, Object.assign(this.options, { special_price: true })) + return this.calculatePrice(this.product, this.location, Object.assign(this.options, { special_price: true })) }, price() { - if (!this.mounted) { - return - } - return this.calculatePrice(this.product, this.type, this.options) + return this.calculatePrice(this.product, this.location, this.options) }, isDiscounted() { - if (!this.mounted) { - return false - } - return this.specialPrice != this.price }, @@ -109,7 +93,7 @@ export default { } let prices = Object.values(this.product.children).map((child) => - this.calculatePrice(child, this.type, Object.assign(this.options, { special_price: true })), + this.calculatePrice(child, this.location, Object.assign(this.options, { special_price: true })), ) return { @@ -119,7 +103,7 @@ export default { }, includesTax() { - return this.$root.includeTaxAt(this.type) + return this.$root.includeTaxAt(this.location) }, }, } diff --git a/resources/views/product/partials/addtocart.blade.php b/resources/views/product/partials/addtocart.blade.php index 15b4a5409..689a6678d 100644 --- a/resources/views/product/partials/addtocart.blade.php +++ b/resources/views/product/partials/addtocart.blade.php @@ -18,7 +18,7 @@

@include('rapidez::product.partials.options') -
+
{{ price($product->special_price ?: $product->price) }}
-
+
{{ $product->special_price ? price($product->price) : '' }}
From c8773533fe32f72a4844bdaaf19ae05f2113f74f Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Wed, 18 Oct 2023 11:31:46 +0200 Subject: [PATCH 04/28] Use price component everywhere, remove unnecessary calculation --- composer.json | 1 + resources/js/components/Product/AddToCart.vue | 18 -------- resources/views/components/price.blade.php | 45 +++++++++---------- .../product/partials/addtocart.blade.php | 21 ++++----- 4 files changed, 32 insertions(+), 53 deletions(-) diff --git a/composer.json b/composer.json index ca2067647..3971e259b 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "php": "^8.0.2|^8.1", "blade-ui-kit/blade-heroicons": "^2.0", "cviebrock/laravel-elasticsearch": "^9.0|^10.0", + "gehrisandro/tailwind-merge-laravel": "^0.2.1", "illuminate/database": "^9.0|^10.0", "illuminate/events": "^9.0|^10.0", "illuminate/queue": "^9.0|^10.0", diff --git a/resources/js/components/Product/AddToCart.vue b/resources/js/components/Product/AddToCart.vue index bdf50e7d4..7389a5272 100644 --- a/resources/js/components/Product/AddToCart.vue +++ b/resources/js/components/Product/AddToCart.vue @@ -38,14 +38,10 @@ export default { adding: false, added: false, - - price: 0, - specialPrice: 0, }), mounted() { this.qty = this.defaultQty - this.calculatePrices() }, render() { @@ -117,11 +113,6 @@ export default { }) }, - calculatePrices: function () { - this.price = parseFloat(this.simpleProduct.price) + this.priceAddition(this.simpleProduct.price) - this.specialPrice = parseFloat(this.simpleProduct.special_price) + this.priceAddition(this.simpleProduct.special_price) - }, - getOptions: function (superAttributeCode) { if (this.$root.swatches.hasOwnProperty(superAttributeCode)) { let swatchOptions = this.$root.swatches[superAttributeCode].options @@ -281,14 +272,5 @@ export default { return disabledOptions }, }, - - watch: { - customOptions: { - handler() { - this.calculatePrices() - }, - deep: true, - }, - }, } diff --git a/resources/views/components/price.blade.php b/resources/views/components/price.blade.php index 6386e725c..48c5696f4 100644 --- a/resources/views/components/price.blade.php +++ b/resources/views/components/price.blade.php @@ -1,30 +1,29 @@ -@props(['product' => 'product', 'type' => 'catalog', 'placeholder' => '']) +@props(['product' => 'product', 'type' => 'catalog']) +@slots(['special'])
class('mt-1 flex items-center gap-2 text-14 font-semibold text-primary') }} + {{ $attributes->twMerge('mt-1 flex items-center gap-2') }} slot-scope="{ price, specialPrice, isDiscounted, range }" > - - + + {{ $slot }} + + attributes->twMerge('text-13 font-normal line-through') }} + v-if="isDiscounted && !range" + v-text="$options.filters.price(price)" + > + {{ $special }} + + + + + +
diff --git a/resources/views/product/partials/addtocart.blade.php b/resources/views/product/partials/addtocart.blade.php index 689a6678d..fb551746a 100644 --- a/resources/views/product/partials/addtocart.blade.php +++ b/resources/views/product/partials/addtocart.blade.php @@ -19,19 +19,16 @@ @include('rapidez::product.partials.options')
- -
-
- {{ price($product->special_price ?: $product->price) }} -
-
- {{ $product->special_price ? price($product->price) : '' }} -
-
-
+ {{ price($product->special_price ?: $product->price) }} + + {{ $product->special_price ? price($product->price) : '' }} + + Date: Wed, 18 Oct 2023 11:45:29 +0200 Subject: [PATCH 05/28] simpleProduct as default --- resources/views/components/price.blade.php | 2 +- resources/views/listing/partials/item/addtocart.blade.php | 2 +- resources/views/product/partials/addtocart.blade.php | 1 - resources/views/product/partials/grouped.blade.php | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/resources/views/components/price.blade.php b/resources/views/components/price.blade.php index 48c5696f4..bc2df4d61 100644 --- a/resources/views/components/price.blade.php +++ b/resources/views/components/price.blade.php @@ -1,4 +1,4 @@ -@props(['product' => 'product', 'type' => 'catalog']) +@props(['product' => 'simpleProduct', 'type' => 'catalog']) @slots(['special']) diff --git a/resources/views/listing/partials/item/addtocart.blade.php b/resources/views/listing/partials/item/addtocart.blade.php index 02c2ab506..3454671a4 100644 --- a/resources/views/listing/partials/item/addtocart.blade.php +++ b/resources/views/listing/partials/item/addtocart.blade.php @@ -1,6 +1,6 @@
- +

@lang('Sorry! This product is currently out of stock.')

diff --git a/resources/views/product/partials/addtocart.blade.php b/resources/views/product/partials/addtocart.blade.php index fb551746a..0e1a050e9 100644 --- a/resources/views/product/partials/addtocart.blade.php +++ b/resources/views/product/partials/addtocart.blade.php @@ -21,7 +21,6 @@
{{ price($product->special_price ?: $product->price) }} diff --git a/resources/views/product/partials/grouped.blade.php b/resources/views/product/partials/grouped.blade.php index fd817558e..1d4a966f2 100644 --- a/resources/views/product/partials/grouped.blade.php +++ b/resources/views/product/partials/grouped.blade.php @@ -4,7 +4,7 @@
@{{ simpleProduct.name }} - +

From 0573295b64eaaf56ec9962da943a3b3f2f84ebb5 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Wed, 18 Oct 2023 11:52:56 +0200 Subject: [PATCH 06/28] v-cloak on addtocart icons --- resources/views/product/partials/addtocart.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/product/partials/addtocart.blade.php b/resources/views/product/partials/addtocart.blade.php index 0e1a050e9..f12bc020b 100644 --- a/resources/views/product/partials/addtocart.blade.php +++ b/resources/views/product/partials/addtocart.blade.php @@ -42,8 +42,8 @@ class="w-auto" - - + + @lang('Add to cart') @lang('Adding')... @lang('Added') From 2f49d6085cea7412f353487b4abb6e0bde6f1888 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Wed, 18 Oct 2023 14:13:18 +0200 Subject: [PATCH 07/28] Add tierprices --- config/rapidez/models.php | 1 + resources/js/components/Product/AddToCart.vue | 68 +++++++++++++++++++ resources/js/components/Product/Price.vue | 32 +++++---- resources/views/components/price.blade.php | 4 +- .../product/partials/addtocart.blade.php | 16 ++++- src/Http/Controllers/ProductController.php | 20 +++++- src/Models/Product.php | 24 ++++--- src/Models/ProductTierPrice.php | 17 +++++ 8 files changed, 155 insertions(+), 27 deletions(-) create mode 100644 src/Models/ProductTierPrice.php diff --git a/config/rapidez/models.php b/config/rapidez/models.php index aed1d04ac..f81643469 100644 --- a/config/rapidez/models.php +++ b/config/rapidez/models.php @@ -20,6 +20,7 @@ 'product_option_type_title' => Rapidez\Core\Models\ProductOptionTypeTitle::class, 'product_option_type_price' => Rapidez\Core\Models\ProductOptionTypePrice::class, 'product_option_type_value' => Rapidez\Core\Models\ProductOptionTypeValue::class, + 'product_tier_price' => Rapidez\Core\Models\ProductTierPrice::class, 'quote' => Rapidez\Core\Models\Quote::class, 'quote_item' => Rapidez\Core\Models\QuoteItem::class, 'quote_item_option' => Rapidez\Core\Models\QuoteItemOption::class, diff --git a/resources/js/components/Product/AddToCart.vue b/resources/js/components/Product/AddToCart.vue index 7389a5272..d7e6bc63e 100644 --- a/resources/js/components/Product/AddToCart.vue +++ b/resources/js/components/Product/AddToCart.vue @@ -35,6 +35,7 @@ export default { options: {}, customOptions: {}, error: null, + tierPrice: null, adding: false, added: false, @@ -138,6 +139,49 @@ export default { reader.onload = () => Vue.set(this.customOptions, optionId, 'FILE;' + file.name + ';' + reader.result) reader.readAsDataURL(file) }, + + getTierPrice(qty) { + qty = parseInt(qty) + let match = null + let matchQty = 0 + Object.entries(this.tierPrices).forEach(([id, tier]) => { + if (tier.qty > matchQty && tier.qty <= qty) { + match = id + matchQty = tier.qty + } + }) + + return match; + }, + + selectTier(id) { + this.tierPrice = id + let tierQty = this.tierPrices[this.tierPrice]?.qty ?? 1 + this.qty = parseInt(tierQty) + }, + + tierPriceDiscount(price, discount) { + if (!discount) { + return price + } + + // Calculate percentage value and return first! + // Magento2 will not clear the `value` column if you set a percentage, but it will clear the `percentage_value` column if you set a value. + if (discount.percentage_value > 0) { + return (price * (100 - discount.percentage_value)) / 100 + } + + if (discount.value > 0) { + return discount.value + } + return price + }, + }, + + watch: { + qty() { + this.tierPrice = this.getTierPrice(this.qty) + }, }, computed: { @@ -271,6 +315,30 @@ export default { return disabledOptions }, + + tierPrices: function () { + if (!this.simpleProduct.tierPrices || this.simpleProduct.tierPrices.length == 0) { + return {} + } + let availableOptions = this.simpleProduct.tierPrices.filter( + (option) => option.all_groups || option.customer_group_id == this.$root.user?.group_id, + ) + return Object.fromEntries( + availableOptions.map((option) => [ + option.value_id, + { + qty: option.qty, + value: option.value, + percentage_value: option.percentage_value, + price: this.tierPriceDiscount(this.simpleProduct.price, option), + }, + ]), + ) + }, + + currentTierPrice: function() { + return this.tierPrices[this.tierPrice] ?? null + }, }, } diff --git a/resources/js/components/Product/Price.vue b/resources/js/components/Product/Price.vue index b491ce2a0..6f86c0984 100644 --- a/resources/js/components/Product/Price.vue +++ b/resources/js/components/Product/Price.vue @@ -11,7 +11,7 @@ export default { }, options: { type: Object, - default: {}, + default: () => ({}), }, }, @@ -20,23 +20,16 @@ export default { }, methods: { - getTaxPercent(product) { - let taxClass = product.tax_class_id ?? product - let taxValues = product.tax_values ?? window.config.tax.values[taxClass] ?? {} - - // TODO: Figure out where to get the tax rate calculation from - let groupId = this.$root.user?.group_id ?? 0 // 0 is always the NOT_LOGGED_IN group - let customerTaxClass = window.config.tax.groups[groupId] ?? 0 - - return taxValues[customerTaxClass] ?? 0 - }, - calculatePrice(product, location, options = {}) { let special_price = options.special_price ?? false let displayTax = this.$root.includeTaxAt(location) let price = special_price ? product.special_price ?? product.price ?? 0 : product.price ?? 0 + if (options.tier_price) { + price = options.tier_price.price + } + if (options.product_options) { price += this.calculateOptionsValue(price, product, options.product_options) } @@ -50,6 +43,17 @@ export default { return displayTax ? price * taxMultiplier : price / taxMultiplier }, + getTaxPercent(product) { + let taxClass = product.tax_class_id ?? product + let taxValues = product.tax_values ?? window.config.tax.values[taxClass] ?? {} + + // TODO: Figure out where to get the tax rate calculation from + let groupId = this.$root.user?.group_id ?? 0 // 0 is always the NOT_LOGGED_IN group + let customerTaxClass = window.config.tax.groups[groupId] ?? 0 + + return taxValues[customerTaxClass] ?? 0 + }, + calculateOptionsValue(basePrice, product, customOptions) { let addition = 0 @@ -76,7 +80,7 @@ export default { computed: { specialPrice() { - return this.calculatePrice(this.product, this.location, Object.assign(this.options, { special_price: true })) + return this.calculatePrice(this.product, this.location, Object.assign({ special_price: true }, this.options)) }, price() { @@ -93,7 +97,7 @@ export default { } let prices = Object.values(this.product.children).map((child) => - this.calculatePrice(child, this.location, Object.assign(this.options, { special_price: true })), + this.calculatePrice(child, this.location, Object.assign({ special_price: true }, this.options)), ) return { diff --git a/resources/views/components/price.blade.php b/resources/views/components/price.blade.php index bc2df4d61..d245b5747 100644 --- a/resources/views/components/price.blade.php +++ b/resources/views/components/price.blade.php @@ -1,7 +1,7 @@ -@props(['product' => 'simpleProduct', 'type' => 'catalog']) +@props(['product' => 'simpleProduct', 'type' => 'catalog', 'options' => '{}']) @slots(['special']) - +

twMerge('mt-1 flex items-center gap-2') }} slot-scope="{ price, specialPrice, isDiscounted, range }" diff --git a/resources/views/product/partials/addtocart.blade.php b/resources/views/product/partials/addtocart.blade.php index f12bc020b..c41b66ed3 100644 --- a/resources/views/product/partials/addtocart.blade.php +++ b/resources/views/product/partials/addtocart.blade.php @@ -1,5 +1,5 @@ - +

{{ $product->name }}

@if (!$product->in_stock)

@lang('Sorry! This product is currently out of stock.')

@@ -18,10 +18,22 @@
@include('rapidez::product.partials.options') + +
    +
  • + @lang('Order :amount and pay :price per item', [ + 'amount' => '@{{ Math.round(tier.qty) }}', + 'price' => '@{{ tier.price | price }}', + ]) +
  • +
{{ price($product->special_price ?: $product->price) }} diff --git a/src/Http/Controllers/ProductController.php b/src/Http/Controllers/ProductController.php index fbdd2b3d6..8498e8ac5 100644 --- a/src/Http/Controllers/ProductController.php +++ b/src/Http/Controllers/ProductController.php @@ -13,9 +13,27 @@ public function show(int $productId) $product = $productModel::selectForProductPage() ->withEventyGlobalScopes('productpage.scopes') ->with('options') + ->with('tierPrices') ->findOrFail($productId); - $attributes = ['entity_id', 'name', 'sku', 'super_attributes', 'children', 'grouped', 'options', 'price', 'special_price', 'tax_class_id', 'tax_rates', 'images', 'url', 'min_sale_qty']; + $attributes = [ + 'entity_id', + 'name', + 'sku', + 'super_attributes', + 'children', + 'grouped', + 'options', + 'price', + 'special_price', + 'tax_class_id', + 'tax_rates', + 'tierPrices', + 'images', + 'url', + 'min_sale_qty' + ]; + $attributes = Eventy::filter('productpage.frontend.attributes', $attributes); foreach ($product->super_attributes ?: [] as $superAttribute) { diff --git a/src/Models/Product.php b/src/Models/Product.php index 9a110e812..4e44bf908 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -93,14 +93,6 @@ public function gallery(): BelongsToMany ); } - public function views(): HasMany - { - return $this->hasMany( - config('rapidez.models.product_view'), - 'product_id', - ); - } - public function options(): HasMany { return $this->hasMany( @@ -117,6 +109,22 @@ public function rewrites(): HasMany ->where('entity_type', 'product'); } + public function tierPrices(): HasMany + { + return $this->hasMany( + config('rapidez.models.product_tier_price'), + 'entity_id' + )->whereIn('website_id', [0, config('rapidez.website')]); + } + + public function views(): HasMany + { + return $this->hasMany( + config('rapidez.models.product_view'), + 'product_id', + ); + } + public function scopeByIds(Builder $query, array $productIds): Builder { return $query->whereIn($this->getQualifiedKeyName(), $productIds); diff --git a/src/Models/ProductTierPrice.php b/src/Models/ProductTierPrice.php new file mode 100644 index 000000000..02b40f33a --- /dev/null +++ b/src/Models/ProductTierPrice.php @@ -0,0 +1,17 @@ +belongsTo(config('rapidez.models.product'), 'entity_id'); + } +} From 62a26fa584844ca1e425383afcdd35021c257050 Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Wed, 18 Oct 2023 12:13:58 +0000 Subject: [PATCH 08/28] Apply fixes from Duster --- src/Http/Controllers/ProductController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Controllers/ProductController.php b/src/Http/Controllers/ProductController.php index 8498e8ac5..944dc311c 100644 --- a/src/Http/Controllers/ProductController.php +++ b/src/Http/Controllers/ProductController.php @@ -31,7 +31,7 @@ public function show(int $productId) 'tierPrices', 'images', 'url', - 'min_sale_qty' + 'min_sale_qty', ]; $attributes = Eventy::filter('productpage.frontend.attributes', $attributes); From 539a3f26a2fb5cbc62ea97d4ec326c0d5dd30a63 Mon Sep 17 00:00:00 2001 From: indy koning Date: Wed, 18 Oct 2023 17:23:30 +0200 Subject: [PATCH 09/28] Change customer token into cookie --- package.json | 2 + resources/js/axios.js | 5 +- .../components/Checkout/CheckoutSuccess.vue | 3 +- resources/js/components/GraphqlMutation.vue | 3 +- resources/js/components/Product/Price.vue | 20 +++---- resources/js/stores/useUser.js | 17 +++++- .../product/partials/addtocart.blade.php | 1 - .../views/product/partials/options.blade.php | 2 +- src/Auth/MagentoCustomerTokenGuard.php | 12 ++++ src/Models/Customer.php | 2 +- src/RapidezServiceProvider.php | 2 +- yarn.lock | 59 +++++++++++++++++++ 12 files changed, 106 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 2099c9a32..f0d587210 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@tailwindcss/typography": "^0.5.9", "@vitejs/plugin-vue2": "^2.2.0", "@vueuse/core": "^9.12.0", + "@vueuse/integrations": "^10.5.0", "autoprefixer": "^10.4.15", "axios": "^1.2.6", "cross-env": "^7.0.3", @@ -23,6 +24,7 @@ "rollup-plugin-visualizer": "^5.9.0", "tailwind-scrollbar-hide": "^1.1.7", "tailwindcss": "^3.3.3", + "universal-cookie": "^6.1.1", "vite": "^4.0.4", "vue": "^2.7", "vue-clickaway": "^2.2.2", diff --git a/resources/js/axios.js b/resources/js/axios.js index 5e36655a9..ff33caab9 100644 --- a/resources/js/axios.js +++ b/resources/js/axios.js @@ -1,6 +1,7 @@ import axios from 'axios' -window.axios = axios +import { token } from './stores/useUser' +window.axios = axios window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest' window.magento = axios.create() @@ -8,7 +9,7 @@ window.magento.defaults.baseURL = config.magento_url + '/rest/' + config.store_c window.magentoUser = axios.create() window.magentoUser.defaults.baseURL = config.magento_url + '/rest/' + config.store_code + '/V1/' -window.magentoUser.defaults.headers.common['Authorization'] = `Bearer ${localStorage.token}` +window.magentoUser.defaults.headers.common['Authorization'] = `Bearer ${token.value}` // It's not possible to set global interceptors like headers // or the base url can; so we set them for all instances. diff --git a/resources/js/components/Checkout/CheckoutSuccess.vue b/resources/js/components/Checkout/CheckoutSuccess.vue index e59c83ab9..1ca75efdb 100644 --- a/resources/js/components/Checkout/CheckoutSuccess.vue +++ b/resources/js/components/Checkout/CheckoutSuccess.vue @@ -1,5 +1,6 @@