Analistable

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

Orders from two stores (Web and Amazon), stacked
CustomerOrderDateAmountStore
ana@lune.frO12026-01-14120Web
ana@lune.frO22026-03-0280Web
ana@lune.frO32026-08-2195Amazon
ben@hart.co.ukO42026-02-1060Amazon
eva@nordic.dkO52026-04-05200Web
eva@nordic.dkO62026-09-12150Web
kim@kiln.ioO72026-06-3045Amazon
Refunds export
OrderRefunded
O280
O550

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;
Result
customerordersgrossrefundsnet
eva@nordic.dk235050300
ana@lune.fr329580215
ben@hart.co.uk160060
kim@kiln.io145045

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

Whole customer base
MeasureCalculationValue
Gross revenuesum of all orders750
Refunds80 + 50130
Net revenue750 − 130620
Refund rate130 ÷ 75017.3%
Average net value per customer620 ÷ 4155

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

Common problems
SymptomCauseFix
Net value negative for a customerRefund matched to the wrong order, or a refund for an order outside the export periodWiden the order export or filter refunds to the same period
Refunds total lower than the payment provider'sRefunds missing an order ID (manual or goodwill refunds)List refunds with no matching order and allocate by hand
Gross higher than the sales reportDuplicate rows after joiningPre-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.

Related guides