Why coupon flows break quietly
- The email is late. Offer emails often go through a marketing tool on a schedule, minutes after sign-up, and nobody notices when it becomes an hour.
- The code works too often. A one-time code accepted twice, or after its end date, costs money every time.
- One person, many accounts. Signing up again as
name+promo1@gets a second welcome offer from many apps. See welcome offer abuse. - The details are wrong. The email says 20% and checkout gives 10%, or the email never says when the offer ends. “Ends Sunday” and “today only” count as an end date, from the day the email arrived.
How Proxy Mail QA helps
- Sign up for the offer on your staging site with a new address.
- See the coupon on the Accounts page and in the inbox: the code with Copy, its discount (“20% off”), its end date, and how long after the address was made it arrived. Past its end date it reads “Expired” and stays copyable, so you can test that checkout refuses it.
- Apply it at checkout with test payments, then record the result in the inbox: Accepted, Rejected or an error, with the message your app showed.
- Read the verdict. A coupon accepted after its end date, accepted twice when it’s one-time, or accepted for an offer one person already claimed fails. An error warns.
What to check
- The coupon arrives within the time you promise, such as 5 minutes after sign-up.
- The discount at checkout matches the email.
- A one-time code is refused the second time, with a clear message.
- An expired code is refused.
- The email says when the offer ends, and has an unsubscribe link: offers are marketing.
In a test
const submitted = new Date();
await page.getByRole('button', { name: 'Get 20% off' }).click();
// Offers often take minutes: this waits up to 10.
const coupon = await qa.waitForCode(address, { type: 'coupon', since: submitted, timeout: 600 });
expect(coupon.discount).toBe('20% off');
expect(coupon.after_seconds).toBeLessThan(300);
await page.getByLabel('Promo code').fill(coupon.value);
await page.getByRole('button', { name: 'Apply' }).click();
const first = await qa.recordCouponResult(address, 'accepted', { oneTime: true });
expect(first.check?.status).toBe('pass');
// The same code again should be refused.
await page.getByLabel('Promo code').fill(coupon.value);
await page.getByRole('button', { name: 'Apply' }).click();
const again = await qa.recordCouponResult(address, 'rejected', { message: await page.getByRole('alert').innerText() });
expect(again.check?.status).not.toBe('fail');
Agents connected to the MCP server get the same with wait_for_code (kind coupon) and record_coupon_result, and the test_coupon_checkout prompt walks through the whole round on staging.