Bugnik
← All problems

35Back-in-Stock Alerts API

Node.js · ExpressEasyBug fixSuggested time: 60 min

Back-in-stock alerts REST API (restock queue, channel defaults, alert limits, outbox)

New to repository-based OA questions? Read the guide.

Sign in to start

Before you start

You'll get more out of this problem if you're comfortable with:

Language

  • JavaScript objects and arrays

Libraries and frameworks

  • Express 5 routers and handlers
  • Mongoose queries with sort, and saving documents

Build and test tools

  • Reading Node.js syntax errors and Jest + Supertest failures

Reasoning

  • First-come, first-served queues with a limit

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-id header (already authenticated upstream). A missing or blank header is 401 with { "error": "sign in required" }.
  • The catalogue team also sends x-role: catalog, and the sending service x-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" }.

  • name is trimmed, 1–100 characters; inStock is 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" }.

  • channel is EMAIL, SMS or PUSH (upper-cased). A shopper who doesn't send a channel gets EMAIL, whatever anyone else chose.
  • status is ACTIVE (waiting), NOTIFIED or CANCELLED.

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 ACTIVE alert for this product, it is returned unchanged with 200 (its channel isn't changed).
  • A shopper may have at most 10 ACTIVE alerts; another one is 409.
  • Otherwise returns 201 with the new ACTIVE alert.

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.json or jest.config.js.
  • Keep your changes minimal. Fix the defects; don't rewrite modules.
  • Additional hidden tests check the behaviour described in this README.