How much has each customer spent, net of refunds?
By the Analistable team · Updated · 3 min read
Join the refunds export to orders on order ID, then sum per customer: net = order total − refunds. Refunds usually live in a separate export, which is why gross spend overstates value. In the example, eva@nordic.dk is the most valuable customer at 300 net (350 gross − 50 refunded).
Part of our guide: How to answer questions across multiple spreadsheets
The data
| Customer | Order | Date | Amount | Store |
|---|---|---|---|---|
| ana@lune.fr | O1 | 2026-01-14 | 120 | Web |
| ana@lune.fr | O2 | 2026-03-02 | 80 | Web |
| ana@lune.fr | O3 | 2026-08-21 | 95 | Amazon |
| ben@hart.co.uk | O4 | 2026-02-10 | 60 | Amazon |
| eva@nordic.dk | O5 | 2026-04-05 | 200 | Web |
| eva@nordic.dk | O6 | 2026-09-12 | 150 | Web |
| kim@kiln.io | O7 | 2026-06-30 | 45 | Amazon |
| Order | Refunded |
|---|---|
| O2 | 80 |
| O5 | 50 |
In a spreadsheet
Refund (on each order row) =SUMIFS(Refunds[Refunded], Refunds[Order], [@Order])
Net per customer =SUMIFS(Orders[Amount], Orders[Customer], A2) - SUMIFS(Orders[Refund], Orders[Customer], A2)In SQL
SELECT o.customer,
COUNT(*) AS orders,
SUM(o.amount) AS gross,
COALESCE(SUM(r.amount), 0) AS refunds,
SUM(o.amount) - COALESCE(SUM(r.amount), 0) AS net
FROM orders o
LEFT JOIN refunds r ON r.order_id = o.order_id
GROUP BY o.customer
ORDER BY net DESC;| customer | orders | gross | refunds | net |
|---|---|---|---|---|
| eva@nordic.dk | 2 | 350 | 50 | 300 |
| ana@lune.fr | 3 | 295 | 80 | 215 |
| ben@hart.co.uk | 1 | 60 | 0 | 60 |
| kim@kiln.io | 1 | 45 | 0 | 45 |
Watch out for
- Partial refunds: sum refunds per order before joining, or one order with two refunds is counted twice.
- This is historical value. A predicted lifetime value also needs a churn or retention rate.
- Margins: for profitability, multiply net revenue by gross margin rather than ranking on revenue.
Ask it in Analistable: “Rank customers by net revenue after refunds, with their number of orders.” Related: repeat purchase rate.
In Google Sheets
=QUERY({Orders!A2:A, Orders!D2:D - ARRAYFORMULA(IFERROR(VLOOKUP(Orders!B2:B, Refunds!A2:B, 2, FALSE), 0))},
"select Col1, sum(Col2) where Col1 is not null group by Col1 order by sum(Col2) desc label sum(Col2) 'Net'", 0)The VLOOKUP brings each order's refund (0 if none) next to its amount; QUERY sums the net per customer and sorts. This assumes at most one refund row per order — sum refunds per order first if there can be several.
The double-counting trap, with numbers
Suppose O2's 80 refund arrives as two rows: 50 and 30. Joining the raw refunds export to orders duplicates the O2 order row, so ana@lune.fr's gross becomes 375 instead of 295. Aggregate refunds per order first:
WITH r AS (
SELECT order_id, SUM(amount) AS refunded
FROM refunds
GROUP BY order_id
)
SELECT o.customer,
SUM(o.amount) AS gross,
COALESCE(SUM(r.refunded), 0) AS refunds,
SUM(o.amount) - COALESCE(SUM(r.refunded), 0) AS net
FROM orders o
LEFT JOIN r ON r.order_id = o.order_id
GROUP BY o.customer
ORDER BY net DESC;This returns the same table as above (eva@nordic.dk 300, ana@lune.fr 215, ben@hart.co.uk 60, kim@kiln.io 45) however many refund rows each order has. In a spreadsheet, SUMIFS on the order ID already sums all refund rows, so the formula version is safe.
Summary figures from the same data
| Measure | Calculation | Value |
|---|---|---|
| Gross revenue | sum of all orders | 750 |
| Refunds | 80 + 50 | 130 |
| Net revenue | 750 − 130 | 620 |
| Refund rate | 130 ÷ 750 | 17.3% |
| Average net value per customer | 620 ÷ 4 | 155 |
Average net value per customer is the historical CLV most teams quote. A simple forward-looking estimate multiplies average order value by expected orders per year and expected years as a customer — but those inputs are assumptions, so show them next to the result.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Net value negative for a customer | Refund matched to the wrong order, or a refund for an order outside the export period | Widen the order export or filter refunds to the same period |
| Refunds total lower than the payment provider's | Refunds missing an order ID (manual or goodwill refunds) | List refunds with no matching order and allocate by hand |
| Gross higher than the sales report | Duplicate rows after joining | Pre-aggregate refunds per order |
Recalculate monthly on the full history, not month by month, so a refund in October reduces the value of the September order it belongs to. Background on joining the two exports: joining spreadsheets.
Frequently asked questions
- How do I calculate customer lifetime value in Excel?
- Sum each customer's order totals with SUMIFS and subtract their refunds, which you first match to orders by order ID.
- Why join refunds by order ID rather than customer?
- Refund exports often lack the customer, and joining by order keeps partial refunds attached to the right purchase.
- Is this the same as predicted CLV?
- No. This is realised value to date; predicted CLV adds an estimate of future purchases.