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
@@ -7362,6 +7362,9 @@
"clearAll": {
"type": "string"
},
"taxInclusive": {
"type": "string"
},
"amount": {
"type": "object",
"properties": {
@@ -7428,6 +7431,7 @@
"allocation",
"addCondition",
"clearAll",
"taxInclusive",
"amount",
"conditions"
],
@@ -7626,6 +7630,19 @@
"required": ["existing", "new", "none"],
"additionalProperties": false
},
"taxInclusive": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": ["title", "description"],
"additionalProperties": false
},
"status": {
"type": "object",
"properties": {
@@ -7852,6 +7869,7 @@
"and",
"selectAttribute",
"campaign",
"taxInclusive",
"status",
"method",
"max_quantity",
@@ -1969,6 +1969,7 @@
"allocation": "Allocation",
"addCondition": "Add condition",
"clearAll": "Clear all",
"taxInclusive": "Tax Inclusive",
"amount": {
"tooltip": "Select the currency code to enable setting the amount"
},
@@ -2045,6 +2046,10 @@
"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": {
"label": "Status",
"draft": {
@@ -19,6 +19,7 @@ import {
ProgressStatus,
ProgressTabs,
RadioGroup,
Switch,
Text,
toast,
} from "@medusajs/ui"
@@ -52,6 +53,7 @@ const defaultValues = {
type: "standard" as PromotionTypeValues,
status: "draft" as PromotionStatusValues,
rules: [],
is_tax_inclusive: false,
application_method: {
allocation: "each" as ApplicationMethodAllocationValues,
type: "fixed" as ApplicationMethodTypeValues,
@@ -89,6 +91,7 @@ export const CreatePromotionForm = () => {
const {
campaign_choice: _campaignChoice,
is_automatic,
is_tax_inclusive,
template_id: _templateId,
application_method,
rules,
@@ -142,6 +145,7 @@ export const CreatePromotionForm = () => {
target_rules: buildRulesData(targetRulesData),
buy_rules: buildRulesData(buyRulesData),
},
is_tax_inclusive,
is_automatic: is_automatic === "true",
},
{
@@ -583,6 +587,49 @@ export const CreatePromotionForm = () => {
/>
</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") && (
<Form.Field
control={form.control}
@@ -27,6 +27,7 @@ export const CreatePromotionSchema = z
type: z.enum(["buyget", "standard"]),
status: z.enum(["draft", "active", "inactive"]),
rules: RuleSchema,
is_tax_inclusive: z.boolean().optional(),
application_method: z.object({
allocation: z.enum(["each", "across"]),
value: z.number().min(0),
@@ -74,7 +74,11 @@ export const templates = [
type: "buy_get",
title: "Buy X Get Y",
description: "Buy X product(s), get Y product(s)",
hiddenFields: [...commonHiddenFields, "application_method.value"],
hiddenFields: [
...commonHiddenFields,
"application_method.value",
"is_tax_inclusive",
],
defaults: {
is_automatic: "false",
type: "buyget",
@@ -180,6 +180,18 @@ export const PromotionGeneralSection = ({
{promotion.application_method?.allocation!}
</Text>
</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>
)
}