The fraud nobody could see: Stripe dispute alerts

· TicketingHub · Design, Rails, tests

A supplier wrote to support asking how to stop something from happening again. A customer had disputed a payment as fraud, which is a chargeback. Later the same customer called to move the booking to a new date, and the staff member on the phone rescheduled it. Nothing on the screen said the payment was in dispute.

TicketingHub already had that information. Stripe reports disputes to the platform as they happen, and the payment page listed them. They never reached the pages where staff do their work. Those pages are in the same dashboard as the availability calendar.

I was asked for ideas on where and how to show a warning. I started the next morning, and before noon there was a working pull request with the design built in and two screen recordings in the ticket. Review took longer than the build. Bartosz Blimke and Csaba Apagyi reviewed the code, Carl Pihl made the product calls, and Attila Csizmadia ran the ticket. It was merged on 17 March 2026.

This is what staff see now.

Live · what staff see now

Top of the order, booking and payment pages

In the bookings list

OrderBookingCustomerProductPriceBooked for
OR-7K2QBK-19XA
Dana Whitfield
Disputed
Old Town Food Tour€98.0021 Mar 2026 10:00
OR-7K2RBK-19XB
Marek Nowak
Sunset Kayak Tour€39.0021 Mar 2026 18:00
OR-7K2SBK-19XC
Inês Carvalho
Harbor Cruise€52.0022 Mar 2026 11:00

Pick a case. The banner sits where staff open an order to reschedule it, and the badge follows the booking into the list. Dummy orders; a shortened bookings table; the banners, badges and wording are the shipped ones.

Disputes and Radar risk: two things called fraud

The ticket said "flagged as fraud in Stripe", and in Stripe that can mean two things.

  • A dispute, or chargeback. The cardholder tells their bank they did not make the payment. It comes after the payment, sometimes much later, and it is a fact: the money is being pulled back.
  • A risk signal. Stripe Radar scores every payment as it happens, from normal to elevated to highest. It comes first, and it is a probability.

I built the dispute alerts and asked in the ticket which of the two the client meant. Product answered that both were needed.

Where a fraud alert belongs in a dashboard

The reschedule happened on the order page, so the banner sits at the top of it. It is also on the booking page and the payment page, because staff reach an order from either, and from those two it links back to the order.

A banner only helps once someone opens the order. The same state also shows as a badge under the customer's name in the bookings list, and in the payments table on the order.

Each banner carries what a person on the phone needs: the reason in plain words ("Suspected fraud" for Stripe's fraudulent), the amount, a status such as Action Required or Under Review, and the date evidence is due. When that date has passed, the text turns bold red and says so.

A dispute is always red with a triangle. A risk signal has a shield, yellow for elevated and red for highest. Orders with nothing wrong look exactly as they did before.

The wording changed in review

My first banner told staff what to do: "Do not reschedule, edit, or check in bookings on this order until the dispute is resolved." That came straight from the ticket, which asked for a warning against exactly those actions.

Csaba pushed back in review. The point was that the dashboard should display information and let the supplier decide how to handle a dispute, and that if editing a disputed order really broke something, it should be prevented in code, not by a sentence that asks nicely. I agreed and changed both banners. The dispute one now reads "A payment on this order has been disputed. The dispute is currently unresolved." The risk one lost its "Exercise caution before processing this order."

Should risk scores be shown at all?

Bartosz questioned the scope. Disputes made sense. A risk score is a probability attached to a payment that was already accepted, and there is no defined action for a "High Risk" badge. Without one it is noise that stresses staff. If the worry is fraudulent payments, a Radar rule can block them before they are accepted.

Carl answered from the supplier's side. The booking that started this was an at-risk one, and staff who know that can ask for identification or a signed waiver. Today they have no way of knowing.

Both alerts shipped. I find Bartosz's argument about blocking at the source convincing, so the risk banner is the part I would measure first. If staff act on it, it stays. If they scroll past it, a Radar rule should do that job.

Three false alarms caught before release

A warning that is wrong a few times gets ignored. Automated review on the pull request flagged three ways my first version would have been wrong.

  • Every dispute that was not won counted as active, so a refunded or closed one would still have raised the red banner.
  • The payment page showed alerts for the whole order, so a clean payment could carry a warning that belonged to another payment.
  • The "Disputed" badge read an older flag with the same flaw as the first point.

All three were fixed before release. The alerts now use one explicit list of active statuses, and the payment page shows only what belongs to that payment.

When the banner shows, and when it stays quiet

Action RequiredBanner and badge
Under ReviewBanner and badge
LostBanner and badge
WonNothing, the dispute is over
Charge RefundedNothing, the dispute is over
Warning ClosedNothing, the dispute is over

The first version treated every dispute that was not won as active, so a refunded or closed one would still have raised a red banner.

Working with review

The first version worked, but the logic was spread across the order model, two helpers and three controllers. Over two rounds, Bartosz and Csaba asked for a cleaner split, and it ended up as a manager that owns the queries and a presenter that only formats them for the page. I addressed each round within a working day.

That split also fixed things I had not thought of. Every query now goes through the supplier, so one tenant cannot see another's alerts. The bookings list went from up to 50 extra queries per page to two. The strings were translated into all 24 dashboard languages instead of shipping as English everywhere.

No new tables were needed. Everything the alerts show was already arriving from Stripe.

Outcome

Dispute and risk alerts are live on the order, booking and payment pages, with badges in the bookings list and the payments table. The follow-up that came out of review, loading the bookings list without the old SDK round trip, has its own ticket.

I built the dispute alerts first, because that was the incident, and asked about risk signals as soon as I saw Stripe had two things under one word. The answer was "both", and the second alert went in the same morning.

Working together

I designed and built this one, from the first question in the ticket to the merged pull request, with two engineers reviewing the code.

If you are planning something similar, I am taking on new work. Write to hello@edigil.com or use the contact page.