Skip to content

[Bug]: Rounding error in order line items with decimal quantities (fractional cents not rounded before tax) #13734

@leivadev

Description

@leivadev

Package.json file

{
  "name": "backend",
  "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": {
    "build": "medusa build",
    "seed": "medusa exec ./src/scripts/seed.ts",
    "start": "medusa start",
    "dev": "medusa develop --host 127.0.0.1 --port 8101",
    "format": "biome format --write --verbose .",
    "migrate": "KNEX_POOL_MAX=2 KNEX_ACQUIRE_TIMEOUT=60000 medusa db:migrate --execute-all-links",
    "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit",
    "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit",
    "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit"
  },
  "dependencies": {
    "@medusajs/admin-sdk": "2.3.1",
    "@medusajs/cli": "2.3.1",
    "@medusajs/framework": "2.3.1",
    "@medusajs/medusa": "2.3.1",
    "@mikro-orm/core": "5.9.7",
    "@mikro-orm/knex": "5.9.7",
    "@mikro-orm/migrations": "5.9.7",
    "@mikro-orm/postgresql": "5.9.7",
    "@retail/types": "*",
    "awilix": "^8.0.1",
    "jsonwebtoken": "^9.0.2",
    "lodash": "^4.17.21",
    "luxon": "^3.2.1",
    "nanoid": "^3.3.8",
    "pg": "^8.13.0",
    "scrypt-kdf": "^2.0.1",
    "zod": "3.22.4"
  },
  "devDependencies": {
    "@medusajs/test-utils": "2.3.1",
    "@mikro-orm/cli": "5.9.7",
    "@swc/core": "1.5.7",
    "@swc/jest": "^0.2.36",
    "@types/jest": "^29.5.13",
    "@types/jsonwebtoken": "^9.0.2",
    "@types/lodash": "^4.17.15",
    "@types/luxon": "^3.2.1",
    "@types/node": "^20.0.0",
    "@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",
    "ts-node": "^10.9.2",
    "typescript": "^5.6.2",
    "vite": "^5.2.11"
  },
  "engines": {
    "node": ">=20"
  },
  "packageManager": "[email protected]"
}

Node.js version

v22.11.0

Database and its version

PostgreSQL 15

Operating system name and version

MacOS 15.7.1

Browser name

No response

What happended?

When creating an order line item with a decimal quantity, Medusa calculates intermediate monetary values without rounding to the currency’s minor unit (2 decimals for USD).

Why using decimal quantities?
In our client’s retail operations, they sell goods by length or volume (for instance, fabrics, linens, or trim) where customers often request fractional quantities like 0.5 or 1.25 units (e.g. half a yard). Because of this, the system must support decimal quantities (not just integers) and compute correct cent-level amounts after multiplying by unit price and applying tax.

Example case:

quantity = 0.5

unit_price = 1.99

tax = 7% (is_tax_inclusive = false)

Medusa internally computes:

subtotal = 0.5 * 1.99 = 0.995
tax_total = 0.995 * 0.07 = 0.06965
total = 0.995 + 0.06965 = 1.06465

Because rounding occurs only at the final total conversion (getSmallestUnit), the value becomes $1.06 instead of the expected $1.07.
This discrepancy causes invoices and payments to mismatch by one cent.
The problematic behavior originates in createOrderLineItemsStep → getLineItemTotals (framework) → calculateTaxTotal.
These functions use BigNumber math but never round after quantity * unit_price or after tax calculations.

Possible Affected areas:

packages/core/core-flows/src/order/steps/create-order-line-items.ts
packages/core/core-flows/src/cart/utils/totals.ts
packages/core/core-flows/src/totals/calculate-tax-total.ts
@medusajs/framework BigNumber transformations and final DTO rounding (decorateCartTotals)

Expected behavior

Each order line item should normalize monetary amounts at the appropriate precision step:

Line Subtotal: round(quantity × unit_price) → $1.00
Line Tax: round(subtotal × tax_rate) → $0.07
Line Total: subtotal + tax_total → $1.07

Totals at the order level should equal the sum of individually rounded lines, matching invoices and payment processor totals.

