DEV Community

Payneteasy
Payneteasy

Posted on

A card is valid through the last day of its expiry month

Found this in a checkout form validator that had been in production for two years. The expiry check parsed MM/YY, built new Date(2000 + yy, mm - 1) and rejected the card if that date was before now.

That constructor returns the first of the month at 00:00. So a card printed 10/26 was rejected from October 2 onward, even though cards stay valid until the last day of the printed month. For roughly 30 days a month, anyone with a card about to expire got "card expired" on a card the issuer would still approve.

The symptom was odd. Support tickets clustered around the start of each month, and the failures never reached the gateway, so the decline dashboard looked clean. The only trace was a client-side validation event nobody charted.

The fix is one line: compare against the first day of the month after expiry, new Date(2000 + yy, mm, 1), and reject only if now is at or past it. Also check the year parsing. Our old code accepted 10/2 as year 2002 and handed back a confusing message.

It makes me wonder how much money sits in failures that happen before the request leaves the browser. Do you track client-side validation rejects as a metric, or only what the gateway returns?

Payneteasy handles this kind of payment flow for merchants and PSPs — see how it works.

Top comments (1)

Collapse
 
crtdrops profile image
Israel Negrete Lepe •

The part that stays with me is "the decline dashboard looked clean". I wrote about the same shape in telemetry: a parser that drops a frame it doesn't like, with no error and no log, so the gaps only show up weeks later in a history nobody can reconstruct. The failures that hurt are the ones that happen before the first point you count.

To your question: I'd count a reject at every boundary that can say no, with its reason, the browser included. And I'd make the browser the most lenient of them. Near the end of the month, "now" in the buyer's time zone and in the issuer's aren't the same day, so the edge should be the issuer's call, not new Date()'s.