Background
The Catalogue team at a large online retailer lets shoppers ask to be told when an out-of-stock product comes back. When the catalogue restocks a product, the shoppers who have waited longest get the first notifications. The service is a Node.js REST API (Express + MongoDB via Mongoose). The latest release doesn't start, and shoppers are getting alerts on channels they never picked.
How the system works
Callers
- Every request needs an
x-user-idheader (already authenticated upstream). A missing or blank header is401with{ "error": "sign in required" }. - The catalogue team also sends
x-role: catalog, and the sending servicex-role: notifier(roles are compared case-insensitively). Any other signed-in user is a shopper.
Errors
Every error response is { "error": "<message>" }: 400 invalid input, 401 no x-user-id,
403 not the catalogue team, 404 unknown product or alert, 409 a rule was broken.
Products
A SKU is trimmed and upper-cased, then must be 3–20 characters of A–Z, 0–9 or - (else 400).
A product is returned as
{ "sku", "name", "inStock", "lastRestockedAt", "waiting" }, where waiting is the number of ACTIVE
alerts for it and lastRestockedAt is the time of its latest restock (or null).
PUT /products/:sku (catalogue team only, else 403): body { "name", "inStock" }.
nameis trimmed, 1–100 characters;inStockis a whole number from 0 to 1000000.- Creates the product (
201) or updates it (200). The response is the product plus"notified": how many shoppers this request notified. - Restock: when the product had 0 units (or didn't exist) and now has more than 0, the waiting shoppers are notified (below). Any other change notifies nobody.
GET /products/:sku (signed in): 200 with the product, or 404.
Alerts
An alert is returned as { "sku", "shopperId", "channel", "status", "createdAt", "notifiedAt" }.
channelisEMAIL,SMSorPUSH(upper-cased). A shopper who doesn't send a channel getsEMAIL, whatever anyone else chose.statusisACTIVE(waiting),NOTIFIEDorCANCELLED.
POST /products/:sku/alerts (signed in): body { "channel" } (optional).
- Unknown product:
404. An invalid channel:400. A product with units in stock:409. - If the shopper already has an
ACTIVEalert for this product, it is returned unchanged with200(its channel isn't changed). - A shopper may have at most 10
ACTIVEalerts; another one is409. - Otherwise returns
201with the newACTIVEalert.
DELETE /products/:sku/alerts (signed in): cancels the shopper's ACTIVE alert for the product
(204), or 404 when there is none.
GET /me/alerts (signed in): 200 with { "alerts": [...] }, the shopper's alerts that aren't
cancelled, newest createdAt first. Optional ?status=ACTIVE or ?status=NOTIFIED keeps only that
status; anything else is 400.
Notifying on restock
On a restock with N units, the ACTIVE alerts for the product are taken oldest createdAt first
(alerts created at the same moment: the one stored first). The first min(N, 100) of them become
NOTIFIED with notifiedAt set to now; the rest stay ACTIVE. Cancelled alerts are never notified.
A shopper who was notified can sign up again (a new alert).
Each notified alert also puts a message in the outbox for the sending service, with the alert's
shopperId, sku and channel, and the text "<product name> is back in stock". For SMS, a text
longer than 60 characters is cut to its first 59 characters followed by … (exactly 60 in total).
Outbox
A message is returned as { "id", "shopperId", "sku", "channel", "text", "createdAt", "sentAt" }.
GET /outbox (sending service only, else 403): 200 with { "messages": [...] }, the messages not
sent yet, oldest first (same moment: the one stored first). Optional ?channel= keeps one channel;
an invalid channel is 400.
POST /outbox/:id/sent (sending service only): marks the message sent (sentAt = now) and returns
200 with it. Marking it again keeps the first sentAt. Unknown message: 404.
Project structure
src/
app.js createApp(): JSON parsing, routes, error handler
server.js connects to MongoDB and starts the server
routes/
products.js /products
me.js /me
outbox.js /outbox
controllers/
productController.js products, signing up and cancelling
meController.js the shopper's alerts
outboxController.js pending messages, marking them sent
middleware/
auth.js x-user-id and role checks
errorHandler.js errors → status + { error }
models/
Product.js a product and its stock
Alert.js a shopper's alert
Message.js a notification for the sending service
services/
productService.js saving products, detecting restocks
alertService.js alert rules and notifications
outboxService.js message texts and the outbox
utils/
AppError.js an error with an HTTP status
input.js reading SKUs, text and channels
present.js documents → API JSON
tests/
alerts.test.js alert endpoints
restock.test.js products and restock notifications
outbox.test.js outbox messages
setup/ in-memory MongoDB for the tests
Your task
Shoppers and the catalogue team report these problems:
- The service doesn't start.
- Some shoppers get alerts on a channel they never chose.
- When stock comes back, fewer waiting shoppers are told than there are units.
Find and fix the defects so that all tests pass.
- Do not modify
tests/,package.jsonorjest.config.js. - Keep your changes minimal. Fix the defects; don't rewrite modules.
- Additional hidden tests check the behaviour described in this README.