Back to blog
Article

Partial refunds and the ledger entries behind them

Partial refunds and the ledger entries behind them
S

StriveBit

4 min readE-commerce

Partial refunds and the ledger entries behind them

A customer orders three items for ₹4,500 — ₹1,200 in product A, ₹1,800 in product B, ₹800 in product C, ₹200 IGST at 5%, and ₹500 in shipping. They receive the parcel, product B is damaged, and they ask for ₹600 back instead of returning the item. The merchant agrees.

The payment gateway call is the easy part. Razorpay's `POST /v1/payments/{id}/refund` takes an `amount` field in paise and returns a `refund_id`. We store that against the order. The harder part is what happens inside the ledger.

A refund is not a reversal of the original sale entry. The sale happened — goods left the warehouse, tax was collected, revenue was recognized. What we need is a new entry that reduces revenue and tax proportionally, adjusts inventory if applicable, and accounts for shipping correctly.

The original sale posts look like this:

# order_id = ORD-2847
journal_entry("2025-01-15", [
    ("Bank", 4500, "debit"),
    ("Sales - Product A", 1200, "credit"),
    ("Sales - Product B", 1800, "credit"),
    ("Sales - Product C", 800, "credit"),
    ("Output IGST", 200, "credit"),
    ("Shipping Revenue", 500, "credit"),
])

For the ₹600 partial refund, we split the amount across product, tax, and shipping based on what the refund is actually for. If the merchant is refunding ₹500 of Product B's price plus proportionate tax, the entry is:

journal_entry("2025-01-22", [
    ("Sales - Product B", 500, "debit"),
    ("Output IGST", 25, "debit"),
    ("Bank", 525, "credit"),
], linked_to="ORD-2847")

The proportionate tax is the issue most teams get wrong. GST is calculated on the taxable value of the line items, not the total order. If the merchant refunds ₹500 off a ₹1,800 line, the tax adjustment is 5% of ₹500 — ₹25 — not 5% of ₹600 or 5% of the order total. Getting this wrong means the GSTR-1 filed for January won't match the credit notes issued, and the reconciliation breaks.

Shipping is a separate decision. If the customer kept all three items and the refund is only for the damage, shipping revenue stays untouched. But if the partial refund is because one item was out of stock after the order was placed, the merchant may choose to refund shipping too. We treat this as a separate flag — `refund_shipping` — and compute it independently of the product refund amount. Conflating the two makes the ledger entry wrong and the shipping P&L column inflated.

We do not issue partial refunds as credit note entries that sit against a receivable. The payment has already been captured and settled to the bank account. The refund goes back through the gateway and settles after 5-7 business days. Until it settles, we hold it in a `Refunds Payable` liability account, not `Bank`:

journal_entry("2025-01-22", [
    ("Sales - Product B", 500, "debit"),
    ("Output IGST", 25, "debit"),
    ("Refunds Payable", 525, "credit"),
], linked_to="ORD-2847")

# On settlement, 5-7 days later
journal_entry("2025-01-29", [
    ("Refunds Payable", 525, "debit"),
    ("Bank", 525, "credit"),
], linked_to="ORD-2847")

The `Refunds Payable` balance at month-end tells the finance team exactly how much is in transit. Without it, the bank reconciliation has unexplained gaps for every refund initiated in the last week of the month.

Inventory is separate. If the customer keeps the damaged item, no stock adjustment is needed. If the merchant asks for it back and it's non-sellable, we post a separate stock movement with its own reason code. Tying that into the refund entry makes the ledger harder to audit — the inventory and money movements are independent events with different timestamps and should be separate journal entries linked by order ID, not combined.

The refund receipt from the gateway includes a `refund_id` and a `payment_id`. We store both in a `refunds` table keyed by the order, and the journal entry references the `refund_id` in a `source_reference` column. When the auditor pulls all refunds for Q3, they get the ledger entry and the gateway receipt in one query.

Back to all articles

Ready to build something great?

We help ambitious teams build software that lasts. If you're interested in working with us or want to discuss your project, let's connect.

Get in touch