Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Price component #362

Draft
wants to merge 31 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c4695f6
Implement tax display values from Magento settings (#303)
Jade-GG Oct 18, 2023
ca4152e
Apply fixes from Prettier
royduin Oct 18, 2023
e7a6d11
Fix merge & implement latent feedback
Jade-GG Oct 18, 2023
c877353
Use price component everywhere, remove unnecessary calculation
Jade-GG Oct 18, 2023
959237d
simpleProduct as default
Jade-GG Oct 18, 2023
0573295
v-cloak on addtocart icons
Jade-GG Oct 18, 2023
2f49d60
Add tierprices
Jade-GG Oct 18, 2023
62a26fa
Apply fixes from Duster
Jade-GG Oct 18, 2023
539a3f2
Change customer token into cookie
indykoning Oct 18, 2023
b01db37
Apply fixes from Duster
indykoning Oct 18, 2023
3828e88
Safety checks on product options
Jade-GG Oct 19, 2023
898cdb2
Fix product options reactivity
Jade-GG Oct 19, 2023
4613d45
Apply fixes from Prettier
Jade-GG Oct 19, 2023
fb8d091
Calculate tax for product, implement price component in POP and search
indykoning Oct 19, 2023
4263d58
Apply fixes from Duster
indykoning Oct 19, 2023
1abbbc1
Restore index command
indykoning Oct 19, 2023
715ca93
Merge branch 'tax_display' of github.com:rapidez/core into tax_display
indykoning Oct 19, 2023
ecbd7e0
Apply fixes from Prettier
indykoning Oct 19, 2023
9d9b943
Restrict tierprices, respect more settings
indykoning Oct 20, 2023
f59d1bc
Apply fixes from Duster
indykoning Oct 20, 2023
bb6ee89
Improve codestyle
indykoning Oct 20, 2023
ce99948
Apply fixes from Duster
indykoning Oct 20, 2023
6cc9ec2
turn undefined into empty
indykoning Oct 20, 2023
60d4399
Merge branch 'tax_display' of github.com:rapidez/core into tax_display
indykoning Oct 20, 2023
f8ec3b7
Apply fixes from Prettier
indykoning Oct 20, 2023
5f19e9d
Fixes and cleanup
royduin Oct 24, 2023
20e6679
Apply fixes from Duster
royduin Oct 24, 2023
2b11f2f
Fix tier prices being strings making comparisons not work
Jade-GG Oct 25, 2023
7b61253
WIP - Price tests and some refactoring
royduin Oct 26, 2023
18ce4d6
Merge branch 'tax_display' of github.com:rapidez/core into tax_display
royduin Oct 26, 2023
43d3a57
Apply fixes from Duster
royduin Oct 26, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions config/rapidez/models.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
'category' => Rapidez\Core\Models\Category::class,
'category_product' => Rapidez\Core\Models\CategoryProduct::class,
'customer' => Rapidez\Core\Models\Customer::class,
'customer_group' => Rapidez\Core\Models\CustomerGroup::class,
'config' => Rapidez\Core\Models\Config::class,
'oauth_token' => Rapidez\Core\Models\OauthToken::class,
'option_swatch' => Rapidez\Core\Models\OptionSwatch::class,
Expand All @@ -20,6 +21,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,
Expand All @@ -32,4 +34,5 @@
'sales_order_item' => Rapidez\Core\Models\SalesOrderItem::class,
'sales_order_payment' => Rapidez\Core\Models\SalesOrderPayment::class,
'search_query' => Rapidez\Core\Models\SearchQuery::class,
'tax_calculation' => Rapidez\Core\Models\TaxCalculation::class,
];
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions phpunit.dusk.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@
<env name="DB_DATABASE" value="magento"/>
<env name="DB_USERNAME" value="magento"/>
<env name="DB_PASSWORD" value="password"/>
<env name="CACHE_DRIVER" value="null"/>
</php>
</phpunit>
5 changes: 3 additions & 2 deletions resources/js/axios.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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()
window.magento.defaults.baseURL = config.magento_url + '/rest/' + config.store_code + '/V1/'

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.
Expand Down
3 changes: 2 additions & 1 deletion resources/js/components/Checkout/CheckoutSuccess.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script>
import GetCart from './../Cart/mixins/GetCart'
import { token } from '../../stores/useUser'

