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:
Frane Polić
2025-06-12 15:07:11 +02:00
committed by GitHub
co-authored by Oli Juhl
parent 08de1f54e4
commit 2621f00bb0
29 changed files with 1091 additions and 24 deletions
@@ -1,11 +1,12 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { PromotionStatus, PromotionType } from "@medusajs/utils"
import { Modules, PromotionStatus, PromotionType } from "@medusajs/utils"
import {
createAdminUser,
generatePublishableKey,
generateStoreHeaders,
} from "../../../../helpers/create-admin-user"
import { medusaTshirtProduct } from "../../../__fixtures__/product"
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures/tax"
jest.setTimeout(50000)
@@ -71,6 +72,8 @@ medusaIntegrationTestRunner({
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, appContainer)
await setupTaxStructure(appContainer.resolve(Modules.TAX))
promotion = standardPromotion = (
await api.post(
`/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", () => {