fix(email): stop a demo purchase telling real customers an item sold (#206) #211
@@ -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()}`);
|
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 }); }
|
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 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' });
|
res.json({ status: 'completed' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await client.query('ROLLBACK');
|
await client.query('ROLLBACK');
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ jest.mock('../../src/mailer', () => ({
|
|||||||
sendMail: jest.fn().mockResolvedValue(undefined)
|
sendMail: jest.fn().mockResolvedValue(undefined)
|
||||||
}));
|
}));
|
||||||
import { sendMail } from '../../src/mailer';
|
import { sendMail } from '../../src/mailer';
|
||||||
|
import { notifyFavoritersOfSale } from '../../src/favoriteAlerts';
|
||||||
const sentMail = sendMail as jest.MockedFunction<typeof sendMail>;
|
const sentMail = sendMail as jest.MockedFunction<typeof sendMail>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -168,36 +169,69 @@ describe('notifying when a favorited item sells', () => {
|
|||||||
expect(purchase.status).toBeLessThan(400);
|
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 () => {
|
it('emails a favoriter who opted in', async () => {
|
||||||
const itemId = await createItem('Wanted item');
|
const itemId = await createItem('Wanted item');
|
||||||
const { agent: watcher } = await register('watcher@example.com');
|
const { agent: watcher } = await register('watcher@example.com');
|
||||||
await watcher.post(`/api/customers/me/favorites/${itemId}`);
|
await watcher.post(`/api/customers/me/favorites/${itemId}`);
|
||||||
await watcher.put('/api/customers/me/favorite-alerts').send({ enabled: true });
|
await watcher.put('/api/customers/me/favorite-alerts').send({ enabled: true });
|
||||||
|
|
||||||
const { agent: buyer } = await register('buyer@example.com');
|
const { agent: buyer, id: buyerId } = await register('buyer@example.com');
|
||||||
await buyViaDemo(buyer, itemId);
|
await buyThenNotify(buyer, itemId, buyerId);
|
||||||
|
|
||||||
expect(soldNotificationsTo()).toEqual(['watcher@example.com']);
|
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 () => {
|
it('does not email a favoriter who never opted in', async () => {
|
||||||
const itemId = await createItem('Wanted item');
|
const itemId = await createItem('Wanted item');
|
||||||
const { agent: watcher } = await register('silent@example.com');
|
const { agent: watcher } = await register('silent@example.com');
|
||||||
await watcher.post(`/api/customers/me/favorites/${itemId}`);
|
await watcher.post(`/api/customers/me/favorites/${itemId}`);
|
||||||
|
|
||||||
const { agent: buyer } = await register('buyer2@example.com');
|
const { agent: buyer, id: buyerId } = await register('buyer2@example.com');
|
||||||
await buyViaDemo(buyer, itemId);
|
await buyThenNotify(buyer, itemId, buyerId);
|
||||||
|
|
||||||
expect(soldNotificationsTo()).toEqual([]);
|
expect(soldNotificationsTo()).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not tell the buyer their own purchase is unavailable', async () => {
|
it('does not tell the buyer their own purchase is unavailable', async () => {
|
||||||
const itemId = await createItem('Self bought');
|
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.post(`/api/customers/me/favorites/${itemId}`);
|
||||||
await buyer.put('/api/customers/me/favorite-alerts').send({ enabled: true });
|
await buyer.put('/api/customers/me/favorite-alerts').send({ enabled: true });
|
||||||
|
|
||||||
await buyViaDemo(buyer, itemId);
|
await buyThenNotify(buyer, itemId, buyerId);
|
||||||
|
|
||||||
expect(soldNotificationsTo()).toEqual([]);
|
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 });
|
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.post(`/api/customers/me/favorites/${itemId}`);
|
||||||
await buyer.put('/api/customers/me/favorite-alerts').send({ enabled: true });
|
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']);
|
expect(soldNotificationsTo().sort()).toEqual(['a@example.com', 'b@example.com']);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user