Actual behavior

Medusa retains high-precision BigNumbers through the pipeline and only rounds at the final payment conversion (getSmallestUnit):

subtotal: 0.995
tax_total: 0.06965
total: 1.06465 → rounds to $1.06

JSON Payload Response for POST {{baseUrl}}/admin/orders/{{orderId}}/line-items

{
    "order": {
        "id": "order_01K77FV6T57PNHNSMMQ91K96XQ",
        "display_id": 1436,
        "status": "draft",
        "version": 1,
        "summary": {
            "paid_total": 0,
            "difference_sum": 0,
            "raw_paid_total": {
                "value": "0",
                "precision": 20
            },
            "refunded_total": 0,
            "accounting_total": 0,
            "credit_line_total": 0,
            "transaction_total": 0,
            "pending_difference": 0,
            "raw_difference_sum": {
                "value": "0",
                "precision": 20
            },
            "raw_refunded_total": {
                "value": "0",
                "precision": 20
            },
            "current_order_total": 0,
            "original_order_total": 0,
            "raw_accounting_total": {
                "value": "0",
                "precision": 20
            },
            "raw_credit_line_total": {
                "value": "0",
                "precision": 20
            },
            "raw_transaction_total": {
                "value": "0",
                "precision": 20
            },
            "raw_pending_difference": {
                "value": "0",
                "precision": 20
            },
            "raw_current_order_total": {
                "value": "0",
                "precision": 20
            },
            "raw_original_order_total": {
                "value": "0",
                "precision": 20
            }
        },
        "metadata": null,
        "created_at": "2025-10-10T16:39:10.917Z",
        "updated_at": "2025-10-10T16:39:10.917Z",
        "region_id": "reg_01JH897JQ0K4EH3AKN6CZ8JXPZ",
        "customer_id": "cus_01JM5QCAKJA3DNHFPZ40P4NSVP",
        "total": 1.06465,
        "subtotal": 0.995,
        "tax_total": 0.06965,
        "discount_total": 0,
        "discount_tax_total": 0,
        "original_total": 1.06465,
        "original_tax_total": 0.06965,
        "item_total": 1.06465,
        "item_subtotal": 0.995,
        "item_tax_total": 0.06965,
        "original_item_total": 1.06465,
        "original_item_subtotal": 0.995,
        "original_item_tax_total": 0.06965,
        "shipping_total": 0,
        "shipping_subtotal": 0,
        "shipping_tax_total": 0,
        "original_shipping_tax_total": 0,
        "original_shipping_subtotal": 0,
        "original_shipping_total": 0,
        "items": [
            {
                "id": "ordli_01K77GRXEAY058ZGP8Q2049HX3",
                "title": "Telas/Sedería",
                "subtitle": "Telas/Sedería",
                "thumbnail": null,
                "variant_id": "variant_01JJVRN3PQJ7Q8GYDP0SZ7VAP5",
                "product_id": "prod_01JJVRN3MVZW2NQF9CT5CJ15AT",
                "product_title": "Telas/Sedería",
                "product_description": null,
                "product_subtitle": null,
                "product_type": null,
                "product_type_id": null,
                "product_collection": null,
                "product_handle": "telassederia",
                "variant_sku": "57",
                "variant_barcode": null,
                "variant_title": "Telas/Sedería",
                "variant_option_values": null,
                "requires_shipping": false,
                "is_discountable": true,
                "is_tax_inclusive": false,
                "raw_compare_at_unit_price": null,
                "raw_unit_price": {
                    "value": "1.99",
                    "precision": 20
                },
                "is_custom_price": true,
                "metadata": {},
                "created_at": "2025-10-10T16:55:24.363Z",
                "updated_at": "2025-10-10T16:55:24.363Z",
                "deleted_at": null,
                "tax_lines": [
                    {
                        "id": "ordlitxl_01K77GRXEAGN7SD7DD6ABX21WA",
                        "description": "ITBMS 7%",
                        "tax_rate_id": "txr_01JHTY5QSKN8RVTZ2RQJ4KDRMK",
                        "code": "pa_tax_7",
                        "raw_rate": {
                            "value": "7",
                            "precision": 20
                        },
                        "provider_id": "system",
                        "created_at": "2025-10-10T16:55:24.363Z",
                        "updated_at": "2025-10-10T16:55:24.363Z",
                        "deleted_at": null,
                        "item_id": "ordli_01K77GRXEAY058ZGP8Q2049HX3",
                        "rate": 7,
                        "total": 0.06965,
                        "subtotal": 0.06965,
                        "raw_total": {
                            "value": "0.06965",
                            "precision": 20
                        },
                        "raw_subtotal": {
                            "value": "0.06965",
                            "precision": 20
                        }
                    }
                ],
                "adjustments": [],
                "compare_at_unit_price": null,
                "unit_price": 1.99,
                "quantity": 0.5,
                "raw_quantity": {
                    "value": "0.5",
                    "precision": 20
                },
                "detail": {
                    "id": "orditem_01K77GRXEAS4VX2PFXCY2G2W0T",
                    "order_id": "order_01K77FV6T57PNHNSMMQ91K96XQ",
                    "version": 1,
                    "item_id": "ordli_01K77GRXEAY058ZGP8Q2049HX3",
                    "raw_unit_price": null,
                    "raw_compare_at_unit_price": null,
                    "raw_quantity": {
                        "value": "0.5",
                        "precision": 20
                    },
                    "raw_fulfilled_quantity": {
                        "value": "0",
                        "precision": 20
                    },
                    "raw_delivered_quantity": {
                        "value": "0",
                        "precision": 20
                    },
                    "raw_shipped_quantity": {
                        "value": "0",
                        "precision": 20
                    },
                    "raw_return_requested_quantity": {
                        "value": "0",
                        "precision": 20
                    },
                    "raw_return_received_quantity": {
                        "value": "0",
                        "precision": 20
                    },
                    "raw_return_dismissed_quantity": {
                        "value": "0",
                        "precision": 20
                    },
                    "raw_written_off_quantity": {
                        "value": "0",
                        "precision": 20
                    },
                    "metadata": null,
                    "created_at": "2025-10-10T16:55:24.363Z",
                    "updated_at": "2025-10-10T16:55:24.363Z",
                    "deleted_at": null,
                    "unit_price": null,
                    "compare_at_unit_price": null,
                    "quantity": 0.5,
                    "fulfilled_quantity": 0,
                    "delivered_quantity": 0,
                    "shipped_quantity": 0,
                    "return_requested_quantity": 0,
                    "return_received_quantity": 0,
                    "return_dismissed_quantity": 0,
                    "written_off_quantity": 0
                },
                "subtotal": 0.995,
                "total": 1.06465,
                "original_total": 1.06465,
                "discount_total": 0,
                "discount_subtotal": 0,
                "discount_tax_total": 0,
                "tax_total": 0.06965,
                "original_tax_total": 0.06965,
                "refundable_total_per_unit": 2.1293,
                "refundable_total": 1.06465,
                "fulfilled_total": 0,
                "shipped_total": 0,
                "return_requested_total": 0,
                "return_received_total": 0,
                "return_dismissed_total": 0,
                "write_off_total": 0,
                "raw_subtotal": {
                    "value": "0.995",
                    "precision": 20
                },
                "raw_total": {
                    "value": "1.06465",
                    "precision": 20
                },
                "raw_original_total": {
                    "value": "1.06465",
                    "precision": 20
                },
                "raw_discount_total": {
                    "value": "0",
                    "precision": 20
                },
                "raw_discount_subtotal": {
                    "value": "0",
                    "precision": 20
                },
                "raw_discount_tax_total": {
                    "value": "0",
                    "precision": 20
                },
                "raw_tax_total": {
                    "value": "0.06965",
                    "precision": 20
                },
                "raw_original_tax_total": {
                    "value": "0.06965",
                    "precision": 20
                },
                "raw_refundable_total_per_unit": {
                    "value": "2.1293",
                    "precision": 20
                },
                "raw_refundable_total": {
                    "value": "1.06465",
                    "precision": 20
                },
                "raw_fulfilled_total": {
                    "value": "0",
                    "precision": 20
                },
                "raw_shipped_total": {
                    "value": "0",
                    "precision": 20
                },
                "raw_return_requested_total": {
                    "value": "0",
                    "precision": 20
                },
                "raw_return_received_total": {
                    "value": "0",
                    "precision": 20
                },
                "raw_return_dismissed_total": {
                    "value": "0",
                    "precision": 20
                },
                "raw_write_off_total": {
                    "value": "0",
                    "precision": 20
                },
                "variant": {
                    "id": "variant_01JJVRN3PQJ7Q8GYDP0SZ7VAP5",
                    "title": "Telas/Sedería",
                    "sku": "57",
                    "barcode": null,
                    "ean": null,
                    "upc": null,
                    "allow_backorder": false,
                    "manage_inventory": false,
                    "hs_code": null,
                    "origin_country": null,
                    "mid_code": null,
                    "material": null,
                    "weight": null,
                    "length": null,
                    "height": null,
                    "width": null,
                    "metadata": null,
                    "variant_rank": 0,
                    "product_id": "prod_01JJVRN3MVZW2NQF9CT5CJ15AT",
                    "product": {
                        "id": "prod_01JJVRN3MVZW2NQF9CT5CJ15AT",
                        "title": "Telas/Sedería",
                        "handle": "telassederia",
                        "subtitle": null,
                        "description": null,
                        "is_giftcard": false,
                        "status": "published",
                        "thumbnail": null,
                        "weight": null,
                        "length": null,
                        "height": null,
                        "width": null,
                        "origin_country": null,
                        "hs_code": null,
                        "mid_code": null,
                        "material": null,
                        "discountable": true,
                        "external_id": null,
                        "metadata": null,
                        "type_id": null,
                        "type": null,
                        "collection_id": null,
                        "collection": null,
                        "created_at": "2025-01-30T14:08:18.577Z",
                        "updated_at": "2025-01-30T14:08:18.577Z",
                        "deleted_at": null
                    },
                    "created_at": "2025-01-30T14:08:18.648Z",
                    "updated_at": "2025-01-30T14:08:18.648Z",
                    "deleted_at": null
                }
            }
        ],
        "shipping_address": {
            "id": "ordaddr_01K77FV6T5T3JSM4GNE7HQQ8SH",
            "customer_id": null,
            "company": "Disclosed",
            "first_name": null,
            "last_name": null,
            "address_1": null,
            "address_2": null,
            "city": null,
            "country_code": "pa",
            "province": null,
            "postal_code": null,
            "phone": null,
            "metadata": null,
            "created_at": "2025-10-10T16:39:10.917Z",
            "updated_at": "2025-10-10T16:39:10.917Z"
        },
        "billing_address": null,
        "shipping_methods": [],
        "customer": {
            "id": "cus_01JM5QCAKJA3DNHFPZ40P4NSVP",
            "company_name": null,
            "first_name": "CF",
            "last_name": null,
            "email": null,
            "phone": null,
            "has_account": false,
            "metadata": {
                "default": true
            },
            "created_by": null,
            "created_at": "2025-02-15T21:14:39.440Z",
            "updated_at": "2025-02-15T21:14:39.440Z",
            "deleted_at": null
        },
        "payment_collections": [],
        "pos_session": {
            "id": "posses_01K77AD24ANXJFTK16DV5Q88AY",
            "public_id": "E1-ZYZ15QQ",
            "status": "active",
            "summary": {
                "initial_amount": 100
            },
            "totals": null,
            "notes": null,
            "closed_at": null,
            "reviewed_at": null,
            "last_payment_at": null,
            "pos_id": "pos_01JKGCWY6YY6Z8HNSK1TT1E8K0",
            "pos": {
                "id": "pos_01JKGCWY6YY6Z8HNSK1TT1E8K0"
            },
            "created_at": "2025-10-10T15:04:04.490Z",
            "updated_at": "2025-10-10T15:04:04.490Z",
            "deleted_at": null
        },
        "fulfillments": [],
        "payment_status": "not_paid",
        "fulfillment_status": "not_fulfilled"
    }
}

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