Bugnik
← All problems

32Support Ticket Routing API

Node.js · ExpressEasyFeatureSuggested time: 60 min

Support ticket routing REST API (implement next-ticket routing; overdue rule, languages, capacity, reopen window)

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 arrays: filter, sort with a comparator, includes
  • Asynchronous JavaScript with promises

Libraries and frameworks

  • Express 5 routers and middleware
  • Mongoose models and queries

Build and test tools

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

Reasoning

  • Sorting by several keys with tie-breaks
  • Time windows on dates and times

Background

The Customer Support Tools team at a large online retailer runs the ticket router: customers open support tickets, team leads register agents with the languages they speak, and each agent asks the queue for their next ticket. The service is a Node.js REST API (Express + MongoDB via Mongoose). The routing rule that picks an agent's next ticket was never finished, and a refactor left a few defects behind.

How the system works

Callers

  • Every request needs an x-user-id header (the caller's id, already authenticated upstream). A missing or blank header is 401 with { "error": "sign in required" }.
  • Team leads also send x-role: lead (compared case-insensitively).
  • An agent is a user that a team lead has registered (see PUT /agents/:agentId).

Errors

Every error response is { "error": "<message>" }:

  • 400 invalid input, or an :id that isn't a valid id
  • 401 no x-user-id
  • 403 the caller isn't allowed (not a lead, not a registered agent, not the ticket's agent)
  • 404 the ticket doesn't exist
  • 409 the request breaks a rule (agent at capacity, ticket not in progress)

Tickets

A ticket is returned as:

{
  "id": "665f…",
  "subject": "Parcel arrived damaged",
  "customerId": "cust-41",
  "priority": 3,
  "language": "en",
  "status": "OPEN",
  "assigneeId": null,
  "createdAt": "2026-03-02T09:00:00.000Z",
  "overdue": false
}
  • priority is 1 (most urgent) to 4 (least urgent).
  • status goes OPEN → IN_PROGRESS (when an agent takes it) → RESOLVED, and back to OPEN if the customer reopens it.
  • A ticket is overdue when it was created 4 hours or more before now (exactly 4 hours counts). The overdue field is true only while the ticket is OPEN.

Routing

POST /queue/next gives the calling agent their next ticket. Among the tickets that are OPEN, not assigned to anyone, and in one of the agent's languages, the agent gets the first one in this order:

  1. overdue tickets before tickets that aren't overdue
  2. then the lower priority number (1 before 4)
  3. then the older ticket (earlier createdAt)
  4. then the smaller id (compared as text)

Tickets that don't meet all three conditions are never picked. When no ticket fits, the agent gets nothing (204).

Endpoints

POST /tickets (any signed-in user; they become customerId): body { "subject", "priority", "language" }.

  • subject is trimmed and must be 3–120 characters.
  • priority is optional (default 3) and must be a whole number from 1 to 4.
  • language is optional (default "en"); it is trimmed and lower-cased and must then be two letters.
  • Returns 201 with the new ticket (OPEN, no assignee).

GET /tickets/mine: 200 with { "tickets": [...] }, the caller's own tickets (as customerId), newest first. Optional ?status=OPEN|IN_PROGRESS|RESOLVED (exact, upper case) keeps only that status; any other value is 400.

GET /tickets/:id: 200 with the ticket, or 404.

POST /tickets/:id/resolve: only the ticket's assigned agent may resolve it (else 403); the ticket must be IN_PROGRESS (else 409). Returns 200 with the ticket, now RESOLVED.

POST /tickets/:id/reopen: only the customer who opened the ticket may reopen it (else 403). The ticket must be RESOLVED, and now must be no more than 7 days (7 × 24 hours, inclusive) after it was resolved (else 409). It goes back to OPEN with no assignee and keeps its original createdAt, so an old ticket is overdue again straight away. Returns 200 with the ticket.

PUT /agents/:agentId (team leads only, else 403): body { "languages", "maxActive" }.

  • languages: a list of 1–5 language codes, each normalized like a ticket's language; duplicates are dropped, keeping the first occurrence's position.
  • maxActive is optional (default 3) and must be a whole number from 1 to 10.
  • Creates the agent, or replaces their languages and maxActive. Returns 200 with { "agentId", "languages", "maxActive" }.

POST /queue/next (registered agents only, else 403):

  • An agent who already has maxActive tickets IN_PROGRESS gets 409.
  • Otherwise the ticket chosen by the routing rules becomes IN_PROGRESS with the agent as assigneeId, and the response is 200 with the updated ticket. With nothing to pick: 204.
  • Two agents never get the same ticket.

GET /queue/stats (team leads only, else 403): 200 with { "open": <tickets waiting>, "inProgress": <tickets being worked on>, "byPriority": { "1": n, "2": n, "3": n, "4": n } }, where open and byPriority count tickets that are OPEN and unassigned.

Project structure

src/
  app.js                         createApp(): JSON parsing, routes, error handler
  server.js                      connects to MongoDB and starts the server
  routes/
    tickets.js                   /tickets
    agents.js                    /agents
    queue.js                     /queue
  controllers/
    ticketController.js          open, list, view, resolve and reopen tickets
    agentController.js           register agents
    queueController.js           claim the next ticket, queue stats
  middleware/
    auth.js                      x-user-id / x-role checks
    errorHandler.js              errors → status + { error }
  models/
    Ticket.js                    a support ticket
    Agent.js                     an agent's languages and capacity
  services/
    routing.js                   which ticket an agent gets next
    ticketService.js             ticket queries and updates
    agentService.js              agent lookups and updates
  utils/
    AppError.js                  an error with an HTTP status
    input.js                     reading numbers, text and language codes from request bodies
    present.js                   documents → API JSON
tests/
  routing.test.js                the routing rules
  tickets.test.js                ticket endpoints
  queue.test.js                  agent and queue endpoints
  setup/                         in-memory MongoDB for the tests

Your task

Implement pickNext in src/services/routing.js as specified in "Routing", and fix any defects that stop the test suite from passing.

  • 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.