Merge pull request 'fix(email): stop a demo purchase telling real customers an item sold (#206)' (#211) from feature/206-no-demo-sale-emails into main
Linting / lint (push) Successful in 2m12s
SonarQube Analysis / sonarqube (push) Failing after 16m59s

Reviewed-on: #211
This commit was merged in pull request #211.
This commit is contained in:
2026-08-28 14:02:07 -05:00
2 changed files with 55 additions and 10 deletions
+13 -2
View File
@@ -246,9 +246,20 @@ router.post('/demo/purchase', requireCustomer, asyncRoute(async (req: Request, r
const opened = await openCheckout(client, req.customerId as number, shippingAddressId, 'demo', `demo-${Date.now()}`);
if (!opened.ok) { await client.query('ROLLBACK'); return res.status(400).json({ error: opened.error }); }
const sold = await completeCheckout(client, opened.checkoutId, 'demo', null, { demo: true });
await completeCheckout(client, opened.checkoutId, 'demo', null, { demo: true });
await client.query('COMMIT');
await notifyFavoritersOfSale(sold.itemIds, sold.buyerId);
// Deliberately no notifyFavoritersOfSale here, unlike the PayPal capture and
// webhook paths above. A demo purchase is not a sale. The item really is
// marked sold, so the storefront stays truthful about availability, but the
// `favoriteSold` copy says the item "has been sold to another customer" and
// "will not be restocked" — and both are false when nobody bought anything.
//
// This is the only outbound consequence a demo purchase has. Everything else
// it does is visible to the person who clicked, who has been told it is a
// demo (#195, #203); these recipients never saw the cart and have no way to
// know. While production runs the demo interim (#191) they are real
// customers on real SMTP. See #206.
res.json({ status: 'completed' });
} catch (err) {
await client.query('ROLLBACK');
@@ -7,6 +7,7 @@ jest.mock('../../src/mailer', () => ({
sendMail: jest.fn().mockResolvedValue(undefined)
}));
import { sendMail } from '../../src/mailer';
import { notifyFavoritersOfSale } from '../../src/favoriteAlerts';
const sentMail = sendMail as jest.MockedFunction<typeof sendMail>;
beforeEach(async () => {
@@ -168,36 +169,69 @@ describe('notifying when a favorited item sells', () => {
expect(purchase.status).toBeLessThan(400);
}
// The demo route deliberately notifies nobody (#206) and the PayPal path has
// no integration coverage, so the tests below call the notifier the way the
// PayPal capture and webhook routes do — after the purchase, with the sold
// ids and the buyer. That keeps them about *who* gets told, which is what
// they were testing all along; whether the demo route itself notifies is
// asserted separately, above.
async function buyThenNotify(
agent: ReturnType<typeof request.agent>,
itemId: number,
buyerId: number | null
) {
await buyViaDemo(agent, itemId);
await notifyFavoritersOfSale([itemId], buyerId);
}
it('emails a favoriter who opted in', async () => {
const itemId = await createItem('Wanted item');
const { agent: watcher } = await register('watcher@example.com');
await watcher.post(`/api/customers/me/favorites/${itemId}`);
await watcher.put('/api/customers/me/favorite-alerts').send({ enabled: true });
const { agent: buyer } = await register('buyer@example.com');
await buyViaDemo(buyer, itemId);
const { agent: buyer, id: buyerId } = await register('buyer@example.com');
await buyThenNotify(buyer, itemId, buyerId);
expect(soldNotificationsTo()).toEqual(['watcher@example.com']);
});
// A demo purchase is not a sale. The item really is marked sold, so the
// storefront is telling the truth about availability, but nobody bought
// anything and nobody is shipping anything — and while production runs the
// demo interim (#191) this mail reaches real favoriters through real SMTP,
// telling them an item "has been sold to another customer" and "will not be
// restocked". Both are false. See #206.
it('sends nothing when the purchase was a demo', async () => {
const itemId = await createItem('Demo bought');
const { agent: watcher } = await register('watcher-demo@example.com');
await watcher.post(`/api/customers/me/favorites/${itemId}`);
await watcher.put('/api/customers/me/favorite-alerts').send({ enabled: true });
const { agent: buyer } = await register('demo-buyer@example.com');
await buyViaDemo(buyer, itemId);
expect(soldNotificationsTo()).toEqual([]);
});
it('does not email a favoriter who never opted in', async () => {
const itemId = await createItem('Wanted item');
const { agent: watcher } = await register('silent@example.com');
await watcher.post(`/api/customers/me/favorites/${itemId}`);
const { agent: buyer } = await register('buyer2@example.com');
await buyViaDemo(buyer, itemId);
const { agent: buyer, id: buyerId } = await register('buyer2@example.com');
await buyThenNotify(buyer, itemId, buyerId);
expect(soldNotificationsTo()).toEqual([]);
});
it('does not tell the buyer their own purchase is unavailable', async () => {
const itemId = await createItem('Self bought');
const { agent: buyer } = await register('selfbuy@example.com');
const { agent: buyer, id: buyerId } = await register('selfbuy@example.com');
await buyer.post(`/api/customers/me/favorites/${itemId}`);
await buyer.put('/api/customers/me/favorite-alerts').send({ enabled: true });
await buyViaDemo(buyer, itemId);
await buyThenNotify(buyer, itemId, buyerId);
expect(soldNotificationsTo()).toEqual([]);
});
@@ -210,10 +244,10 @@ describe('notifying when a favorited item sells', () => {
await agent.put('/api/customers/me/favorite-alerts').send({ enabled: true });
}
const { agent: buyer } = await register('c@example.com');
const { agent: buyer, id: buyerId } = await register('c@example.com');
await buyer.post(`/api/customers/me/favorites/${itemId}`);
await buyer.put('/api/customers/me/favorite-alerts').send({ enabled: true });
await buyViaDemo(buyer, itemId);
await buyThenNotify(buyer, itemId, buyerId);
expect(soldNotificationsTo().sort()).toEqual(['a@example.com', 'b@example.com']);
});