feat: tell favoriters when an item is withdrawn (#34)

Deleting an item cascades its favorites away, so anyone watching it lost the record silently and heard nothing. Recipients are now collected before the delete, since after it there is nobody left to look up, and the mail is sent only once the delete has succeeded so nobody hears about a withdrawal that did not happen.

Items that had already sold are excluded. Their favoriters were told at the point of sale, and a second "no longer available" for the same item reads as a duplicate rather than news.

The wording differs from the sale notification — withdrawn rather than sold — because the customer did not lose out to another buyer and saying so would be untrue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-18 14:40:16 -05:00
co-authored by Claude Opus 5
parent 2fd8a1481e
commit c4fb47853f
3 changed files with 142 additions and 31 deletions
+15 -4
View File
@@ -8,7 +8,7 @@ import { ADMIN_ITEM_SELECT } from '../itemSelect';
import { asyncRoute } from '../asyncRoute';
import { parseItemFilters, buildItemFilterSql, FilterError } from '../itemFilters';
import { tagColorFor } from '../utils';
import { notifyFavoritersOfSale } from '../favoriteAlerts';
import { notifyFavoritersOfSale, notifyFavoritersOfRemoval, collectFavoriteRecipients } from '../favoriteAlerts';
const router = Router();
@@ -229,10 +229,21 @@ router.put('/items/:id', uploadImages, async (req: Request, res: Response) => {
}
});
router.delete('/items/:id', async (req: Request, res: Response) => {
await pool.query(`DELETE FROM items WHERE id = $1`, [req.params.id]);
router.delete('/items/:id', asyncRoute(async (req: Request, res: Response) => {
const itemId = Number(req.params.id);
// Collected before the delete: favorites cascade with the item, so after it
// is gone there is no record of who was watching. Restricted to unsold items
// because anyone watching a sold one has already been told it went.
const recipients = await collectFavoriteRecipients([itemId], null, true);
await pool.query(`DELETE FROM items WHERE id = $1`, [itemId]);
// Sent only once the delete has succeeded, so nobody hears about a withdrawal
// that did not happen.
notifyFavoritersOfRemoval(recipients);
res.status(204).end();
});
}));
router.delete('/items/:id/images/:imageId', async (req: Request, res: Response) => {
await pool.query(`DELETE FROM item_images WHERE id = $1 AND item_id = $2`, [req.params.imageId, req.params.id]);