This fixes the discount_ calculation logic and promotion tax inclusiveness calculation (#12960)
* This fixes the discount_ calculation logic * This fixes the adjustment to be handled as a subtotal value in every calculation and applies the tax inclusive logic on the promotion value itself * Added some testcases and revoked some changes to improve testing output * Fixed a test case based on feedback * Corrected promotion/admin test cases * Corrected cart/store test case * Improved cart/store test cases for more robust promotion testing considering tax inclusion flags * Remove unnessary changes as adjustments now automatically are subtotals and therefore the tax inclusive flag does not need to be applied again * Remove adjustments->is_tax_inclusive usage everywhere * Migration script to remove is_tax_inclusive in cart line item adjustment * Forgot to adjust one more testcase * Corrections based on fPolic feedback * Refactored PR to consider feedback from oliver * Added more testcases for promotion in cart --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/promotion": patch
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
"integration-tests-http": patch
|
||||||
|
"@medusajs/cart": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
This fixes the discount\_ calculation logic and promotion tax inclusiveness calculation
|
||||||
@@ -419,22 +419,22 @@ medusaIntegrationTestRunner({
|
|||||||
compare_at_unit_price: null,
|
compare_at_unit_price: null,
|
||||||
is_tax_inclusive: true,
|
is_tax_inclusive: true,
|
||||||
quantity: 2,
|
quantity: 2,
|
||||||
tax_lines: [
|
tax_lines: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
description: "CA Default Rate",
|
description: "CA Default Rate",
|
||||||
code: "CADEFAULT",
|
code: "CADEFAULT",
|
||||||
rate: 5,
|
rate: 5,
|
||||||
provider_id: "system",
|
provider_id: "system",
|
||||||
}),
|
}),
|
||||||
],
|
]),
|
||||||
adjustments: [
|
adjustments: expect.arrayContaining([
|
||||||
{
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
code: "PROMOTION_APPLIED",
|
code: "PROMOTION_APPLIED",
|
||||||
promotion_id: promotion.id,
|
promotion_id: promotion.id,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
},
|
}),
|
||||||
],
|
]),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
@@ -456,14 +456,14 @@ medusaIntegrationTestRunner({
|
|||||||
id: cart.id,
|
id: cart.id,
|
||||||
items: expect.arrayContaining([
|
items: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
adjustments: [
|
adjustments: expect.arrayContaining([
|
||||||
{
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
code: "PROMOTION_APPLIED",
|
code: "PROMOTION_APPLIED",
|
||||||
promotion_id: promotion.id,
|
promotion_id: promotion.id,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
},
|
}),
|
||||||
],
|
]),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
@@ -823,22 +823,22 @@ medusaIntegrationTestRunner({
|
|||||||
compare_at_unit_price: 1500,
|
compare_at_unit_price: 1500,
|
||||||
is_tax_inclusive: true,
|
is_tax_inclusive: true,
|
||||||
quantity: 2,
|
quantity: 2,
|
||||||
tax_lines: [
|
tax_lines: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
description: "CA Default Rate",
|
description: "CA Default Rate",
|
||||||
code: "CADEFAULT",
|
code: "CADEFAULT",
|
||||||
rate: 5,
|
rate: 5,
|
||||||
provider_id: "system",
|
provider_id: "system",
|
||||||
}),
|
}),
|
||||||
],
|
]),
|
||||||
adjustments: [
|
adjustments: expect.arrayContaining([
|
||||||
{
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
code: "PROMOTION_APPLIED",
|
code: "PROMOTION_APPLIED",
|
||||||
promotion_id: promotion.id,
|
promotion_id: promotion.id,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
},
|
}),
|
||||||
],
|
]),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
@@ -1235,7 +1235,7 @@ medusaIntegrationTestRunner({
|
|||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
credit_line_total: 2395,
|
credit_line_total: 2395,
|
||||||
discount_total: 100,
|
discount_total: 105,
|
||||||
credit_lines: [
|
credit_lines: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
amount: 2395,
|
amount: 2395,
|
||||||
@@ -2794,7 +2794,7 @@ medusaIntegrationTestRunner({
|
|||||||
is_discountable: true,
|
is_discountable: true,
|
||||||
unit_price: 1500,
|
unit_price: 1500,
|
||||||
total: 1395,
|
total: 1395,
|
||||||
discount_total: 100,
|
discount_total: 105,
|
||||||
adjustments: [
|
adjustments: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
promotion_id: promotion.id,
|
promotion_id: promotion.id,
|
||||||
@@ -2825,14 +2825,14 @@ medusaIntegrationTestRunner({
|
|||||||
id: cart.id,
|
id: cart.id,
|
||||||
items: expect.arrayContaining([
|
items: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
adjustments: [
|
adjustments: expect.arrayContaining([
|
||||||
{
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
code: "PROMOTION_APPLIED",
|
code: "PROMOTION_APPLIED",
|
||||||
promotion_id: promotion.id,
|
promotion_id: promotion.id,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
},
|
}),
|
||||||
],
|
]),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
@@ -2860,6 +2860,609 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should add a 100 USD tax exclusive promotion for a 105 USD tax inclusive item and logically result in a 0 total with tax 5%", async () => {
|
||||||
|
const taxExclPromotion = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/promotions`,
|
||||||
|
{
|
||||||
|
code: "PROMOTION_TAX_EXCLUSIVE",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
status: PromotionStatus.ACTIVE,
|
||||||
|
is_tax_inclusive: false, //Here we apply a tax exclusive promotion to a tax inclusive item in a way that the total SHOULD be 0
|
||||||
|
application_method: {
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
allocation: "across",
|
||||||
|
currency_code: "usd",
|
||||||
|
value: 100,
|
||||||
|
apply_to_quantity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.promotion
|
||||||
|
|
||||||
|
const product = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/products`,
|
||||||
|
{
|
||||||
|
title: "Product for free",
|
||||||
|
description: "test",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Size",
|
||||||
|
values: ["S", "M", "L", "XL"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "S / Black",
|
||||||
|
sku: "special-shirt",
|
||||||
|
options: {
|
||||||
|
Size: "S",
|
||||||
|
},
|
||||||
|
manage_inventory: false,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 105,
|
||||||
|
currency_code: "usd",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts`,
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: region.id,
|
||||||
|
shipping_address: shippingAddressData,
|
||||||
|
},
|
||||||
|
storeHeadersWithCustomer
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts/${cart.id}/line-items`,
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
let updated = await api.post(
|
||||||
|
`/store/carts/${cart.id}`,
|
||||||
|
{ promo_codes: [taxExclPromotion.code] },
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(updated.status).toEqual(200)
|
||||||
|
expect(updated.data.cart).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
discount_total: 105,
|
||||||
|
discount_subtotal: 100,
|
||||||
|
discount_tax_total: 5,
|
||||||
|
original_total: 105,
|
||||||
|
total: 0, // 105 - 100 tax excl promotion + 5 promotion tax
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
adjustments: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
code: taxExclPromotion.code,
|
||||||
|
amount: 100,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
promotions: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
code: "PROMOTION_TAX_EXCLUSIVE",
|
||||||
|
application_method: expect.objectContaining({
|
||||||
|
value: 100,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add a 105 USD tax inclusive promotion (fixed, across, apply_to_quantity=1) for a 105 USD tax inclusive item and logically result in a 0 total with tax 5%", async () => {
|
||||||
|
const taxInclPromotion = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/promotions`,
|
||||||
|
{
|
||||||
|
code: "PROMOTION_TAX_INCLUSIVE",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
status: PromotionStatus.ACTIVE,
|
||||||
|
is_tax_inclusive: true, //Here we apply a tax inclusive promotion to a tax inclusive item in a way that the total SHOULD be 0
|
||||||
|
application_method: {
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
allocation: "across",
|
||||||
|
currency_code: "usd",
|
||||||
|
value: 105,
|
||||||
|
apply_to_quantity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.promotion
|
||||||
|
|
||||||
|
const product = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/products`,
|
||||||
|
{
|
||||||
|
title: "Product for free",
|
||||||
|
description: "test",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Size",
|
||||||
|
values: ["S", "M", "L", "XL"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "S / Black",
|
||||||
|
sku: "special-shirt",
|
||||||
|
options: {
|
||||||
|
Size: "S",
|
||||||
|
},
|
||||||
|
manage_inventory: false,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 105,
|
||||||
|
currency_code: "usd",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts`,
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: region.id,
|
||||||
|
shipping_address: shippingAddressData,
|
||||||
|
},
|
||||||
|
storeHeadersWithCustomer
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts/${cart.id}/line-items`,
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
let updated = await api.post(
|
||||||
|
`/store/carts/${cart.id}`,
|
||||||
|
{ promo_codes: [taxInclPromotion.code] },
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(updated.status).toEqual(200)
|
||||||
|
expect(updated.data.cart).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
discount_total: 105,
|
||||||
|
discount_subtotal: 100,
|
||||||
|
discount_tax_total: 5,
|
||||||
|
original_total: 105,
|
||||||
|
total: 0, // 105 - 100 tax excl promotion + 5 promotion tax
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
adjustments: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
code: taxInclPromotion.code,
|
||||||
|
amount: 105,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
promotions: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
code: "PROMOTION_TAX_INCLUSIVE",
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
application_method: expect.objectContaining({
|
||||||
|
value: 105,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add a 105 USD tax inclusive promotion (fixed, across, apply_to_quantity=1) for two 105 USD tax inclusive items and logically result in a 105 total with tax 5%", async () => {
|
||||||
|
const taxInclPromotion = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/promotions`,
|
||||||
|
{
|
||||||
|
code: "PROMOTION_TAX_INCLUSIVE",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
status: PromotionStatus.ACTIVE,
|
||||||
|
is_tax_inclusive: true, //Here we apply a tax inclusive promotion to a tax inclusive item in a way that the total SHOULD be 0
|
||||||
|
application_method: {
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
allocation: "across",
|
||||||
|
currency_code: "usd",
|
||||||
|
value: 105,
|
||||||
|
apply_to_quantity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.promotion
|
||||||
|
|
||||||
|
const product = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/products`,
|
||||||
|
{
|
||||||
|
title: "Product for free",
|
||||||
|
description: "test",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Size",
|
||||||
|
values: ["S", "M", "L", "XL"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "S / Black",
|
||||||
|
sku: "special-shirt",
|
||||||
|
options: {
|
||||||
|
Size: "S",
|
||||||
|
},
|
||||||
|
manage_inventory: false,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 105,
|
||||||
|
currency_code: "usd",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts`,
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: region.id,
|
||||||
|
shipping_address: shippingAddressData,
|
||||||
|
},
|
||||||
|
storeHeadersWithCustomer
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts/${cart.id}/line-items`,
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 2,
|
||||||
|
},
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
let updated = await api.post(
|
||||||
|
`/store/carts/${cart.id}`,
|
||||||
|
{ promo_codes: [taxInclPromotion.code] },
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(updated.status).toEqual(200)
|
||||||
|
expect(updated.data.cart).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
discount_total: 105,
|
||||||
|
discount_subtotal: 100,
|
||||||
|
discount_tax_total: 5,
|
||||||
|
original_total: 210,
|
||||||
|
total: 105, // 210 - 100 tax excl promotion + 5 promotion tax
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
adjustments: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
code: taxInclPromotion.code,
|
||||||
|
amount: 105,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
promotions: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
code: "PROMOTION_TAX_INCLUSIVE",
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
application_method: expect.objectContaining({
|
||||||
|
value: 105,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add a 105 USD tax inclusive promotion (fixed, each, max_quantity=2) for two 105 USD tax inclusive items and logically result in a 0 total with tax 5%", async () => {
|
||||||
|
const taxInclPromotion = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/promotions`,
|
||||||
|
{
|
||||||
|
code: "PROMOTION_TAX_INCLUSIVE",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
status: PromotionStatus.ACTIVE,
|
||||||
|
is_tax_inclusive: true, //Here we apply a tax inclusive promotion to a tax inclusive item in a way that the total SHOULD be 0
|
||||||
|
application_method: {
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
allocation: "each",
|
||||||
|
currency_code: "usd",
|
||||||
|
value: 105,
|
||||||
|
max_quantity: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.promotion
|
||||||
|
|
||||||
|
const product = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/products`,
|
||||||
|
{
|
||||||
|
title: "Product for free",
|
||||||
|
description: "test",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Size",
|
||||||
|
values: ["S", "M", "L", "XL"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "S / Black",
|
||||||
|
sku: "special-shirt",
|
||||||
|
options: {
|
||||||
|
Size: "S",
|
||||||
|
},
|
||||||
|
manage_inventory: false,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 105,
|
||||||
|
currency_code: "usd",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts`,
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: region.id,
|
||||||
|
shipping_address: shippingAddressData,
|
||||||
|
},
|
||||||
|
storeHeadersWithCustomer
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts/${cart.id}/line-items`,
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 2,
|
||||||
|
},
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
let updated = await api.post(
|
||||||
|
`/store/carts/${cart.id}`,
|
||||||
|
{ promo_codes: [taxInclPromotion.code] },
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(updated.status).toEqual(200)
|
||||||
|
expect(updated.data.cart).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
discount_total: 105,
|
||||||
|
discount_subtotal: 100,
|
||||||
|
discount_tax_total: 5,
|
||||||
|
original_total: 210,
|
||||||
|
total: 105, // 210 - 100 tax excl promotion + 5 promotion tax
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
adjustments: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
code: taxInclPromotion.code,
|
||||||
|
amount: 105,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
promotions: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
code: "PROMOTION_TAX_INCLUSIVE",
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
application_method: expect.objectContaining({
|
||||||
|
value: 105,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add two tax inclusive promotions (50,100) (fixed, across) for two 105 USD tax inclusive items", async () => {
|
||||||
|
const taxInclPromotion50 = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/promotions`,
|
||||||
|
{
|
||||||
|
code: "PROMOTION_TAX_INCLUSIVE_50",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
status: PromotionStatus.ACTIVE,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
application_method: {
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
allocation: "across",
|
||||||
|
currency_code: "usd",
|
||||||
|
value: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.promotion
|
||||||
|
|
||||||
|
const taxInclPromotion100 = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/promotions`,
|
||||||
|
{
|
||||||
|
code: "PROMOTION_TAX_INCLUSIVE_100",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
status: PromotionStatus.ACTIVE,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
application_method: {
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
allocation: "across",
|
||||||
|
currency_code: "usd",
|
||||||
|
value: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.promotion
|
||||||
|
|
||||||
|
const product = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/products`,
|
||||||
|
{
|
||||||
|
title: "Product for free",
|
||||||
|
description: "test",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Size",
|
||||||
|
values: ["S", "M", "L", "XL"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "S / Black",
|
||||||
|
sku: "special-shirt",
|
||||||
|
options: {
|
||||||
|
Size: "S",
|
||||||
|
},
|
||||||
|
manage_inventory: false,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 105,
|
||||||
|
currency_code: "usd",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts`,
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: region.id,
|
||||||
|
shipping_address: shippingAddressData,
|
||||||
|
},
|
||||||
|
storeHeadersWithCustomer
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts/${cart.id}/line-items`,
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 2,
|
||||||
|
},
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
let updated = await api.post(
|
||||||
|
`/store/carts/${cart.id}`,
|
||||||
|
{
|
||||||
|
promo_codes: [taxInclPromotion50.code, taxInclPromotion100.code],
|
||||||
|
},
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(updated.status).toEqual(200)
|
||||||
|
expect(updated.data.cart).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
discount_total: 150,
|
||||||
|
original_total: 210,
|
||||||
|
total: 60, // 210 - (100 + 50 tax incl promotion)
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
adjustments: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
code: taxInclPromotion50.code,
|
||||||
|
amount: 50,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
code: taxInclPromotion100.code,
|
||||||
|
amount: 100,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("POST /store/carts/:id/customer", () => {
|
describe("POST /store/carts/:id/customer", () => {
|
||||||
|
|||||||
@@ -1322,9 +1322,9 @@ medusaIntegrationTestRunner({
|
|||||||
original_total: 1300,
|
original_total: 1300,
|
||||||
original_tax_total: 260,
|
original_tax_total: 260,
|
||||||
|
|
||||||
discount_total: 100,
|
discount_total: 125,
|
||||||
discount_subtotal: 100,
|
discount_subtotal: 100,
|
||||||
discount_tax_total: 20,
|
discount_tax_total: 25,
|
||||||
|
|
||||||
item_total: 1175,
|
item_total: 1175,
|
||||||
item_subtotal: 1040,
|
item_subtotal: 1040,
|
||||||
@@ -1354,14 +1354,13 @@ medusaIntegrationTestRunner({
|
|||||||
original_total: 1300,
|
original_total: 1300,
|
||||||
original_tax_total: 260,
|
original_tax_total: 260,
|
||||||
|
|
||||||
discount_total: 100,
|
discount_total: 125,
|
||||||
discount_subtotal: 100,
|
discount_subtotal: 100,
|
||||||
discount_tax_total: 20,
|
discount_tax_total: 25,
|
||||||
|
|
||||||
adjustments: expect.arrayContaining([
|
adjustments: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
amount: 100,
|
amount: 100,
|
||||||
is_tax_inclusive: false,
|
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
@@ -1403,9 +1402,9 @@ medusaIntegrationTestRunner({
|
|||||||
original_total: 1300,
|
original_total: 1300,
|
||||||
original_tax_total: 260,
|
original_tax_total: 260,
|
||||||
|
|
||||||
discount_total: 100,
|
discount_total: 125,
|
||||||
discount_subtotal: 100,
|
discount_subtotal: 100,
|
||||||
discount_tax_total: 20,
|
discount_tax_total: 25,
|
||||||
|
|
||||||
item_total: 1175,
|
item_total: 1175,
|
||||||
item_subtotal: 1040,
|
item_subtotal: 1040,
|
||||||
@@ -1435,14 +1434,13 @@ medusaIntegrationTestRunner({
|
|||||||
original_total: 1300,
|
original_total: 1300,
|
||||||
original_tax_total: 260,
|
original_tax_total: 260,
|
||||||
|
|
||||||
discount_total: 100,
|
discount_total: 125,
|
||||||
discount_subtotal: 100,
|
discount_subtotal: 100,
|
||||||
discount_tax_total: 20,
|
discount_tax_total: 25,
|
||||||
|
|
||||||
adjustments: expect.arrayContaining([
|
adjustments: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
amount: 100,
|
amount: 100,
|
||||||
is_tax_inclusive: false,
|
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
@@ -1598,7 +1596,6 @@ medusaIntegrationTestRunner({
|
|||||||
adjustments: expect.arrayContaining([
|
adjustments: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
amount: 100,
|
amount: 100,
|
||||||
is_tax_inclusive: false,
|
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
@@ -1679,7 +1676,6 @@ medusaIntegrationTestRunner({
|
|||||||
adjustments: expect.arrayContaining([
|
adjustments: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
amount: 100,
|
amount: 100,
|
||||||
is_tax_inclusive: false,
|
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -61,11 +61,10 @@ export interface AddItemAdjustmentAction {
|
|||||||
amount: BigNumberInput
|
amount: BigNumberInput
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the adjustment amount includes tax.
|
* Whether the promotion amount includes tax.
|
||||||
*/
|
*/
|
||||||
is_tax_inclusive?: boolean
|
is_tax_inclusive?: boolean
|
||||||
|
|
||||||
/**
|
|
||||||
/**
|
/**
|
||||||
* The promotion's code.
|
* The promotion's code.
|
||||||
*/
|
*/
|
||||||
@@ -186,6 +185,11 @@ export interface ComputeActionItemLine extends Record<string, unknown> {
|
|||||||
*/
|
*/
|
||||||
subtotal: BigNumberInput
|
subtotal: BigNumberInput
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total of the line item.
|
||||||
|
*/
|
||||||
|
total: BigNumberInput
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the line item is discountable.
|
* Whether the line item is discountable.
|
||||||
*/
|
*/
|
||||||
@@ -211,6 +215,11 @@ export interface ComputeActionShippingLine extends Record<string, unknown> {
|
|||||||
*/
|
*/
|
||||||
subtotal: BigNumberInput
|
subtotal: BigNumberInput
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total of the shipping method.
|
||||||
|
*/
|
||||||
|
total: BigNumberInput
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The adjustments applied before on the shipping method.
|
* The adjustments applied before on the shipping method.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -124,8 +124,8 @@ describe("Total calculation", function () {
|
|||||||
adjustments: [
|
adjustments: [
|
||||||
{
|
{
|
||||||
amount: 10,
|
amount: 10,
|
||||||
total: 11,
|
|
||||||
subtotal: 10,
|
subtotal: 10,
|
||||||
|
total: 11,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
subtotal: 100,
|
subtotal: 100,
|
||||||
@@ -244,16 +244,16 @@ describe("Total calculation", function () {
|
|||||||
adjustments: [
|
adjustments: [
|
||||||
{
|
{
|
||||||
amount: 9,
|
amount: 9,
|
||||||
subtotal: 8.181818181818182,
|
subtotal: 9,
|
||||||
total: 9,
|
total: 9.9,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
subtotal: 90,
|
subtotal: 90,
|
||||||
total: 89.1,
|
total: 89.1,
|
||||||
original_total: 99,
|
original_total: 99,
|
||||||
discount_total: 9,
|
discount_total: 9.9,
|
||||||
discount_subtotal: 9,
|
discount_subtotal: 9,
|
||||||
discount_tax_total: 0.8181818181818182,
|
discount_tax_total: 0.9,
|
||||||
tax_total: 8.1,
|
tax_total: 8.1,
|
||||||
original_tax_total: 9,
|
original_tax_total: 9,
|
||||||
},
|
},
|
||||||
@@ -298,17 +298,17 @@ describe("Total calculation", function () {
|
|||||||
adjustments: [
|
adjustments: [
|
||||||
{
|
{
|
||||||
amount: 9,
|
amount: 9,
|
||||||
subtotal: 8.181818181818182,
|
subtotal: 9,
|
||||||
total: 9,
|
total: 9.9,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
amount: 99,
|
amount: 99,
|
||||||
subtotal: 90,
|
subtotal: 90,
|
||||||
total: 89.1,
|
total: 89.1,
|
||||||
original_total: 99,
|
original_total: 99,
|
||||||
discount_total: 9,
|
discount_total: 9.9,
|
||||||
discount_subtotal: 9,
|
discount_subtotal: 9,
|
||||||
discount_tax_total: 0.8181818181818182,
|
discount_tax_total: 0.9,
|
||||||
tax_total: 8.1,
|
tax_total: 8.1,
|
||||||
original_tax_total: 9,
|
original_tax_total: 9,
|
||||||
},
|
},
|
||||||
@@ -342,9 +342,9 @@ describe("Total calculation", function () {
|
|||||||
total: 191.4,
|
total: 191.4,
|
||||||
subtotal: 198,
|
subtotal: 198,
|
||||||
tax_total: 17.4,
|
tax_total: 17.4,
|
||||||
discount_total: 24.6,
|
discount_total: 26.4,
|
||||||
discount_subtotal: 24,
|
discount_subtotal: 24,
|
||||||
discount_tax_total: 2.2363636363636363,
|
discount_tax_total: 2.4,
|
||||||
original_total: 217.8,
|
original_total: 217.8,
|
||||||
original_tax_total: 19.8,
|
original_tax_total: 19.8,
|
||||||
item_total: 95.7,
|
item_total: 95.7,
|
||||||
@@ -562,7 +562,7 @@ describe("Total calculation", function () {
|
|||||||
* TAX INCLUSIVE CART
|
* TAX INCLUSIVE CART
|
||||||
*
|
*
|
||||||
* Total price -> 120 tax inclusive
|
* Total price -> 120 tax inclusive
|
||||||
* Fixed discount -> 10 tax inclusive
|
* Fixed discount -> 10 tax inclusive total (which results in a subtotal of 8.33 of the discount)
|
||||||
* Tax rate -> 20%
|
* Tax rate -> 20%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -574,8 +574,7 @@ describe("Total calculation", function () {
|
|||||||
is_tax_inclusive: true,
|
is_tax_inclusive: true,
|
||||||
adjustments: [
|
adjustments: [
|
||||||
{
|
{
|
||||||
amount: 10,
|
amount: 8.333333333333334,
|
||||||
is_tax_inclusive: true,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tax_lines: [
|
tax_lines: [
|
||||||
@@ -615,8 +614,7 @@ describe("Total calculation", function () {
|
|||||||
],
|
],
|
||||||
adjustments: [
|
adjustments: [
|
||||||
{
|
{
|
||||||
is_tax_inclusive: true,
|
amount: 8.333333333333334,
|
||||||
amount: 10, // <- amount is tax inclusive so it's equal to total
|
|
||||||
subtotal: 8.333333333333334,
|
subtotal: 8.333333333333334,
|
||||||
total: 10,
|
total: 10,
|
||||||
},
|
},
|
||||||
@@ -625,7 +623,7 @@ describe("Total calculation", function () {
|
|||||||
],
|
],
|
||||||
subtotal: 100,
|
subtotal: 100,
|
||||||
tax_total: 18.333333333333332,
|
tax_total: 18.333333333333332,
|
||||||
total: 110, // total is 120 - 10 tax inclusive discount
|
total: 110,
|
||||||
|
|
||||||
original_item_subtotal: 100,
|
original_item_subtotal: 100,
|
||||||
original_item_tax_total: 20,
|
original_item_tax_total: 20,
|
||||||
@@ -882,4 +880,338 @@ describe("Total calculation", function () {
|
|||||||
credit_line_total: 40,
|
credit_line_total: 40,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should calculate carts with items + taxes + adjustments", function () {
|
||||||
|
const cart = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
unit_price: 119,
|
||||||
|
quantity: 1,
|
||||||
|
tax_lines: [
|
||||||
|
{
|
||||||
|
rate: 19,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
adjustments: [
|
||||||
|
{
|
||||||
|
amount: 119,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialized = JSON.parse(JSON.stringify(decorateCartTotals(cart)))
|
||||||
|
|
||||||
|
expect(serialized).toEqual({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
unit_price: 119,
|
||||||
|
quantity: 1,
|
||||||
|
tax_lines: [
|
||||||
|
{
|
||||||
|
rate: 19,
|
||||||
|
subtotal: 22.61,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
adjustments: [
|
||||||
|
{
|
||||||
|
amount: 119,
|
||||||
|
subtotal: 119,
|
||||||
|
total: 141.61,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
subtotal: 119,
|
||||||
|
total: 0,
|
||||||
|
original_total: 141.61,
|
||||||
|
discount_total: 141.61,
|
||||||
|
discount_subtotal: 119,
|
||||||
|
discount_tax_total: 22.61,
|
||||||
|
tax_total: 0,
|
||||||
|
original_tax_total: 22.61,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 0,
|
||||||
|
subtotal: 119,
|
||||||
|
tax_total: 0,
|
||||||
|
discount_total: 141.61,
|
||||||
|
discount_subtotal: 119,
|
||||||
|
discount_tax_total: 22.61,
|
||||||
|
original_total: 141.61,
|
||||||
|
original_tax_total: 22.61,
|
||||||
|
item_total: 0,
|
||||||
|
item_subtotal: 119,
|
||||||
|
item_tax_total: 0,
|
||||||
|
original_item_total: 141.61,
|
||||||
|
original_item_subtotal: 119,
|
||||||
|
original_item_tax_total: 22.61,
|
||||||
|
credit_line_subtotal: 0,
|
||||||
|
credit_line_tax_total: 0,
|
||||||
|
credit_line_total: 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should calculate carts with items + taxes with is_tax_inclusive", function () {
|
||||||
|
const cartWithTax = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
unit_price: 119,
|
||||||
|
quantity: 1,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
tax_lines: [
|
||||||
|
{
|
||||||
|
rate: 19,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const cartWithoutTax = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
unit_price: 119,
|
||||||
|
quantity: 1,
|
||||||
|
is_tax_inclusive: false,
|
||||||
|
tax_lines: [
|
||||||
|
{
|
||||||
|
rate: 19,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const cartMixed = {
|
||||||
|
items: [...cartWithTax.items, ...cartWithoutTax.items],
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializedWith = JSON.parse(
|
||||||
|
JSON.stringify(decorateCartTotals(cartWithTax))
|
||||||
|
)
|
||||||
|
const serializedWithout = JSON.parse(
|
||||||
|
JSON.stringify(decorateCartTotals(cartWithoutTax))
|
||||||
|
)
|
||||||
|
const serializedMixed = JSON.parse(
|
||||||
|
JSON.stringify(decorateCartTotals(cartMixed))
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(serializedWith).toEqual({
|
||||||
|
credit_line_subtotal: 0,
|
||||||
|
credit_line_tax_total: 0,
|
||||||
|
credit_line_total: 0,
|
||||||
|
discount_subtotal: 0,
|
||||||
|
discount_tax_total: 0,
|
||||||
|
discount_total: 0,
|
||||||
|
item_subtotal: 100,
|
||||||
|
item_tax_total: 19,
|
||||||
|
item_total: 119,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
discount_subtotal: 0,
|
||||||
|
discount_tax_total: 0,
|
||||||
|
discount_total: 0,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
original_tax_total: 19,
|
||||||
|
original_total: 119,
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 100,
|
||||||
|
tax_lines: [
|
||||||
|
{
|
||||||
|
rate: 19,
|
||||||
|
subtotal: 19,
|
||||||
|
total: 19,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tax_total: 19,
|
||||||
|
total: 119,
|
||||||
|
unit_price: 119,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
original_item_subtotal: 100,
|
||||||
|
original_item_tax_total: 19,
|
||||||
|
original_item_total: 119,
|
||||||
|
original_tax_total: 19,
|
||||||
|
original_total: 119,
|
||||||
|
subtotal: 100,
|
||||||
|
tax_total: 19,
|
||||||
|
total: 119,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(serializedWithout).toEqual({
|
||||||
|
credit_line_subtotal: 0,
|
||||||
|
credit_line_tax_total: 0,
|
||||||
|
credit_line_total: 0,
|
||||||
|
discount_subtotal: 0,
|
||||||
|
discount_tax_total: 0,
|
||||||
|
discount_total: 0,
|
||||||
|
item_subtotal: 119,
|
||||||
|
item_tax_total: 22.61,
|
||||||
|
item_total: 141.61,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
discount_subtotal: 0,
|
||||||
|
discount_tax_total: 0,
|
||||||
|
discount_total: 0,
|
||||||
|
is_tax_inclusive: false,
|
||||||
|
original_tax_total: 22.61,
|
||||||
|
original_total: 141.61,
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 119,
|
||||||
|
tax_lines: [
|
||||||
|
{
|
||||||
|
rate: 19,
|
||||||
|
subtotal: 22.61,
|
||||||
|
total: 22.61,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tax_total: 22.61,
|
||||||
|
total: 141.61,
|
||||||
|
unit_price: 119,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
original_item_subtotal: 119,
|
||||||
|
original_item_tax_total: 22.61,
|
||||||
|
original_item_total: 141.61,
|
||||||
|
original_tax_total: 22.61,
|
||||||
|
original_total: 141.61,
|
||||||
|
subtotal: 119,
|
||||||
|
tax_total: 22.61,
|
||||||
|
total: 141.61,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(serializedMixed).toEqual({
|
||||||
|
credit_line_subtotal: 0,
|
||||||
|
credit_line_tax_total: 0,
|
||||||
|
credit_line_total: 0,
|
||||||
|
discount_subtotal: 0,
|
||||||
|
discount_tax_total: 0,
|
||||||
|
discount_total: 0,
|
||||||
|
item_subtotal: 219,
|
||||||
|
item_tax_total: 41.61,
|
||||||
|
item_total: 260.61,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
discount_subtotal: 0,
|
||||||
|
discount_tax_total: 0,
|
||||||
|
discount_total: 0,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
original_tax_total: 19,
|
||||||
|
original_total: 119,
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 100,
|
||||||
|
tax_lines: [
|
||||||
|
{
|
||||||
|
rate: 19,
|
||||||
|
subtotal: 19,
|
||||||
|
total: 19,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tax_total: 19,
|
||||||
|
total: 119,
|
||||||
|
unit_price: 119,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
discount_subtotal: 0,
|
||||||
|
discount_tax_total: 0,
|
||||||
|
discount_total: 0,
|
||||||
|
is_tax_inclusive: false,
|
||||||
|
original_tax_total: 22.61,
|
||||||
|
original_total: 141.61,
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 119,
|
||||||
|
tax_lines: [
|
||||||
|
{
|
||||||
|
rate: 19,
|
||||||
|
subtotal: 22.61,
|
||||||
|
total: 22.61,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tax_total: 22.61,
|
||||||
|
total: 141.61,
|
||||||
|
unit_price: 119,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
original_item_subtotal: 219,
|
||||||
|
original_item_tax_total: 41.61,
|
||||||
|
original_item_total: 260.61,
|
||||||
|
original_tax_total: 41.61,
|
||||||
|
original_total: 260.61,
|
||||||
|
subtotal: 219,
|
||||||
|
tax_total: 41.61,
|
||||||
|
total: 260.61,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should calculate tax inclusive carts with items + taxes with tax inclusive adjustments", function () {
|
||||||
|
const cart = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
unit_price: 119,
|
||||||
|
quantity: 1,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
adjustments: [
|
||||||
|
{
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tax_lines: [
|
||||||
|
{
|
||||||
|
rate: 19,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialized = JSON.parse(JSON.stringify(decorateCartTotals(cart)))
|
||||||
|
|
||||||
|
expect(serialized).toEqual({
|
||||||
|
credit_line_subtotal: 0,
|
||||||
|
credit_line_tax_total: 0,
|
||||||
|
credit_line_total: 0,
|
||||||
|
discount_subtotal: 100,
|
||||||
|
discount_tax_total: 19,
|
||||||
|
discount_total: 119,
|
||||||
|
item_subtotal: 100,
|
||||||
|
item_tax_total: 0,
|
||||||
|
item_total: 0,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
adjustments: [
|
||||||
|
{
|
||||||
|
amount: 100,
|
||||||
|
subtotal: 100,
|
||||||
|
total: 119,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
discount_subtotal: 100,
|
||||||
|
discount_tax_total: 19,
|
||||||
|
discount_total: 119,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
original_tax_total: 19,
|
||||||
|
original_total: 119,
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 100,
|
||||||
|
tax_lines: [
|
||||||
|
{
|
||||||
|
rate: 19,
|
||||||
|
subtotal: 19,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tax_total: 0,
|
||||||
|
total: 0,
|
||||||
|
unit_price: 119,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
original_item_subtotal: 100,
|
||||||
|
original_item_tax_total: 19,
|
||||||
|
original_item_total: 119,
|
||||||
|
original_tax_total: 19,
|
||||||
|
original_total: 119,
|
||||||
|
subtotal: 100,
|
||||||
|
tax_total: 0,
|
||||||
|
total: 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ import { MathBN } from "../math"
|
|||||||
|
|
||||||
export function calculateAdjustmentTotal({
|
export function calculateAdjustmentTotal({
|
||||||
adjustments,
|
adjustments,
|
||||||
includesTax,
|
|
||||||
taxRate,
|
taxRate,
|
||||||
}: {
|
}: {
|
||||||
adjustments: Pick<AdjustmentLineDTO, "amount" | "is_tax_inclusive">[]
|
adjustments: Pick<AdjustmentLineDTO, "amount" | "is_tax_inclusive">[]
|
||||||
includesTax?: boolean
|
|
||||||
taxRate?: BigNumberInput
|
taxRate?: BigNumberInput
|
||||||
}) {
|
}) {
|
||||||
// the sum of all adjustment amounts excluding tax
|
// the sum of all adjustment amounts excluding tax
|
||||||
@@ -24,35 +22,22 @@ export function calculateAdjustmentTotal({
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const adjustmentAmount = MathBN.convert(adj.amount)
|
const adjustmentSubtotal =
|
||||||
|
isDefined(taxRate) && adj.is_tax_inclusive
|
||||||
|
? MathBN.div(adj.amount, MathBN.add(1, taxRate))
|
||||||
|
: adj.amount
|
||||||
|
|
||||||
if (adj.is_tax_inclusive && isDefined(taxRate)) {
|
const adjustmentTaxTotal = isDefined(taxRate)
|
||||||
adjustmentsSubtotal = MathBN.add(
|
? MathBN.mult(adjustmentSubtotal, taxRate)
|
||||||
adjustmentsSubtotal,
|
: 0
|
||||||
MathBN.div(adjustmentAmount, MathBN.add(1, taxRate))
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
adjustmentsSubtotal = MathBN.add(adjustmentsSubtotal, adjustmentAmount)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDefined(taxRate)) {
|
|
||||||
const adjustmentSubtotal = includesTax
|
|
||||||
? MathBN.div(adjustmentAmount, MathBN.add(1, taxRate))
|
|
||||||
: adjustmentAmount
|
|
||||||
|
|
||||||
const adjustmentTaxTotal = MathBN.mult(adjustmentSubtotal, taxRate)
|
|
||||||
const adjustmentTotal = MathBN.add(adjustmentSubtotal, adjustmentTaxTotal)
|
const adjustmentTotal = MathBN.add(adjustmentSubtotal, adjustmentTaxTotal)
|
||||||
|
|
||||||
adj["subtotal"] = new BigNumber(adjustmentSubtotal)
|
adjustmentsSubtotal = MathBN.add(adjustmentsSubtotal, adjustmentSubtotal)
|
||||||
adj["total"] = new BigNumber(adjustmentTotal)
|
|
||||||
|
|
||||||
adjustmentsTotal = MathBN.add(adjustmentsTotal, adjustmentTotal)
|
|
||||||
adjustmentsTaxTotal = MathBN.add(adjustmentsTaxTotal, adjustmentTaxTotal)
|
adjustmentsTaxTotal = MathBN.add(adjustmentsTaxTotal, adjustmentTaxTotal)
|
||||||
} else {
|
adjustmentsTotal = MathBN.add(adjustmentsTotal, adjustmentTotal)
|
||||||
adj["subtotal"] = new BigNumber(adjustmentAmount)
|
|
||||||
adj["adjustmentAmount"] = new BigNumber(adjustmentAmount)
|
adj["subtotal"] = new BigNumber(adjustmentsSubtotal)
|
||||||
adjustmentsTotal = MathBN.add(adjustmentsTotal, adjustmentAmount)
|
adj["total"] = new BigNumber(adjustmentsTotal)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface GetItemTotalInput {
|
|||||||
quantity: BigNumber
|
quantity: BigNumber
|
||||||
is_tax_inclusive?: boolean
|
is_tax_inclusive?: boolean
|
||||||
tax_lines?: Pick<TaxLineDTO, "rate">[]
|
tax_lines?: Pick<TaxLineDTO, "rate">[]
|
||||||
adjustments?: Pick<AdjustmentLineDTO, "amount">[]
|
adjustments?: Pick<AdjustmentLineDTO, "amount" | "is_tax_inclusive">[]
|
||||||
detail?: {
|
detail?: {
|
||||||
fulfilled_quantity: BigNumber
|
fulfilled_quantity: BigNumber
|
||||||
delivered_quantity: BigNumber
|
delivered_quantity: BigNumber
|
||||||
@@ -133,7 +133,6 @@ function getLineItemTotals(
|
|||||||
adjustmentsTaxTotal: discountTaxTotal,
|
adjustmentsTaxTotal: discountTaxTotal,
|
||||||
} = calculateAdjustmentTotal({
|
} = calculateAdjustmentTotal({
|
||||||
adjustments: item.adjustments || [],
|
adjustments: item.adjustments || [],
|
||||||
includesTax: isTaxInclusive,
|
|
||||||
taxRate: sumTaxRate,
|
taxRate: sumTaxRate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,23 +5,23 @@ import {
|
|||||||
} from "../../promotion"
|
} from "../../promotion"
|
||||||
import { MathBN } from "../math"
|
import { MathBN } from "../math"
|
||||||
|
|
||||||
function getPromotionValueForPercentage(promotion, lineItemTotal) {
|
function getPromotionValueForPercentage(promotion, lineItemAmount) {
|
||||||
return MathBN.mult(MathBN.div(promotion.value, 100), lineItemTotal)
|
return MathBN.mult(MathBN.div(promotion.value, 100), lineItemAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPromotionValueForFixed(promotion, itemTotal, allItemsTotal) {
|
function getPromotionValueForFixed(promotion, lineItemAmount, lineItemsAmount) {
|
||||||
if (promotion.allocation === ApplicationMethodAllocation.ACROSS) {
|
if (promotion.allocation === ApplicationMethodAllocation.ACROSS) {
|
||||||
const promotionValueForItem = MathBN.mult(
|
const promotionValueForItem = MathBN.mult(
|
||||||
MathBN.div(itemTotal, allItemsTotal),
|
MathBN.div(lineItemAmount, lineItemsAmount),
|
||||||
promotion.value
|
promotion.value
|
||||||
)
|
)
|
||||||
|
|
||||||
if (MathBN.lte(promotionValueForItem, itemTotal)) {
|
if (MathBN.lte(promotionValueForItem, lineItemAmount)) {
|
||||||
return promotionValueForItem
|
return promotionValueForItem
|
||||||
}
|
}
|
||||||
|
|
||||||
const percentage = MathBN.div(
|
const percentage = MathBN.div(
|
||||||
MathBN.mult(itemTotal, 100),
|
MathBN.mult(lineItemAmount, 100),
|
||||||
promotionValueForItem
|
promotionValueForItem
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,16 +30,15 @@ function getPromotionValueForFixed(promotion, itemTotal, allItemsTotal) {
|
|||||||
MathBN.div(percentage, 100)
|
MathBN.div(percentage, 100)
|
||||||
).precision(4)
|
).precision(4)
|
||||||
}
|
}
|
||||||
|
|
||||||
return promotion.value
|
return promotion.value
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPromotionValue(promotion, lineItemTotal, lineItemsTotal) {
|
export function getPromotionValue(promotion, lineItemAmount, lineItemsAmount) {
|
||||||
if (promotion.type === ApplicationMethodType.PERCENTAGE) {
|
if (promotion.type === ApplicationMethodType.PERCENTAGE) {
|
||||||
return getPromotionValueForPercentage(promotion, lineItemTotal)
|
return getPromotionValueForPercentage(promotion, lineItemAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
return getPromotionValueForFixed(promotion, lineItemTotal, lineItemsTotal)
|
return getPromotionValueForFixed(promotion, lineItemAmount, lineItemsAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getApplicableQuantity(lineItem, maxQuantity) {
|
export function getApplicableQuantity(lineItem, maxQuantity) {
|
||||||
@@ -50,14 +49,18 @@ export function getApplicableQuantity(lineItem, maxQuantity) {
|
|||||||
return lineItem.quantity
|
return lineItem.quantity
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLineItemUnitPrice(lineItem) {
|
function getLineItemSubtotal(lineItem) {
|
||||||
return MathBN.div(lineItem.subtotal, lineItem.quantity)
|
return MathBN.div(lineItem.subtotal, lineItem.quantity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLineItemTotal(lineItem) {
|
||||||
|
return MathBN.div(lineItem.total, lineItem.quantity)
|
||||||
|
}
|
||||||
|
|
||||||
export function calculateAdjustmentAmountFromPromotion(
|
export function calculateAdjustmentAmountFromPromotion(
|
||||||
lineItem,
|
lineItem,
|
||||||
promotion,
|
promotion,
|
||||||
lineItemsTotal: BigNumberInput = 0
|
lineItemsAmount: BigNumberInput = 0
|
||||||
) {
|
) {
|
||||||
/*
|
/*
|
||||||
For a promotion with an across allocation, we consider not only the line item total, but also the total of all other line items in the order.
|
For a promotion with an across allocation, we consider not only the line item total, but also the total of all other line items in the order.
|
||||||
@@ -89,20 +92,26 @@ export function calculateAdjustmentAmountFromPromotion(
|
|||||||
*/
|
*/
|
||||||
if (promotion.allocation === ApplicationMethodAllocation.ACROSS) {
|
if (promotion.allocation === ApplicationMethodAllocation.ACROSS) {
|
||||||
const quantity = getApplicableQuantity(lineItem, promotion.max_quantity)
|
const quantity = getApplicableQuantity(lineItem, promotion.max_quantity)
|
||||||
const lineItemTotal = MathBN.mult(getLineItemUnitPrice(lineItem), quantity)
|
|
||||||
const applicableTotal = MathBN.sub(lineItemTotal, promotion.applied_value)
|
|
||||||
|
|
||||||
if (MathBN.lte(applicableTotal, 0)) {
|
const lineItemAmount = MathBN.mult(
|
||||||
return applicableTotal
|
promotion.is_tax_inclusive
|
||||||
|
? getLineItemTotal(lineItem)
|
||||||
|
: getLineItemSubtotal(lineItem),
|
||||||
|
quantity
|
||||||
|
)
|
||||||
|
const applicableAmount = MathBN.sub(lineItemAmount, promotion.applied_value)
|
||||||
|
|
||||||
|
if (MathBN.lte(applicableAmount, 0)) {
|
||||||
|
return applicableAmount
|
||||||
}
|
}
|
||||||
|
|
||||||
const promotionValue = getPromotionValue(
|
const promotionValue = getPromotionValue(
|
||||||
promotion,
|
promotion,
|
||||||
applicableTotal,
|
applicableAmount,
|
||||||
lineItemsTotal
|
lineItemsAmount
|
||||||
)
|
)
|
||||||
|
|
||||||
return MathBN.min(promotionValue, applicableTotal)
|
return MathBN.min(promotionValue, applicableAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -124,26 +133,32 @@ export function calculateAdjustmentAmountFromPromotion(
|
|||||||
We then apply whichever is lower.
|
We then apply whichever is lower.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const remainingItemTotal = MathBN.sub(
|
const remainingItemAmount = MathBN.sub(
|
||||||
lineItem.subtotal,
|
promotion.is_tax_inclusive ? lineItem.total : lineItem.subtotal,
|
||||||
promotion.applied_value
|
promotion.applied_value
|
||||||
)
|
)
|
||||||
const unitPrice = MathBN.div(lineItem.subtotal, lineItem.quantity)
|
const itemAmount = MathBN.div(
|
||||||
const maximumPromotionTotal = MathBN.mult(
|
promotion.is_tax_inclusive ? lineItem.total : lineItem.subtotal,
|
||||||
unitPrice,
|
lineItem.quantity
|
||||||
|
)
|
||||||
|
const maximumPromotionAmount = MathBN.mult(
|
||||||
|
itemAmount,
|
||||||
promotion.max_quantity ?? MathBN.convert(1)
|
promotion.max_quantity ?? MathBN.convert(1)
|
||||||
)
|
)
|
||||||
const applicableTotal = MathBN.min(remainingItemTotal, maximumPromotionTotal)
|
const applicableAmount = MathBN.min(
|
||||||
|
remainingItemAmount,
|
||||||
|
maximumPromotionAmount
|
||||||
|
)
|
||||||
|
|
||||||
if (MathBN.lte(applicableTotal, 0)) {
|
if (MathBN.lte(applicableAmount, 0)) {
|
||||||
return MathBN.convert(0)
|
return MathBN.convert(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const promotionValue = getPromotionValue(
|
const promotionValue = getPromotionValue(
|
||||||
promotion,
|
promotion,
|
||||||
applicableTotal,
|
applicableAmount,
|
||||||
lineItemsTotal
|
lineItemsAmount
|
||||||
)
|
)
|
||||||
|
|
||||||
return MathBN.min(promotionValue, applicableTotal)
|
return MathBN.min(promotionValue, applicableAmount)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ export function getShippingMethodTotals(
|
|||||||
adjustmentsTaxTotal: discountsTaxTotal,
|
adjustmentsTaxTotal: discountsTaxTotal,
|
||||||
} = calculateAdjustmentTotal({
|
} = calculateAdjustmentTotal({
|
||||||
adjustments: shippingMethod.adjustments || [],
|
adjustments: shippingMethod.adjustments || [],
|
||||||
includesTax: isTaxInclusive,
|
|
||||||
taxRate: sumTaxRate,
|
taxRate: sumTaxRate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const defaultStoreCartFields = [
|
|||||||
"promotions.id",
|
"promotions.id",
|
||||||
"promotions.code",
|
"promotions.code",
|
||||||
"promotions.is_automatic",
|
"promotions.is_automatic",
|
||||||
|
"promotions.is_tax_inclusive",
|
||||||
"promotions.application_method.value",
|
"promotions.application_method.value",
|
||||||
"promotions.application_method.type",
|
"promotions.application_method.type",
|
||||||
"promotions.application_method.currency_code",
|
"promotions.application_method.currency_code",
|
||||||
@@ -77,6 +78,7 @@ export const defaultStoreCartFields = [
|
|||||||
"items.adjustments.code",
|
"items.adjustments.code",
|
||||||
"items.adjustments.promotion_id",
|
"items.adjustments.promotion_id",
|
||||||
"items.adjustments.amount",
|
"items.adjustments.amount",
|
||||||
|
"items.adjustments.is_tax_inclusive",
|
||||||
"customer.id",
|
"customer.id",
|
||||||
"customer.email",
|
"customer.email",
|
||||||
"customer.groups.id",
|
"customer.groups.id",
|
||||||
|
|||||||
@@ -103,24 +103,29 @@ function applyPromotionToItems(
|
|||||||
? 1
|
? 1
|
||||||
: applicationMethod?.max_quantity!
|
: applicationMethod?.max_quantity!
|
||||||
|
|
||||||
let lineItemsTotal = MathBN.convert(0)
|
let lineItemsAmount = MathBN.convert(0)
|
||||||
if (allocation === ApplicationMethodAllocation.ACROSS) {
|
if (allocation === ApplicationMethodAllocation.ACROSS) {
|
||||||
lineItemsTotal = applicableItems.reduce(
|
lineItemsAmount = applicableItems.reduce(
|
||||||
(acc, item) =>
|
(acc, item) =>
|
||||||
MathBN.sub(
|
MathBN.sub(
|
||||||
MathBN.add(acc, item.subtotal),
|
MathBN.add(
|
||||||
|
acc,
|
||||||
|
promotion.is_tax_inclusive ? item.total : item.subtotal
|
||||||
|
),
|
||||||
appliedPromotionsMap.get(item.id) ?? 0
|
appliedPromotionsMap.get(item.id) ?? 0
|
||||||
),
|
),
|
||||||
MathBN.convert(0)
|
MathBN.convert(0)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (MathBN.lte(lineItemsTotal, 0)) {
|
if (MathBN.lte(lineItemsAmount, 0)) {
|
||||||
return computedActions
|
return computedActions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of applicableItems) {
|
for (const item of applicableItems) {
|
||||||
if (MathBN.lte(item.subtotal, 0)) {
|
if (
|
||||||
|
MathBN.lte(promotion.is_tax_inclusive ? item.total : item.subtotal, 0)
|
||||||
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,11 +140,12 @@ function applyPromotionToItems(
|
|||||||
{
|
{
|
||||||
value: promotionValue,
|
value: promotionValue,
|
||||||
applied_value: appliedPromoValue,
|
applied_value: appliedPromoValue,
|
||||||
|
is_tax_inclusive: promotion.is_tax_inclusive,
|
||||||
max_quantity: maxQuantity,
|
max_quantity: maxQuantity,
|
||||||
type: applicationMethod?.type!,
|
type: applicationMethod?.type!,
|
||||||
allocation,
|
allocation,
|
||||||
},
|
},
|
||||||
lineItemsTotal
|
lineItemsAmount
|
||||||
)
|
)
|
||||||
|
|
||||||
if (MathBN.lte(amount, 0)) {
|
if (MathBN.lte(amount, 0)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user