export default {
mixins: [GetCart],
Expand All @@ -26,7 +27,7 @@ export default {
},

created() {
this.token ??= localStorage.token
this.token ??= token.value
this.mask ??= localStorage.mask

this.refreshOrder()
Expand Down
3 changes: 2 additions & 1 deletion resources/js/components/GraphqlMutation.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script>
import InteractWithUser from './User/mixins/InteractWithUser'
import { token } from '../stores/useUser'

export default {
mixins: [InteractWithUser],
Expand Down Expand Up @@ -103,7 +104,7 @@ export default {
let options = { headers: {} }

if (this.$root.loggedIn) {
options['headers']['Authorization'] = `Bearer ${localStorage.token}`
options['headers']['Authorization'] = `Bearer ${token.value}`
}

if (this.recaptcha) {
Expand Down
91 changes: 58 additions & 33 deletions resources/js/components/Product/AddToCart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,14 @@ export default {
options: {},
customOptions: {},
error: null,
tierPrice: null,

adding: false,
added: false,

price: 0,
specialPrice: 0,
}),

mounted() {
this.qty = this.defaultQty
this.calculatePrices()
},

render() {
Expand Down Expand Up @@ -117,11 +114,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
Expand All @@ -148,27 +140,47 @@ export default {
reader.readAsDataURL(file)
},

priceAddition: function (basePrice) {
let addition = 0

Object.entries(this.customOptions).forEach(([key, val]) => {
if (!val) {
return
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
}
})

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
return match
},

if (optionPrice.price_type == 'fixed') {
addition += parseFloat(optionPrice.price)
} else {
addition += (parseFloat(basePrice) * parseFloat(optionPrice.price)) / 100
}
})
selectTier(id) {
this.tierPrice = id
let tierQty = this.tierPrices[this.tierPrice]?.qty ?? 1
this.qty = parseInt(tierQty)
},

tierPriceDiscount(price, discount) {
if (!discount) {
return price
}

return addition
// 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)
},
},

Expand Down Expand Up @@ -303,14 +315,27 @@ export default {

return disabledOptions
},
},

watch: {
customOptions: {
handler() {
this.calculatePrices()
},
deep: true,
tierPrices: function () {
if (!this.simpleProduct.tierPrices || this.simpleProduct.tierPrices.length == 0) {
return {}
}

return Object.fromEntries(
this.simpleProduct.tierPrices.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
},
},
Jade-GG marked this conversation as resolved.
Show resolved Hide resolved
}
Expand Down
139 changes: 139 additions & 0 deletions resources/js/components/Product/Price.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<script>
export default {
props: {
product: {
type: Object,
default: () => config.product || {},
},
location: {
type: String,
default: 'catalog',
},
options: {
type: Object,
default: () => ({}),
},
},

render() {
return this.$scopedSlots.default(this)
},

methods: {
calculatePrice(options = null) {
options ??= this.options
let product = options?.product || this.product
let location = options?.location || this.location

let special_price = options.special_price ?? false
let displayTax = this.$root.includeTaxAt(location)
let price = options?.price || (special_price ? product.special_price ?? product.price ?? 0 : product.price ?? 0) * 1

if (options.tier_price) {
price = options.tier_price.price * 1
}

if (options.product_options) {
price += this.calculateOptionsValue(price, product, options.product_options)
}

let taxMultiplier = this.getTaxPercent(product) + 1

return price

// Tax calculation can be done from the backend right?
if (window.config.tax.calculation.price_includes_tax) {
if (displayTax) {
return price
} else {
return price / taxMultiplier
}
} else {
if (displayTax) {
return price * taxMultiplier
} else {
return price
}
}
},

getTaxPercent(product) {
let country_id = window.config.tax.defaults.country_id
let region_id = window.config.tax.defaults.region_id
let postcode = window.config.tax.defaults.postcode

if (['shipping', 'billing'].includes(window.config?.tax?.calculation.based_on)) {
country_id = window.app?.$data?.checkout?.[window.config?.tax?.calculation.based_on + '_address']?.country_id || country_id
region_id = window.app?.$data?.checkout?.[window.config?.tax?.calculation.based_on + '_address']?.region_id || region_id
postcode = window.app?.$data?.checkout?.[window.config?.tax?.calculation.based_on + '_address']?.postcode || postcode
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let basedOn = window.app?.$data?.checkout?.[window.config?.tax?.calculation.based_on + '_address']

And then use that instead of repeating it 3 times, would probably be much cleaner here.

}

let taxRate = window.config.tax.rates?.[product.tax_class_id]?.find(
(rate) =>
rate.tax_country_id === country_id &&
rate.tax_region_id * 1 === region_id * 1 &&
postcode.match('^' + rate.tax_postcode.replace('*', '.*') + '$'),
)?.rate

if (taxRate === undefined || taxRate === null) {
console.debug('No tax rates found for', product, country_id, region_id, postcode)
}

return (taxRate ?? 0) / 100
},

calculateOptionsValue(basePrice, product, customOptions) {
return Object.entries(customOptions).reduce((priceAddition, [key, val]) => {
if (!val) {
return priceAddition
}

let option = 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') {
return priceAddition * 1 + parseFloat(optionPrice.price)
}

return priceAddition * 1 + (parseFloat(basePrice) * parseFloat(optionPrice.price)) / 100
}, 0)
},
},

computed: {
specialPrice() {
JSON.stringify(this.options) // Hack: make vue recognize reactivity within the options object
return this.calculatePrice({ ...this.options, ...{ special_price: true } })
},

price() {
return this.calculatePrice(this.options)
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be thought out again, Magento doesn't work with prices this way.

Prices are calculated as follows: Final Price=Min(Regular(Base) Price, Group(Tier) Price, Special Price, Catalog Price Rule) + Sum(Min Price per each required custom option)

Note that prices are clamped to a minimum of 0 before and after the custom options are added. Also, the price gets rounded before getting multiplied by the qty.


isDiscounted() {
return this.specialPrice != this.price
},

range() {
if (!this.product.super_attributes) {
return null
}

let prices = Object.values(this.product.children).map((child) =>
this.calculatePrice({ ...this.option, ...{ special_price: true, product: child } }),
)

return {
min: Math.min(...prices),
max: Math.max(...prices),
}
},

includesTax() {
return this.$root.includeTaxAt(this.location)
},
},
}
</script>
6 changes: 4 additions & 2 deletions resources/js/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ Vue.filter('truncate', function (value, limit) {
return value
})

Vue.filter('price', function (value) {
window.price = function (value) {
return new Intl.NumberFormat(config.locale.replace('_', '-'), {
style: 'currency',
currency: config.currency,
}).format(value)
})
}

Vue.filter('price', window.price)

window.url = function (path = '') {
// Transform urls starting with / into url with domain
Expand Down
Loading