Skip to content

[Bug]: Refundable totals are incorrect when `is_tax_inclusive = true #14136

@blxck20

Description

@blxck20

Package.json file

{
  "name": "medusa",
  "version": "0.0.1",
  "description": "A starter for Medusa projects.",
  "author": "Medusa (https://medusajs.com)",
  "license": "MIT",
  "keywords": [
    "sqlite",
    "postgres",
    "typescript",
    "ecommerce",
    "headless",
    "medusa"
  ],
  "scripts": {
    "seed": "medusa exec ./src/scripts/seed.ts",
    "start": "medusa start",
    "dev": "medusa develop",
    "predeploy": "npx medusa db:migrate",
    "build": "medusa build",
  },
  "dependencies": {
    "@medusajs/admin-sdk": "2.9.0",
    "@medusajs/cli": "2.9.0",
    "@medusajs/dashboard": "2.9.0",
    "@medusajs/framework": "2.9.0",
    "@medusajs/medusa": "2.9.0",
    "@mikro-orm/core": "6.4.3",
    "@mikro-orm/knex": "6.4.3",
    "@mikro-orm/migrations": "6.4.3",
    "@mikro-orm/postgresql": "6.4.3",
    "awilix": "^8.0.1",
    "date-fns-tz": "^3.2.0",
    "module-alias": "^2.2.3",
    "multer": "^1.4.5-lts.2",
    "pdfmake": "^0.2.20",
    "pg": "^8.13.0",
    "react-pdf": "^10.1.0",
    "xlsx": "^0.18.5"
  },
  "devDependencies": {
    "@medusajs/test-utils": "2.9.0",
    "@mikro-orm/cli": "6.4.3",
    "@swc/core": "1.5.7",
    "@swc/jest": "^0.2.36",
    "@types/jest": "^29.5.13",
    "@types/multer": "^1",
    "@types/node": "^20.0.0",
    "@types/pdfmake": "^0.2.11",
    "@types/react": "^18.3.2",
    "@types/react-dom": "^18.2.25",
    "jest": "^29.7.0",
    "prop-types": "^15.8.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "rimraf": "^6.0.1",
    "ts-node": "^10.9.2",
    "tsc-alias": "^1.8.13",
    "typescript": "^5.6.2",
    "vite": "^5.2.11",
    "yalc": "^1.0.0-pre.53"
  },
  "engines": {
    "node": ">=20"
  },
  "packageManager": "[email protected]",
}
{
  "name": "medusa",
  "version": "0.0.1",
  "description": "A starter for Medusa projects.",
  "author": "Medusa (https://medusajs.com)",
  "license": "MIT",
  "keywords": [
    "sqlite",
    "postgres",
    "typescript",
    "ecommerce",
    "headless",
    "medusa"
  ],
  "scripts": {
    "seed": "medusa exec ./src/scripts/seed.ts",
    "start": "medusa start",
    "dev": "medusa develop",
    "predeploy": "npx medusa db:migrate",
    "build": "medusa build",
  },
  "dependencies": {
    "@medusajs/admin-sdk": "2.9.0",
    "@medusajs/cli": "2.9.0",
    "@medusajs/dashboard": "2.9.0",
    "@medusajs/framework": "2.9.0",
    "@medusajs/medusa": "2.9.0",
    "@mikro-orm/core": "6.4.3",
    "@mikro-orm/knex": "6.4.3",
    "@mikro-orm/migrations": "6.4.3",
    "@mikro-orm/postgresql": "6.4.3",
    "awilix": "^8.0.1",
    "date-fns-tz": "^3.2.0",
    "module-alias": "^2.2.3",
    "multer": "^1.4.5-lts.2",
    "pdfmake": "^0.2.20",
    "pg": "^8.13.0",
    "react-pdf": "^10.1.0",
    "xlsx": "^0.18.5"
  },
  "devDependencies": {
    "@medusajs/test-utils": "2.9.0",
    "@mikro-orm/cli": "6.4.3",
    "@swc/core": "1.5.7",
    "@swc/jest": "^0.2.36",
    "@types/jest": "^29.5.13",
    "@types/multer": "^1",
    "@types/node": "^20.0.0",
    "@types/pdfmake": "^0.2.11",
    "@types/react": "^18.3.2",
    "@types/react-dom": "^18.2.25",
    "jest": "^29.7.0",
    "prop-types": "^15.8.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "rimraf": "^6.0.1",
    "ts-node": "^10.9.2",
    "tsc-alias": "^1.8.13",
    "typescript": "^5.6.2",
    "vite": "^5.2.11",
    "yalc": "^1.0.0-pre.53"
  },
  "engines": {
    "node": ">=20"
  },
  "packageManager": "[email protected]",
}

Node.js version

v20.10.0

Database and its version

Postgres 17

Operating system name and version

MacOs

Browser name

Brave

What happended?

When an item is configured with tax-inclusive pricing, the item totals are calculated correctly — but the refundable totals incorrectly include tax. This results in inflated refundable amounts.

Example

Item data:

{
  "is_discountable": false,
  "is_tax_inclusive": true,
  "is_custom_price": true,
  "unit_price": 0.5,
  "quantity": 3,
  "subtotal": 1.4285714285714286,
  "total": 1.5,
  "original_total": 1.5,
  "discount_total": 0,
  "discount_subtotal": 0,
  "discount_tax_total": 0,
  "tax_total": 0.07142857142857142,
  "original_tax_total": 0.07142857142857142,
  "refundable_total_per_unit": 0.525,
  "refundable_total": 1.575
}

In this scenario, the refundable_total should exclude tax when tax is already baked into the price, otherwise refunds return more than the customer actually paid pre-tax.

Code causing issue

The current implementation recalculates tax based on the refundable subtotal:

const refundableSubTotal = MathBN.sub(
  MathBN.mult(currentQuantity, item.unit_price),
  MathBN.mult(currentQuantity, discountPerUnit)
)

const taxTotal = calculateTaxTotal({
  taxLines: item.tax_lines || [],
  taxableAmount: refundableSubTotal,
})

const refundableTotal = MathBN.add(refundableSubTotal, taxTotal)

This logic assumes the item price is tax-exclusive, which is wrong when is_tax_inclusive is true.

Impact

Refund processes can credit higher amounts than what customers paid, leading to accounting discrepancies.

Expected behavior

When is_tax_inclusive = true:

  • Do not add tax back into refundable totals
  • Refundable subtotal already includes tax → tax shouldn’t be re-applied

Proposed Fix

Before calculating refundable tax, check is_tax_inclusive:

  • If true → use unit_price as final price (no added tax)
  • If false → retain existing logic

Actual behavior

Actual Behavior

For a tax-inclusive item:

{
  "unit_price": 0.5,
  "quantity": 3,
  "is_tax_inclusive": true,
  "subtotal": 1.4285714285714286,
  "total": 1.5,
  "tax_total": 0.07142857142857142
}

The item totals are correctly calculated from a tax-inclusive price:

0.50 × 3 = 1.50 (total)
1.50 / 1.05 = 1.428571... (subtotal)
Tax = 1.50 − 1.428571... = 0.071428...

However, the refundable calculation incorrectly re-adds tax:

refundable_subtotal = 3 × 0.50 = 1.50  (already includes tax)
tax_total = 1.50 × 5% = 0.071428...
refundable_total = 1.50 + 0.071428... = 1.571428... (recorded as 1.575)
refundable_total_per_unit = 1.575 / 3 = 0.525

This results in:

Field Value
refundable_total 1.575
refundable_total_per_unit 0.525

Refunds end up higher than the paid price, because tax is refunded twice.

Link to reproduction repo

N/A

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions