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:
@@ -132,6 +132,7 @@ export const prepareAdjustmentsFromPromotionActionsStep = createStep(
|
||||
.map((action) => ({
|
||||
code: action.code,
|
||||
amount: (action as AddItemAdjustmentAction).amount,
|
||||
is_tax_inclusive: (action as AddItemAdjustmentAction).is_tax_inclusive,
|
||||
item_id: (action as AddItemAdjustmentAction).item_id,
|
||||
promotion_id: promotionsMap.get(action.code)?.id,
|
||||
}))
|
||||
|
||||
@@ -22,6 +22,11 @@ export interface AdjustmentLineDTO {
|
||||
*/
|
||||
amount: BigNumberValue
|
||||
|
||||
/**
|
||||
* Whether the adjustment is tax inclusive.
|
||||
*/
|
||||
is_tax_inclusive?: boolean
|
||||
|
||||
/**
|
||||
* The raw amount to adjust the original amount with.
|
||||
*/
|
||||
|
||||
@@ -236,6 +236,11 @@ export interface CreateAdjustmentDTO {
|
||||
*/
|
||||
amount: BigNumberInput
|
||||
|
||||
/**
|
||||
* Whether the adjustment amount includes tax.
|
||||
*/
|
||||
is_tax_inclusive?: boolean
|
||||
|
||||
/**
|
||||
* The description of the adjustment.
|
||||
*/
|
||||
|
||||
@@ -10,8 +10,8 @@ import { AdminCreateCampaign } from "../../campaign"
|
||||
|
||||
export interface AdminCreatePromotionRule {
|
||||
/**
|
||||
* 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
|
||||
* 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
|
||||
* must match the specified value.
|
||||
*/
|
||||
operator: PromotionRuleOperatorValues
|
||||
@@ -21,14 +21,14 @@ export interface AdminCreatePromotionRule {
|
||||
description?: string | null
|
||||
/**
|
||||
* The attribute to compare against when checking whether a promotion can be applied on a cart.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* items.product_id
|
||||
*/
|
||||
attribute: string
|
||||
/**
|
||||
* The value to compare against when checking whether a promotion can be applied on a cart.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* prod_123
|
||||
*/
|
||||
@@ -54,7 +54,7 @@ export interface AdminCreateApplicationMethod {
|
||||
value: number
|
||||
/**
|
||||
* The currency code of the application method.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* usd
|
||||
*/
|
||||
@@ -68,12 +68,12 @@ export interface AdminCreateApplicationMethod {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
allocation?: ApplicationMethodAllocationValues
|
||||
@@ -90,7 +90,7 @@ export interface AdminCreateApplicationMethod {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
buy_rules_min_quantity?: number | null
|
||||
@@ -111,7 +111,7 @@ export interface AdminUpdateApplicationMethod {
|
||||
max_quantity?: number | null
|
||||
/**
|
||||
* The currency code of the application method.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* usd
|
||||
*/
|
||||
@@ -121,12 +121,12 @@ export interface AdminUpdateApplicationMethod {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
allocation?: ApplicationMethodAllocationValues
|
||||
@@ -143,7 +143,7 @@ export interface AdminUpdateApplicationMethod {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
buy_rules_min_quantity?: number | null
|
||||
@@ -155,11 +155,15 @@ export interface AdminCreatePromotion {
|
||||
*/
|
||||
code: string
|
||||
/**
|
||||
* Whether the promotion is applied automatically
|
||||
* Whether the promotion is applied automatically
|
||||
* or requires the customer to manually apply it
|
||||
* by entering the code at checkout.
|
||||
*/
|
||||
is_automatic?: boolean
|
||||
/**
|
||||
* Whether the promotion is tax inclusive.
|
||||
*/
|
||||
is_tax_inclusive?: boolean
|
||||
/**
|
||||
* The type of promotion.
|
||||
*/
|
||||
@@ -188,7 +192,7 @@ export interface AdminUpdatePromotion {
|
||||
*/
|
||||
code?: string
|
||||
/**
|
||||
* Whether the promotion is applied automatically
|
||||
* Whether the promotion is applied automatically
|
||||
* or requires the customer to manually apply it
|
||||
* by entering the code at checkout.
|
||||
*/
|
||||
|
||||
@@ -19,23 +19,23 @@ export interface BasePromotionRule {
|
||||
description?: string | null
|
||||
/**
|
||||
* The attribute to compare against when checking whether a promotion can be applied on a cart.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* items.product_id
|
||||
*/
|
||||
attribute?: string
|
||||
/**
|
||||
* 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
|
||||
* 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
|
||||
* must match the specified value.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* eq
|
||||
*/
|
||||
operator?: PromotionRuleOperatorValues
|
||||
/**
|
||||
* The values to compare against when checking whether a promotion can be applied on a cart.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* prod_123
|
||||
*/
|
||||
@@ -62,6 +62,7 @@ export interface BasePromotion {
|
||||
code?: string
|
||||
type?: PromotionTypeValues
|
||||
is_automatic?: boolean
|
||||
is_tax_inclusive?: boolean
|
||||
application_method?: BaseApplicationMethod
|
||||
rules?: BasePromotionRule[]
|
||||
status?: PromotionStatusValues
|
||||
|
||||
@@ -60,6 +60,12 @@ export interface AddItemAdjustmentAction {
|
||||
*/
|
||||
amount: BigNumberInput
|
||||
|
||||
/**
|
||||
* Whether the adjustment amount includes tax.
|
||||
*/
|
||||
is_tax_inclusive?: boolean
|
||||
|
||||
/**
|
||||
/**
|
||||
* The promotion's code.
|
||||
*/
|
||||
|
||||
@@ -60,6 +60,11 @@ export interface PromotionDTO {
|
||||
*/
|
||||
is_automatic?: boolean
|
||||
|
||||
/**
|
||||
* Whether the promotion is tax inclusive.
|
||||
*/
|
||||
is_tax_inclusive?: boolean
|
||||
|
||||
/**
|
||||
* The associated application method.
|
||||
*/
|
||||
@@ -113,6 +118,11 @@ export interface CreatePromotionDTO {
|
||||
*/
|
||||
is_automatic?: boolean
|
||||
|
||||
/**
|
||||
* Whether the promotion is tax inclusive.
|
||||
*/
|
||||
is_tax_inclusive?: boolean
|
||||
|
||||
/**
|
||||
* 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 () {
|
||||
const cart = {
|
||||
items: [
|
||||
|
||||
@@ -8,7 +8,7 @@ export function calculateAdjustmentTotal({
|
||||
includesTax,
|
||||
taxRate,
|
||||
}: {
|
||||
adjustments: Pick<AdjustmentLineDTO, "amount">[]
|
||||
adjustments: Pick<AdjustmentLineDTO, "amount" | "is_tax_inclusive">[]
|
||||
includesTax?: boolean
|
||||
taxRate?: BigNumberInput
|
||||
}) {
|
||||
@@ -25,7 +25,15 @@ export function calculateAdjustmentTotal({
|
||||
}
|
||||
|
||||
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)) {
|
||||
const adjustmentSubtotal = includesTax
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface DecorateCartLikeInputDTO {
|
||||
unit_price: BigNumberInput
|
||||
is_tax_inclusive?: boolean
|
||||
quantity: BigNumberInput
|
||||
adjustments?: { amount: BigNumberInput }[]
|
||||
adjustments?: { amount: BigNumberInput; is_tax_inclusive?: boolean }[]
|
||||
tax_lines?: {
|
||||
rate: BigNumberInput
|
||||
}[]
|
||||
|
||||
Reference in New Issue
Block a user