feat(promotion, dashboard, core-flows, cart, types, utils, medusa): tax inclusive promotions (#12412)
* feat: tax inclusive promotions * feat: add a totals test case * feat: add integration test * chore: changeset * fix: typo * chore: refactor * fix: tests * fix: rest of buyget action tests * fix: cart spec * chore: expand integration test with item level totals * feat: add a few more test cases --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
1 parent
08de1f54e4
commit
2621f00bb0
29 files changed
+1091
-24
No files matched your search
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/promotion": patch
|
||||||
|
"@medusajs/dashboard": patch
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
"@medusajs/cart": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(promotion, dashboard, core-flows, cart, types, utils, medusa): tax inclusive promotions
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||||
import { PromotionStatus, PromotionType } from "@medusajs/utils"
|
import { Modules, PromotionStatus, PromotionType } from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
createAdminUser,
|
createAdminUser,
|
||||||
generatePublishableKey,
|
generatePublishableKey,
|
||||||
generateStoreHeaders,
|
generateStoreHeaders,
|
||||||
} from "../../../../helpers/create-admin-user"
|
} from "../../../../helpers/create-admin-user"
|
||||||
import { medusaTshirtProduct } from "../../../__fixtures__/product"
|
import { medusaTshirtProduct } from "../../../__fixtures__/product"
|
||||||
|
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures/tax"
|
||||||
|
|
||||||
jest.setTimeout(50000)
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
@@ -71,6 +72,8 @@ medusaIntegrationTestRunner({
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||||
|
|
||||||
|
await setupTaxStructure(appContainer.resolve(Modules.TAX))
|
||||||
|
|
||||||
promotion = standardPromotion = (
|
promotion = standardPromotion = (
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/promotions`,
|
`/admin/promotions`,
|
||||||
@@ -629,6 +632,729 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should add tax inclusive promotion to cart successfully in a tax inclusive currency", async () => {
|
||||||
|
const publishableKey = await generatePublishableKey(appContainer)
|
||||||
|
const storeHeaders = generateStoreHeaders({ publishableKey })
|
||||||
|
|
||||||
|
const salesChannel = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/sales-channels",
|
||||||
|
{ name: "Webshop", description: "channel" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.sales_channel
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
"/admin/price-preferences",
|
||||||
|
{
|
||||||
|
attribute: "currency_code",
|
||||||
|
value: "dkk",
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const region = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/regions",
|
||||||
|
{
|
||||||
|
name: "DK",
|
||||||
|
currency_code: "dkk",
|
||||||
|
countries: ["dk"],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.region
|
||||||
|
|
||||||
|
const product = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
{
|
||||||
|
...medusaTshirtProduct,
|
||||||
|
shipping_profile_id: shippingProfile.id,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/promotions`,
|
||||||
|
{
|
||||||
|
code: "FIXED_10",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
status: PromotionStatus.ACTIVE,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
is_automatic: true,
|
||||||
|
application_method: {
|
||||||
|
target_type: "items",
|
||||||
|
type: "fixed",
|
||||||
|
allocation: "across",
|
||||||
|
currency_code: "DKK",
|
||||||
|
value: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.promotion).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
code: "FIXED_10",
|
||||||
|
type: "standard",
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
is_automatic: true,
|
||||||
|
application_method: expect.objectContaining({
|
||||||
|
value: 100,
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
allocation: "across",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts?fields=*items,*items.adjustments`,
|
||||||
|
{
|
||||||
|
currency_code: "dkk",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: region.id,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
promo_codes: [response.data.promotion.code],
|
||||||
|
},
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orignal total -> 1300 DKK (tax incl.)
|
||||||
|
* Tax rate -> 25%
|
||||||
|
* Promotion -> FIXED 100 DKK (tax incl.)
|
||||||
|
*
|
||||||
|
* We want total to be 1300 DKK - 100 DKK = 1200 DKK
|
||||||
|
*/
|
||||||
|
expect(cart).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "dkk",
|
||||||
|
|
||||||
|
subtotal: 1040, // taxable base (item subtotal - discount subtotal) = 1040 - 80 = 960
|
||||||
|
total: 1200, // total = taxable base * (1 + tax rate) = 960 * (1 + 0.25) = 1200
|
||||||
|
tax_total: 240,
|
||||||
|
|
||||||
|
original_total: 1300,
|
||||||
|
original_tax_total: 260,
|
||||||
|
|
||||||
|
discount_total: 100,
|
||||||
|
discount_subtotal: 80,
|
||||||
|
discount_tax_total: 20,
|
||||||
|
|
||||||
|
item_total: 1200,
|
||||||
|
item_subtotal: 1040,
|
||||||
|
item_tax_total: 240,
|
||||||
|
|
||||||
|
original_item_total: 1300,
|
||||||
|
original_item_subtotal: 1040,
|
||||||
|
original_item_tax_total: 260,
|
||||||
|
|
||||||
|
shipping_total: 0,
|
||||||
|
shipping_subtotal: 0,
|
||||||
|
shipping_tax_total: 0,
|
||||||
|
|
||||||
|
original_shipping_tax_total: 0,
|
||||||
|
original_shipping_subtotal: 0,
|
||||||
|
original_shipping_total: 0,
|
||||||
|
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
quantity: 1,
|
||||||
|
unit_price: 1300,
|
||||||
|
|
||||||
|
subtotal: 1040,
|
||||||
|
tax_total: 240,
|
||||||
|
total: 1200,
|
||||||
|
|
||||||
|
original_total: 1300,
|
||||||
|
original_tax_total: 260,
|
||||||
|
|
||||||
|
discount_total: 100,
|
||||||
|
discount_subtotal: 80,
|
||||||
|
discount_tax_total: 20,
|
||||||
|
|
||||||
|
adjustments: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 100,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add tax inclusive promotion to cart successfully in a tax inclusive currency with 2 items and each allocation", async () => {
|
||||||
|
const publishableKey = await generatePublishableKey(appContainer)
|
||||||
|
const storeHeaders = generateStoreHeaders({ publishableKey })
|
||||||
|
|
||||||
|
const salesChannel = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/sales-channels",
|
||||||
|
{ name: "Webshop", description: "channel" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.sales_channel
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
"/admin/price-preferences",
|
||||||
|
{
|
||||||
|
attribute: "currency_code",
|
||||||
|
value: "dkk",
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const region = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/regions",
|
||||||
|
{
|
||||||
|
name: "DK",
|
||||||
|
currency_code: "dkk",
|
||||||
|
countries: ["dk"],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.region
|
||||||
|
|
||||||
|
const product = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
{
|
||||||
|
title: "Discounted Medusa T-Shirt",
|
||||||
|
handle: "discounted-medusa-t-shirt",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Size",
|
||||||
|
values: ["S", "M"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "S",
|
||||||
|
sku: "SHIRT-S",
|
||||||
|
options: {
|
||||||
|
Size: "S",
|
||||||
|
},
|
||||||
|
manage_inventory: false,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 1000,
|
||||||
|
currency_code: "dkk",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "M",
|
||||||
|
sku: "SHIRT-M",
|
||||||
|
options: {
|
||||||
|
Size: "S",
|
||||||
|
},
|
||||||
|
manage_inventory: false,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 500,
|
||||||
|
currency_code: "dkk",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
shipping_profile_id: shippingProfile.id,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/promotions`,
|
||||||
|
{
|
||||||
|
code: "FIXED_10",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
status: PromotionStatus.ACTIVE,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
is_automatic: true,
|
||||||
|
application_method: {
|
||||||
|
target_type: "items",
|
||||||
|
type: "fixed",
|
||||||
|
allocation: "each",
|
||||||
|
currency_code: "DKK",
|
||||||
|
value: 100,
|
||||||
|
max_quantity: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.promotion).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
code: "FIXED_10",
|
||||||
|
type: "standard",
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
is_automatic: true,
|
||||||
|
application_method: expect.objectContaining({
|
||||||
|
value: 100,
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
allocation: "each",
|
||||||
|
max_quantity: 2,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts?fields=*items,*items.adjustments`,
|
||||||
|
{
|
||||||
|
currency_code: "dkk",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: region.id,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant_id: product.variants[1].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
promo_codes: [response.data.promotion.code],
|
||||||
|
},
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orignal total -> 1500 DKK (tax incl.)
|
||||||
|
* Promotion -> FIXED 100 DKK per item (tax incl.)
|
||||||
|
* Tax rate -> 25%
|
||||||
|
*
|
||||||
|
* We want total to be 1500 DKK - 100 DKK - 100 DKK = 1300 DKK
|
||||||
|
*/
|
||||||
|
expect(cart).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "dkk",
|
||||||
|
|
||||||
|
total: 1300,
|
||||||
|
subtotal: 1200, // taxable base (item subtotal - discount subtotal) = 1200 - 200 = 1000
|
||||||
|
tax_total: 260,
|
||||||
|
|
||||||
|
discount_total: 200, // 2 * 100 DKK fixed tax inclusive
|
||||||
|
discount_subtotal: 160,
|
||||||
|
discount_tax_total: 40,
|
||||||
|
|
||||||
|
original_total: 1500,
|
||||||
|
original_tax_total: 300,
|
||||||
|
|
||||||
|
item_total: 1300,
|
||||||
|
item_subtotal: 1200,
|
||||||
|
item_tax_total: 260,
|
||||||
|
|
||||||
|
original_item_total: 1500,
|
||||||
|
original_item_subtotal: 1200,
|
||||||
|
original_item_tax_total: 300,
|
||||||
|
|
||||||
|
shipping_total: 0,
|
||||||
|
shipping_subtotal: 0,
|
||||||
|
shipping_tax_total: 0,
|
||||||
|
|
||||||
|
original_shipping_tax_total: 0,
|
||||||
|
original_shipping_subtotal: 0,
|
||||||
|
original_shipping_total: 0,
|
||||||
|
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
quantity: 1,
|
||||||
|
unit_price: 500,
|
||||||
|
|
||||||
|
subtotal: 400,
|
||||||
|
total: 400, // 400 - 80 = 320 -> 320 * 1.25 = 400
|
||||||
|
tax_total: 80,
|
||||||
|
|
||||||
|
original_total: 500,
|
||||||
|
original_tax_total: 100,
|
||||||
|
|
||||||
|
discount_total: 100,
|
||||||
|
discount_subtotal: 80,
|
||||||
|
discount_tax_total: 20,
|
||||||
|
|
||||||
|
adjustments: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 100,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
quantity: 1,
|
||||||
|
unit_price: 1000,
|
||||||
|
|
||||||
|
subtotal: 800, // 800 - 80 = 720 -> 720 * 1.25 = 900
|
||||||
|
total: 900,
|
||||||
|
tax_total: 180,
|
||||||
|
|
||||||
|
original_total: 1000,
|
||||||
|
original_tax_total: 200,
|
||||||
|
|
||||||
|
discount_total: 100,
|
||||||
|
discount_subtotal: 80,
|
||||||
|
discount_tax_total: 20,
|
||||||
|
|
||||||
|
adjustments: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 100,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add tax exclusive promotion to cart successfully for tax inclusive currency", async () => {
|
||||||
|
const publishableKey = await generatePublishableKey(appContainer)
|
||||||
|
const storeHeaders = generateStoreHeaders({ publishableKey })
|
||||||
|
|
||||||
|
const salesChannel = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/sales-channels",
|
||||||
|
{ name: "Webshop", description: "channel" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.sales_channel
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
"/admin/price-preferences",
|
||||||
|
{
|
||||||
|
attribute: "currency_code",
|
||||||
|
value: "dkk",
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const region = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/regions",
|
||||||
|
{
|
||||||
|
name: "DK",
|
||||||
|
currency_code: "dkk",
|
||||||
|
countries: ["dk"],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.region
|
||||||
|
|
||||||
|
const product = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
{
|
||||||
|
...medusaTshirtProduct,
|
||||||
|
shipping_profile_id: shippingProfile.id,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/promotions`,
|
||||||
|
{
|
||||||
|
code: "FIXED_10",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
status: PromotionStatus.ACTIVE,
|
||||||
|
is_automatic: true,
|
||||||
|
application_method: {
|
||||||
|
target_type: "items",
|
||||||
|
type: "fixed",
|
||||||
|
allocation: "across",
|
||||||
|
currency_code: "DKK",
|
||||||
|
value: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.promotion).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
code: "FIXED_10",
|
||||||
|
type: "standard",
|
||||||
|
is_tax_inclusive: false, // tax exclusive by default
|
||||||
|
is_automatic: true,
|
||||||
|
application_method: expect.objectContaining({
|
||||||
|
value: 100,
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
allocation: "across",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts?fields=*items,*items.adjustments`,
|
||||||
|
{
|
||||||
|
currency_code: "dkk",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: region.id,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
promo_codes: [response.data.promotion.code],
|
||||||
|
},
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orignal total -> 1300 DKK (tax incl.)
|
||||||
|
* Tax rate -> 25%
|
||||||
|
* Promotion -> FIXED 100 DKK (tax exclusive !)
|
||||||
|
*/
|
||||||
|
expect(cart).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "dkk",
|
||||||
|
|
||||||
|
subtotal: 1040, // taxable base (item subtotal - discount subtotal) = 1040 - 100 = 940
|
||||||
|
total: 1175, // total = taxable base * (1 + tax rate) = 940 * (1 + 0.25) = 1175
|
||||||
|
tax_total: 235,
|
||||||
|
|
||||||
|
original_total: 1300,
|
||||||
|
original_tax_total: 260,
|
||||||
|
|
||||||
|
discount_total: 100,
|
||||||
|
discount_subtotal: 100,
|
||||||
|
discount_tax_total: 20,
|
||||||
|
|
||||||
|
item_total: 1175,
|
||||||
|
item_subtotal: 1040,
|
||||||
|
item_tax_total: 235,
|
||||||
|
|
||||||
|
original_item_total: 1300,
|
||||||
|
original_item_subtotal: 1040,
|
||||||
|
original_item_tax_total: 260,
|
||||||
|
|
||||||
|
shipping_total: 0,
|
||||||
|
shipping_subtotal: 0,
|
||||||
|
shipping_tax_total: 0,
|
||||||
|
|
||||||
|
original_shipping_tax_total: 0,
|
||||||
|
original_shipping_subtotal: 0,
|
||||||
|
original_shipping_total: 0,
|
||||||
|
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
quantity: 1,
|
||||||
|
unit_price: 1300,
|
||||||
|
|
||||||
|
subtotal: 1040,
|
||||||
|
tax_total: 235,
|
||||||
|
total: 1175,
|
||||||
|
|
||||||
|
original_total: 1300,
|
||||||
|
original_tax_total: 260,
|
||||||
|
|
||||||
|
discount_total: 100,
|
||||||
|
discount_subtotal: 100,
|
||||||
|
discount_tax_total: 20,
|
||||||
|
|
||||||
|
adjustments: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 100,
|
||||||
|
is_tax_inclusive: false,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add tax exclusive promotion to cart successfully for tax exclusive currency", async () => {
|
||||||
|
const publishableKey = await generatePublishableKey(appContainer)
|
||||||
|
const storeHeaders = generateStoreHeaders({ publishableKey })
|
||||||
|
|
||||||
|
const salesChannel = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/sales-channels",
|
||||||
|
{ name: "Webshop", description: "channel" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.sales_channel
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
"/admin/price-preferences",
|
||||||
|
{
|
||||||
|
attribute: "currency_code",
|
||||||
|
value: "dkk",
|
||||||
|
is_tax_inclusive: false,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const region = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/regions",
|
||||||
|
{
|
||||||
|
name: "DK",
|
||||||
|
currency_code: "dkk",
|
||||||
|
countries: ["dk"],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.region
|
||||||
|
|
||||||
|
const product = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
{
|
||||||
|
...medusaTshirtProduct,
|
||||||
|
shipping_profile_id: shippingProfile.id,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/promotions`,
|
||||||
|
{
|
||||||
|
code: "FIXED_10",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
status: PromotionStatus.ACTIVE,
|
||||||
|
is_automatic: true,
|
||||||
|
application_method: {
|
||||||
|
target_type: "items",
|
||||||
|
type: "fixed",
|
||||||
|
allocation: "across",
|
||||||
|
currency_code: "DKK",
|
||||||
|
value: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.promotion).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
code: "FIXED_10",
|
||||||
|
type: "standard",
|
||||||
|
is_tax_inclusive: false, // tax exclusive by default
|
||||||
|
is_automatic: true,
|
||||||
|
application_method: expect.objectContaining({
|
||||||
|
value: 100,
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
allocation: "across",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts?fields=*items,*items.adjustments`,
|
||||||
|
{
|
||||||
|
currency_code: "dkk",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: region.id,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
promo_codes: [response.data.promotion.code],
|
||||||
|
},
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orignal total -> 1300 DKK (tax excl.)
|
||||||
|
* Tax rate -> 25%
|
||||||
|
* Promotion -> FIXED 100 DKK (tax exclusive !)
|
||||||
|
*/
|
||||||
|
expect(cart).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "dkk",
|
||||||
|
|
||||||
|
subtotal: 1300, // taxable base (item subtotal - discount subtotal) = 1300 - 100 = 1200
|
||||||
|
total: 1500, // total = taxable base * (1 + tax rate) = 1200 * (1 + 0.25) = 1500
|
||||||
|
tax_total: 300,
|
||||||
|
|
||||||
|
original_total: 1625,
|
||||||
|
original_tax_total: 325,
|
||||||
|
|
||||||
|
discount_total: 125,
|
||||||
|
discount_subtotal: 100,
|
||||||
|
discount_tax_total: 25,
|
||||||
|
|
||||||
|
item_total: 1500,
|
||||||
|
item_subtotal: 1300,
|
||||||
|
item_tax_total: 300,
|
||||||
|
|
||||||
|
original_item_total: 1625,
|
||||||
|
original_item_subtotal: 1300,
|
||||||
|
original_item_tax_total: 325,
|
||||||
|
|
||||||
|
shipping_total: 0,
|
||||||
|
shipping_subtotal: 0,
|
||||||
|
shipping_tax_total: 0,
|
||||||
|
|
||||||
|
original_shipping_tax_total: 0,
|
||||||
|
original_shipping_subtotal: 0,
|
||||||
|
original_shipping_total: 0,
|
||||||
|
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
quantity: 1,
|
||||||
|
unit_price: 1300,
|
||||||
|
|
||||||
|
subtotal: 1300,
|
||||||
|
total: 1500,
|
||||||
|
tax_total: 300,
|
||||||
|
|
||||||
|
discount_total: 125,
|
||||||
|
discount_subtotal: 100,
|
||||||
|
discount_tax_total: 25,
|
||||||
|
|
||||||
|
original_total: 1625,
|
||||||
|
original_tax_total: 325,
|
||||||
|
|
||||||
|
adjustments: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 100,
|
||||||
|
is_tax_inclusive: false,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("DELETE /admin/promotions/:id", () => {
|
describe("DELETE /admin/promotions/:id", () => {
|
||||||
|
|||||||
@@ -7362,6 +7362,9 @@
|
|||||||
"clearAll": {
|
"clearAll": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"taxInclusive": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"amount": {
|
"amount": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -7428,6 +7431,7 @@
|
|||||||
"allocation",
|
"allocation",
|
||||||
"addCondition",
|
"addCondition",
|
||||||
"clearAll",
|
"clearAll",
|
||||||
|
"taxInclusive",
|
||||||
"amount",
|
"amount",
|
||||||
"conditions"
|
"conditions"
|
||||||
],
|
],
|
||||||
@@ -7626,6 +7630,19 @@
|
|||||||
"required": ["existing", "new", "none"],
|
"required": ["existing", "new", "none"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"taxInclusive": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["title", "description"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -7852,6 +7869,7 @@
|
|||||||
"and",
|
"and",
|
||||||
"selectAttribute",
|
"selectAttribute",
|
||||||
"campaign",
|
"campaign",
|
||||||
|
"taxInclusive",
|
||||||
"status",
|
"status",
|
||||||
"method",
|
"method",
|
||||||
"max_quantity",
|
"max_quantity",
|
||||||
|
|||||||
@@ -1969,6 +1969,7 @@
|
|||||||
"allocation": "Allocation",
|
"allocation": "Allocation",
|
||||||
"addCondition": "Add condition",
|
"addCondition": "Add condition",
|
||||||
"clearAll": "Clear all",
|
"clearAll": "Clear all",
|
||||||
|
"taxInclusive": "Tax Inclusive",
|
||||||
"amount": {
|
"amount": {
|
||||||
"tooltip": "Select the currency code to enable setting the amount"
|
"tooltip": "Select the currency code to enable setting the amount"
|
||||||
},
|
},
|
||||||
@@ -2045,6 +2046,10 @@
|
|||||||
"description": "Proceed without associating promotion with campaign"
|
"description": "Proceed without associating promotion with campaign"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"taxInclusive": {
|
||||||
|
"title": "Does promotion include taxes?",
|
||||||
|
"description": "Whether the promotion will be applied before or after taxes"
|
||||||
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"label": "Status",
|
"label": "Status",
|
||||||
"draft": {
|
"draft": {
|
||||||
|
|||||||
+47
@@ -19,6 +19,7 @@ import {
|
|||||||
ProgressStatus,
|
ProgressStatus,
|
||||||
ProgressTabs,
|
ProgressTabs,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
toast,
|
toast,
|
||||||
} from "@medusajs/ui"
|
} from "@medusajs/ui"
|
||||||
@@ -52,6 +53,7 @@ const defaultValues = {
|
|||||||
type: "standard" as PromotionTypeValues,
|
type: "standard" as PromotionTypeValues,
|
||||||
status: "draft" as PromotionStatusValues,
|
status: "draft" as PromotionStatusValues,
|
||||||
rules: [],
|
rules: [],
|
||||||
|
is_tax_inclusive: false,
|
||||||
application_method: {
|
application_method: {
|
||||||
allocation: "each" as ApplicationMethodAllocationValues,
|
allocation: "each" as ApplicationMethodAllocationValues,
|
||||||
type: "fixed" as ApplicationMethodTypeValues,
|
type: "fixed" as ApplicationMethodTypeValues,
|
||||||
@@ -89,6 +91,7 @@ export const CreatePromotionForm = () => {
|
|||||||
const {
|
const {
|
||||||
campaign_choice: _campaignChoice,
|
campaign_choice: _campaignChoice,
|
||||||
is_automatic,
|
is_automatic,
|
||||||
|
is_tax_inclusive,
|
||||||
template_id: _templateId,
|
template_id: _templateId,
|
||||||
application_method,
|
application_method,
|
||||||
rules,
|
rules,
|
||||||
@@ -142,6 +145,7 @@ export const CreatePromotionForm = () => {
|
|||||||
target_rules: buildRulesData(targetRulesData),
|
target_rules: buildRulesData(targetRulesData),
|
||||||
buy_rules: buildRulesData(buyRulesData),
|
buy_rules: buildRulesData(buyRulesData),
|
||||||
},
|
},
|
||||||
|
is_tax_inclusive,
|
||||||
is_automatic: is_automatic === "true",
|
is_automatic: is_automatic === "true",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -583,6 +587,49 @@ export const CreatePromotionForm = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!currentTemplate?.hiddenFields?.includes(
|
||||||
|
"is_tax_inclusive"
|
||||||
|
) && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<div className="flex gap-x-2 gap-y-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="is_tax_inclusive"
|
||||||
|
render={({
|
||||||
|
field: { onChange, value, ...field },
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Form.Item className="basis-full">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="block">
|
||||||
|
<Form.Label>
|
||||||
|
{t("promotions.form.taxInclusive.title")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Hint className="!mt-1">
|
||||||
|
{t(
|
||||||
|
"promotions.form.taxInclusive.description"
|
||||||
|
)}
|
||||||
|
</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<Form.Control className="mr-2 self-center">
|
||||||
|
<Switch
|
||||||
|
className="mt-[2px]"
|
||||||
|
checked={!!value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{!currentTemplate?.hiddenFields?.includes("type") && (
|
{!currentTemplate?.hiddenFields?.includes("type") && (
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
+1
@@ -27,6 +27,7 @@ export const CreatePromotionSchema = z
|
|||||||
type: z.enum(["buyget", "standard"]),
|
type: z.enum(["buyget", "standard"]),
|
||||||
status: z.enum(["draft", "active", "inactive"]),
|
status: z.enum(["draft", "active", "inactive"]),
|
||||||
rules: RuleSchema,
|
rules: RuleSchema,
|
||||||
|
is_tax_inclusive: z.boolean().optional(),
|
||||||
application_method: z.object({
|
application_method: z.object({
|
||||||
allocation: z.enum(["each", "across"]),
|
allocation: z.enum(["each", "across"]),
|
||||||
value: z.number().min(0),
|
value: z.number().min(0),
|
||||||
|
|||||||
+5
-1
@@ -74,7 +74,11 @@ export const templates = [
|
|||||||
type: "buy_get",
|
type: "buy_get",
|
||||||
title: "Buy X Get Y",
|
title: "Buy X Get Y",
|
||||||
description: "Buy X product(s), get Y product(s)",
|
description: "Buy X product(s), get Y product(s)",
|
||||||
hiddenFields: [...commonHiddenFields, "application_method.value"],
|
hiddenFields: [
|
||||||
|
...commonHiddenFields,
|
||||||
|
"application_method.value",
|
||||||
|
"is_tax_inclusive",
|
||||||
|
],
|
||||||
defaults: {
|
defaults: {
|
||||||
is_automatic: "false",
|
is_automatic: "false",
|
||||||
type: "buyget",
|
type: "buyget",
|
||||||
|
|||||||
+12
@@ -180,6 +180,18 @@ export const PromotionGeneralSection = ({
|
|||||||
{promotion.application_method?.allocation!}
|
{promotion.application_method?.allocation!}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("promotions.fields.taxInclusive")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<Text className="inline" size="small" leading="compact">
|
||||||
|
{promotion.is_tax_inclusive ? t("fields.true") : t("fields.false")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -132,6 +132,7 @@ export const prepareAdjustmentsFromPromotionActionsStep = createStep(
|
|||||||
.map((action) => ({
|
.map((action) => ({
|
||||||
code: action.code,
|
code: action.code,
|
||||||
amount: (action as AddItemAdjustmentAction).amount,
|
amount: (action as AddItemAdjustmentAction).amount,
|
||||||
|
is_tax_inclusive: (action as AddItemAdjustmentAction).is_tax_inclusive,
|
||||||
item_id: (action as AddItemAdjustmentAction).item_id,
|
item_id: (action as AddItemAdjustmentAction).item_id,
|
||||||
promotion_id: promotionsMap.get(action.code)?.id,
|
promotion_id: promotionsMap.get(action.code)?.id,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ export interface AdjustmentLineDTO {
|
|||||||
*/
|
*/
|
||||||
amount: BigNumberValue
|
amount: BigNumberValue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the adjustment is tax inclusive.
|
||||||
|
*/
|
||||||
|
is_tax_inclusive?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The raw amount to adjust the original amount with.
|
* The raw amount to adjust the original amount with.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -236,6 +236,11 @@ export interface CreateAdjustmentDTO {
|
|||||||
*/
|
*/
|
||||||
amount: BigNumberInput
|
amount: BigNumberInput
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the adjustment amount includes tax.
|
||||||
|
*/
|
||||||
|
is_tax_inclusive?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The description of the adjustment.
|
* The description of the adjustment.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { AdminCreateCampaign } from "../../campaign"
|
|||||||
|
|
||||||
export interface AdminCreatePromotionRule {
|
export interface AdminCreatePromotionRule {
|
||||||
/**
|
/**
|
||||||
* The operator used to check whether the buy rule applies on a cart.
|
* The operator used to check whether the buy rule applies on a cart.
|
||||||
* For example, `eq` means that the cart's value for the specified attribute
|
* For example, `eq` means that the cart's value for the specified attribute
|
||||||
* must match the specified value.
|
* must match the specified value.
|
||||||
*/
|
*/
|
||||||
operator: PromotionRuleOperatorValues
|
operator: PromotionRuleOperatorValues
|
||||||
@@ -21,14 +21,14 @@ export interface AdminCreatePromotionRule {
|
|||||||
description?: string | null
|
description?: string | null
|
||||||
/**
|
/**
|
||||||
* The attribute to compare against when checking whether a promotion can be applied on a cart.
|
* The attribute to compare against when checking whether a promotion can be applied on a cart.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* items.product_id
|
* items.product_id
|
||||||
*/
|
*/
|
||||||
attribute: string
|
attribute: string
|
||||||
/**
|
/**
|
||||||
* The value to compare against when checking whether a promotion can be applied on a cart.
|
* The value to compare against when checking whether a promotion can be applied on a cart.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* prod_123
|
* prod_123
|
||||||
*/
|
*/
|
||||||
@@ -54,7 +54,7 @@ export interface AdminCreateApplicationMethod {
|
|||||||
value: number
|
value: number
|
||||||
/**
|
/**
|
||||||
* The currency code of the application method.
|
* The currency code of the application method.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* usd
|
* usd
|
||||||
*/
|
*/
|
||||||
@@ -68,12 +68,12 @@ export interface AdminCreateApplicationMethod {
|
|||||||
*/
|
*/
|
||||||
type: ApplicationMethodTypeValues
|
type: ApplicationMethodTypeValues
|
||||||
/**
|
/**
|
||||||
* The target type of the application method indicating whether the associated promotion is applied
|
* The target type of the application method indicating whether the associated promotion is applied
|
||||||
* to the cart's items, shipping methods, or the whole order.
|
* to the cart's items, shipping methods, or the whole order.
|
||||||
*/
|
*/
|
||||||
target_type: ApplicationMethodTargetTypeValues
|
target_type: ApplicationMethodTargetTypeValues
|
||||||
/**
|
/**
|
||||||
* The allocation value that indicates whether the associated promotion is applied on each
|
* The allocation value that indicates whether the associated promotion is applied on each
|
||||||
* item in a cart or split between the items in the cart.
|
* item in a cart or split between the items in the cart.
|
||||||
*/
|
*/
|
||||||
allocation?: ApplicationMethodAllocationValues
|
allocation?: ApplicationMethodAllocationValues
|
||||||
@@ -90,7 +90,7 @@ export interface AdminCreateApplicationMethod {
|
|||||||
*/
|
*/
|
||||||
apply_to_quantity?: number | null
|
apply_to_quantity?: number | null
|
||||||
/**
|
/**
|
||||||
* The minimum quantity required for a `buyget` promotion to be applied. For example,
|
* The minimum quantity required for a `buyget` promotion to be applied. For example,
|
||||||
* if the promotion is a "Buy 2 shirts get 1 free", the value of this attribute is 2.
|
* if the promotion is a "Buy 2 shirts get 1 free", the value of this attribute is 2.
|
||||||
*/
|
*/
|
||||||
buy_rules_min_quantity?: number | null
|
buy_rules_min_quantity?: number | null
|
||||||
@@ -111,7 +111,7 @@ export interface AdminUpdateApplicationMethod {
|
|||||||
max_quantity?: number | null
|
max_quantity?: number | null
|
||||||
/**
|
/**
|
||||||
* The currency code of the application method.
|
* The currency code of the application method.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* usd
|
* usd
|
||||||
*/
|
*/
|
||||||
@@ -121,12 +121,12 @@ export interface AdminUpdateApplicationMethod {
|
|||||||
*/
|
*/
|
||||||
type?: ApplicationMethodTypeValues
|
type?: ApplicationMethodTypeValues
|
||||||
/**
|
/**
|
||||||
* The target type of the application method indicating whether the associated promotion is applied
|
* The target type of the application method indicating whether the associated promotion is applied
|
||||||
* to the cart's items, shipping methods, or the whole order.
|
* to the cart's items, shipping methods, or the whole order.
|
||||||
*/
|
*/
|
||||||
target_type?: ApplicationMethodTargetTypeValues
|
target_type?: ApplicationMethodTargetTypeValues
|
||||||
/**
|
/**
|
||||||
* The allocation value that indicates whether the associated promotion is applied on each
|
* The allocation value that indicates whether the associated promotion is applied on each
|
||||||
* item in a cart or split between the items in the cart.
|
* item in a cart or split between the items in the cart.
|
||||||
*/
|
*/
|
||||||
allocation?: ApplicationMethodAllocationValues
|
allocation?: ApplicationMethodAllocationValues
|
||||||
@@ -143,7 +143,7 @@ export interface AdminUpdateApplicationMethod {
|
|||||||
*/
|
*/
|
||||||
apply_to_quantity?: number | null
|
apply_to_quantity?: number | null
|
||||||
/**
|
/**
|
||||||
* The minimum quantity required for a `buyget` promotion to be applied. For example,
|
* The minimum quantity required for a `buyget` promotion to be applied. For example,
|
||||||
* if the promotion is a "Buy 2 shirts get 1 free", the value of this attribute is 2.
|
* if the promotion is a "Buy 2 shirts get 1 free", the value of this attribute is 2.
|
||||||
*/
|
*/
|
||||||
buy_rules_min_quantity?: number | null
|
buy_rules_min_quantity?: number | null
|
||||||
@@ -155,11 +155,15 @@ export interface AdminCreatePromotion {
|
|||||||
*/
|
*/
|
||||||
code: string
|
code: string
|
||||||
/**
|
/**
|
||||||
* Whether the promotion is applied automatically
|
* Whether the promotion is applied automatically
|
||||||
* or requires the customer to manually apply it
|
* or requires the customer to manually apply it
|
||||||
* by entering the code at checkout.
|
* by entering the code at checkout.
|
||||||
*/
|
*/
|
||||||
is_automatic?: boolean
|
is_automatic?: boolean
|
||||||
|
/**
|
||||||
|
* Whether the promotion is tax inclusive.
|
||||||
|
*/
|
||||||
|
is_tax_inclusive?: boolean
|
||||||
/**
|
/**
|
||||||
* The type of promotion.
|
* The type of promotion.
|
||||||
*/
|
*/
|
||||||
@@ -188,7 +192,7 @@ export interface AdminUpdatePromotion {
|
|||||||
*/
|
*/
|
||||||
code?: string
|
code?: string
|
||||||
/**
|
/**
|
||||||
* Whether the promotion is applied automatically
|
* Whether the promotion is applied automatically
|
||||||
* or requires the customer to manually apply it
|
* or requires the customer to manually apply it
|
||||||
* by entering the code at checkout.
|
* by entering the code at checkout.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -19,23 +19,23 @@ export interface BasePromotionRule {
|
|||||||
description?: string | null
|
description?: string | null
|
||||||
/**
|
/**
|
||||||
* The attribute to compare against when checking whether a promotion can be applied on a cart.
|
* The attribute to compare against when checking whether a promotion can be applied on a cart.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* items.product_id
|
* items.product_id
|
||||||
*/
|
*/
|
||||||
attribute?: string
|
attribute?: string
|
||||||
/**
|
/**
|
||||||
* The operator used to check whether the buy rule applies on a cart.
|
* The operator used to check whether the buy rule applies on a cart.
|
||||||
* For example, `eq` means that the cart's value for the specified attribute
|
* For example, `eq` means that the cart's value for the specified attribute
|
||||||
* must match the specified value.
|
* must match the specified value.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* eq
|
* eq
|
||||||
*/
|
*/
|
||||||
operator?: PromotionRuleOperatorValues
|
operator?: PromotionRuleOperatorValues
|
||||||
/**
|
/**
|
||||||
* The values to compare against when checking whether a promotion can be applied on a cart.
|
* The values to compare against when checking whether a promotion can be applied on a cart.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* prod_123
|
* prod_123
|
||||||
*/
|
*/
|
||||||
@@ -62,6 +62,7 @@ export interface BasePromotion {
|
|||||||
code?: string
|
code?: string
|
||||||
type?: PromotionTypeValues
|
type?: PromotionTypeValues
|
||||||
is_automatic?: boolean
|
is_automatic?: boolean
|
||||||
|
is_tax_inclusive?: boolean
|
||||||
application_method?: BaseApplicationMethod
|
application_method?: BaseApplicationMethod
|
||||||
rules?: BasePromotionRule[]
|
rules?: BasePromotionRule[]
|
||||||
status?: PromotionStatusValues
|
status?: PromotionStatusValues
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ export interface AddItemAdjustmentAction {
|
|||||||
*/
|
*/
|
||||||
amount: BigNumberInput
|
amount: BigNumberInput
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the adjustment amount includes tax.
|
||||||
|
*/
|
||||||
|
is_tax_inclusive?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
/**
|
/**
|
||||||
* The promotion's code.
|
* The promotion's code.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ export interface PromotionDTO {
|
|||||||
*/
|
*/
|
||||||
is_automatic?: boolean
|
is_automatic?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the promotion is tax inclusive.
|
||||||
|
*/
|
||||||
|
is_tax_inclusive?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The associated application method.
|
* The associated application method.
|
||||||
*/
|
*/
|
||||||
@@ -113,6 +118,11 @@ export interface CreatePromotionDTO {
|
|||||||
*/
|
*/
|
||||||
is_automatic?: boolean
|
is_automatic?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the promotion is tax inclusive.
|
||||||
|
*/
|
||||||
|
is_tax_inclusive?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The associated application method.
|
* The associated application method.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -557,6 +557,96 @@ describe("Total calculation", function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should calculate tax inclusive carts with items + taxes with tax inclusive adjustments", function () {
|
||||||
|
/**
|
||||||
|
* TAX INCLUSIVE CART
|
||||||
|
*
|
||||||
|
* Total price -> 120 tax inclusive
|
||||||
|
* Fixed discount -> 10 tax inclusive
|
||||||
|
* Tax rate -> 20%
|
||||||
|
*/
|
||||||
|
|
||||||
|
const cart = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
unit_price: 60,
|
||||||
|
quantity: 2,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
adjustments: [
|
||||||
|
{
|
||||||
|
amount: 10,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tax_lines: [
|
||||||
|
{
|
||||||
|
rate: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialized = JSON.parse(JSON.stringify(decorateCartTotals(cart)))
|
||||||
|
|
||||||
|
expect(serialized).toEqual({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
unit_price: 60,
|
||||||
|
quantity: 2,
|
||||||
|
subtotal: 100,
|
||||||
|
tax_total: 18.333333333333332,
|
||||||
|
total: 110,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
|
||||||
|
original_total: 120,
|
||||||
|
original_tax_total: 20,
|
||||||
|
|
||||||
|
discount_subtotal: 8.333333333333334,
|
||||||
|
discount_tax_total: 1.6666666666666667,
|
||||||
|
discount_total: 10,
|
||||||
|
|
||||||
|
tax_lines: [
|
||||||
|
{
|
||||||
|
rate: 20,
|
||||||
|
total: 18.333333333333332,
|
||||||
|
subtotal: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
adjustments: [
|
||||||
|
{
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
amount: 10, // <- amount is tax inclusive so it's equal to total
|
||||||
|
subtotal: 8.333333333333334,
|
||||||
|
total: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
subtotal: 100,
|
||||||
|
tax_total: 18.333333333333332,
|
||||||
|
total: 110, // total is 120 - 10 tax inclusive discount
|
||||||
|
|
||||||
|
original_item_subtotal: 100,
|
||||||
|
original_item_tax_total: 20,
|
||||||
|
original_item_total: 120,
|
||||||
|
original_tax_total: 20,
|
||||||
|
original_total: 120,
|
||||||
|
|
||||||
|
discount_subtotal: 8.333333333333334,
|
||||||
|
discount_tax_total: 1.6666666666666667,
|
||||||
|
discount_total: 10,
|
||||||
|
|
||||||
|
item_subtotal: 100,
|
||||||
|
item_tax_total: 18.333333333333332,
|
||||||
|
item_total: 110,
|
||||||
|
|
||||||
|
credit_line_subtotal: 0,
|
||||||
|
credit_line_tax_total: 0,
|
||||||
|
credit_line_total: 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("should calculate carts with items + taxes + adjustments + shipping methods", function () {
|
it("should calculate carts with items + taxes + adjustments + shipping methods", function () {
|
||||||
const cart = {
|
const cart = {
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export function calculateAdjustmentTotal({
|
|||||||
includesTax,
|
includesTax,
|
||||||
taxRate,
|
taxRate,
|
||||||
}: {
|
}: {
|
||||||
adjustments: Pick<AdjustmentLineDTO, "amount">[]
|
adjustments: Pick<AdjustmentLineDTO, "amount" | "is_tax_inclusive">[]
|
||||||
includesTax?: boolean
|
includesTax?: boolean
|
||||||
taxRate?: BigNumberInput
|
taxRate?: BigNumberInput
|
||||||
}) {
|
}) {
|
||||||
@@ -25,7 +25,15 @@ export function calculateAdjustmentTotal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const adjustmentAmount = MathBN.convert(adj.amount)
|
const adjustmentAmount = MathBN.convert(adj.amount)
|
||||||
adjustmentsSubtotal = MathBN.add(adjustmentsSubtotal, adjustmentAmount)
|
|
||||||
|
if (adj.is_tax_inclusive && isDefined(taxRate)) {
|
||||||
|
adjustmentsSubtotal = MathBN.add(
|
||||||
|
adjustmentsSubtotal,
|
||||||
|
MathBN.div(adjustmentAmount, MathBN.add(1, taxRate))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
adjustmentsSubtotal = MathBN.add(adjustmentsSubtotal, adjustmentAmount)
|
||||||
|
}
|
||||||
|
|
||||||
if (isDefined(taxRate)) {
|
if (isDefined(taxRate)) {
|
||||||
const adjustmentSubtotal = includesTax
|
const adjustmentSubtotal = includesTax
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export interface DecorateCartLikeInputDTO {
|
|||||||
unit_price: BigNumberInput
|
unit_price: BigNumberInput
|
||||||
is_tax_inclusive?: boolean
|
is_tax_inclusive?: boolean
|
||||||
quantity: BigNumberInput
|
quantity: BigNumberInput
|
||||||
adjustments?: { amount: BigNumberInput }[]
|
adjustments?: { amount: BigNumberInput; is_tax_inclusive?: boolean }[]
|
||||||
tax_lines?: {
|
tax_lines?: {
|
||||||
rate: BigNumberInput
|
rate: BigNumberInput
|
||||||
}[]
|
}[]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export const defaultAdminPromotionFields = [
|
|||||||
"id",
|
"id",
|
||||||
"code",
|
"code",
|
||||||
"is_automatic",
|
"is_automatic",
|
||||||
|
"is_tax_inclusive",
|
||||||
"type",
|
"type",
|
||||||
"status",
|
"status",
|
||||||
"created_at",
|
"created_at",
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export const CreatePromotion = z
|
|||||||
code: z.string(),
|
code: z.string(),
|
||||||
is_automatic: z.boolean().optional(),
|
is_automatic: z.boolean().optional(),
|
||||||
type: z.nativeEnum(PromotionType),
|
type: z.nativeEnum(PromotionType),
|
||||||
|
is_tax_inclusive: z.boolean().optional(),
|
||||||
status: z.nativeEnum(PromotionStatus).default(PromotionStatus.DRAFT),
|
status: z.nativeEnum(PromotionStatus).default(PromotionStatus.DRAFT),
|
||||||
campaign_id: z.string().nullish(),
|
campaign_id: z.string().nullish(),
|
||||||
campaign: CreateCampaign.optional(),
|
campaign: CreateCampaign.optional(),
|
||||||
|
|||||||
@@ -2913,6 +2913,7 @@ moduleIntegrationTestRunner<ICartModuleService>({
|
|||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
item_id: expect.any(String),
|
item_id: expect.any(String),
|
||||||
|
is_tax_inclusive: false,
|
||||||
promotion_id: null,
|
promotion_id: null,
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
@@ -3020,6 +3021,7 @@ moduleIntegrationTestRunner<ICartModuleService>({
|
|||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
item_id: expect.any(String),
|
item_id: expect.any(String),
|
||||||
|
is_tax_inclusive: false,
|
||||||
promotion_id: null,
|
promotion_id: null,
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
amount: 200,
|
amount: 200,
|
||||||
|
|||||||
@@ -998,6 +998,16 @@
|
|||||||
"nullable": false,
|
"nullable": false,
|
||||||
"mappedType": "decimal"
|
"mappedType": "decimal"
|
||||||
},
|
},
|
||||||
|
"is_tax_inclusive": {
|
||||||
|
"name": "is_tax_inclusive",
|
||||||
|
"type": "boolean",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"default": "false",
|
||||||
|
"mappedType": "boolean"
|
||||||
|
},
|
||||||
"provider_id": {
|
"provider_id": {
|
||||||
"name": "provider_id",
|
"name": "provider_id",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Migration } from '@mikro-orm/migrations';
|
||||||
|
|
||||||
|
export class Migration20250508081553 extends Migration {
|
||||||
|
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
this.addSql(`alter table if exists "cart_line_item_adjustment" add column if not exists "is_tax_inclusive" boolean not null default false;`);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async down(): Promise<void> {
|
||||||
|
this.addSql(`alter table if exists "cart_line_item_adjustment" drop column if exists "is_tax_inclusive";`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ const LineItemAdjustment = model
|
|||||||
description: model.text().nullable(),
|
description: model.text().nullable(),
|
||||||
code: model.text().nullable(),
|
code: model.text().nullable(),
|
||||||
amount: model.bigNumber(),
|
amount: model.bigNumber(),
|
||||||
|
is_tax_inclusive: model.boolean().default(false),
|
||||||
provider_id: model.text().nullable(),
|
provider_id: model.text().nullable(),
|
||||||
promotion_id: model.text().nullable(),
|
promotion_id: model.text().nullable(),
|
||||||
metadata: model.json().nullable(),
|
metadata: model.json().nullable(),
|
||||||
|
|||||||
+58
@@ -189,12 +189,14 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 100,
|
amount: 100,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 150,
|
amount: 150,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -326,18 +328,21 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 50,
|
amount: 50,
|
||||||
code: "PROMOTION_TEST_2",
|
code: "PROMOTION_TEST_2",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 50,
|
amount: 50,
|
||||||
code: "PROMOTION_TEST_2",
|
code: "PROMOTION_TEST_2",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 30,
|
amount: 30,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -434,12 +439,14 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 50,
|
amount: 50,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 150,
|
amount: 150,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -497,6 +504,7 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 500,
|
amount: 500,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -624,12 +632,14 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 10,
|
amount: 10,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 15,
|
amount: 15,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -726,24 +736,28 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 30,
|
amount: 30,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 45,
|
amount: 45,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 5,
|
amount: 5,
|
||||||
code: "PROMOTION_TEST_2",
|
code: "PROMOTION_TEST_2",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 10.5,
|
amount: 10.5,
|
||||||
code: "PROMOTION_TEST_2",
|
code: "PROMOTION_TEST_2",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -813,12 +827,14 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 50,
|
amount: 50,
|
||||||
code: "PROMO_PERCENTAGE_1",
|
code: "PROMO_PERCENTAGE_1",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 50,
|
amount: 50,
|
||||||
code: "PROMO_PERCENTAGE_2",
|
code: "PROMO_PERCENTAGE_2",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -915,12 +931,14 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 50,
|
amount: 50,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 150,
|
amount: 150,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -1103,12 +1121,14 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 100,
|
amount: 100,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 300,
|
amount: 300,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -1177,12 +1197,14 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 100,
|
amount: 100,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 300,
|
amount: 300,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -1278,24 +1300,28 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 12.5,
|
amount: 12.5,
|
||||||
code: "PROMOTION_TEST_2",
|
code: "PROMOTION_TEST_2",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 37.5,
|
amount: 37.5,
|
||||||
code: "PROMOTION_TEST_2",
|
code: "PROMOTION_TEST_2",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 7.5,
|
amount: 7.5,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 22.5,
|
amount: 22.5,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -1391,12 +1417,14 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 50,
|
amount: 50,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 150,
|
amount: 150,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -1574,12 +1602,14 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 20,
|
amount: 20,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 60,
|
amount: 60,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -1648,12 +1678,14 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 20,
|
amount: 20,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 60,
|
amount: 60,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -1748,24 +1780,28 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 5,
|
amount: 5,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 15,
|
amount: 15,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 4.5,
|
amount: 4.5,
|
||||||
code: "PROMOTION_TEST_2",
|
code: "PROMOTION_TEST_2",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 13.5,
|
amount: 13.5,
|
||||||
code: "PROMOTION_TEST_2",
|
code: "PROMOTION_TEST_2",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -1838,24 +1874,28 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 150,
|
amount: 150,
|
||||||
code: "PROMO_PERCENTAGE_1",
|
code: "PROMO_PERCENTAGE_1",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_wool_tshirt",
|
item_id: "item_wool_tshirt",
|
||||||
amount: 50,
|
amount: 50,
|
||||||
code: "PROMO_PERCENTAGE_1",
|
code: "PROMO_PERCENTAGE_1",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 75,
|
amount: 75,
|
||||||
code: "PROMO_PERCENTAGE_2",
|
code: "PROMO_PERCENTAGE_2",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_wool_tshirt",
|
item_id: "item_wool_tshirt",
|
||||||
amount: 25,
|
amount: 25,
|
||||||
code: "PROMO_PERCENTAGE_2",
|
code: "PROMO_PERCENTAGE_2",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -1914,12 +1954,14 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 300,
|
amount: 300,
|
||||||
code: "PROMO_PERCENTAGE_3",
|
code: "PROMO_PERCENTAGE_3",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_wool_tshirt",
|
item_id: "item_wool_tshirt",
|
||||||
amount: 100,
|
amount: 100,
|
||||||
code: "PROMO_PERCENTAGE_3",
|
code: "PROMO_PERCENTAGE_3",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -2014,24 +2056,28 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 5,
|
amount: 5,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 15,
|
amount: 15,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 4.5,
|
amount: 4.5,
|
||||||
code: "PROMOTION_TEST_2",
|
code: "PROMOTION_TEST_2",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 13.5,
|
amount: 13.5,
|
||||||
code: "PROMOTION_TEST_2",
|
code: "PROMOTION_TEST_2",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -4174,12 +4220,14 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 50,
|
amount: 50,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 150,
|
amount: 150,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -4241,12 +4289,14 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 50,
|
amount: 50,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 150,
|
amount: 150,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -4329,24 +4379,28 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 12.5,
|
amount: 12.5,
|
||||||
code: "PROMOTION_TEST_2",
|
code: "PROMOTION_TEST_2",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 37.5,
|
amount: 37.5,
|
||||||
code: "PROMOTION_TEST_2",
|
code: "PROMOTION_TEST_2",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 7.5,
|
amount: 7.5,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 22.5,
|
amount: 22.5,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -4429,12 +4483,14 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 50,
|
amount: 50,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 150,
|
amount: 150,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -4519,12 +4575,14 @@ moduleIntegrationTestRunner({
|
|||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 100,
|
amount: 100,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 150,
|
amount: 150,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
|
is_tax_inclusive: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -332,6 +332,16 @@
|
|||||||
"default": "false",
|
"default": "false",
|
||||||
"mappedType": "boolean"
|
"mappedType": "boolean"
|
||||||
},
|
},
|
||||||
|
"is_tax_inclusive": {
|
||||||
|
"name": "is_tax_inclusive",
|
||||||
|
"type": "boolean",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"default": "false",
|
||||||
|
"mappedType": "boolean"
|
||||||
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"name": "type",
|
"name": "type",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Migration } from "@mikro-orm/migrations"
|
||||||
|
|
||||||
|
export class Migration20250508081510 extends Migration {
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`alter table if exists "promotion" add column if not exists "is_tax_inclusive" boolean not null default false;`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override async down(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`alter table if exists "promotion" drop column if exists "is_tax_inclusive";`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ const Promotion = model
|
|||||||
id: model.id({ prefix: "promo" }).primaryKey(),
|
id: model.id({ prefix: "promo" }).primaryKey(),
|
||||||
code: model.text().searchable(),
|
code: model.text().searchable(),
|
||||||
is_automatic: model.boolean().default(false),
|
is_automatic: model.boolean().default(false),
|
||||||
|
is_tax_inclusive: model.boolean().default(false),
|
||||||
type: model.enum(PromotionUtils.PromotionType).index("IDX_promotion_type"),
|
type: model.enum(PromotionUtils.PromotionType).index("IDX_promotion_type"),
|
||||||
status: model
|
status: model
|
||||||
.enum(PromotionUtils.PromotionStatus)
|
.enum(PromotionUtils.PromotionStatus)
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ function applyPromotionToItems(
|
|||||||
item_id: item.id,
|
item_id: item.id,
|
||||||
amount,
|
amount,
|
||||||
code: promotion.code!,
|
code: promotion.code!,
|
||||||
|
is_tax_inclusive: promotion.is_tax_inclusive,
|
||||||
})
|
})
|
||||||
} else if (isTargetShippingMethod) {
|
} else if (isTargetShippingMethod) {
|
||||||
computedActions.push({
|
computedActions.push({
|
||||||
|
|||||||
Reference in new issue
Block a user