Service Request Service API

The Service Request module is part of the Organisation Service (internal port 9107, gateway prefix /organisation). It exposes two distinct route groups:

  • /admin/service-requests/* — used by the Admin Portal (support staff, requires isAdmin === true).
  • /hub/service-requests/* — used by the Hub (organisation clients, scoped server-side to the caller’s defaultOrganisationId).

All endpoints require a valid Bearer token (Authorization: Bearer <accessToken>).


Concepts

Types

Type Use
INCIDENT An unplanned interruption / degradation of service.
BUG A defect in the product to be reproduced and fixed.
CHANGE_REQUEST A planned change requiring approval and a rollback plan.
GENERAL Anything else (Q&A, feature ask, billing query…).

Status lifecycle

NEW → OPEN → IN_PROGRESS ⇄ PENDING_CUSTOMER ⇄ ON_HOLD
IN_PROGRESS → RESOLVED → CLOSED
RESOLVED → REOPENED → IN_PROGRESS
*CANCELLED   (admin only, except NEW/OPEN which a client can also cancel)
CLOSED → REOPENED  (within reopen window)

Invalid transitions return HTTP 409. Cancelling once a request is IN_PROGRESS requires admin.

Priority / Severity / Urgency / Impact

Field Values
priority LOW, MEDIUM (default), HIGH, URGENT
severity S1, S2, S3, S4
urgency LOW, MEDIUM, HIGH
impact LOW, MEDIUM, HIGH

Type-specific metadata (typeMeta)

typeMeta: {
  bug?: {
    environment?: 'DEV' | 'STAGING' | 'PROD';
    affectedVersion?: string;
    browser?: string;
    os?: string;
    stepsToReproduce?: string;
    expectedBehaviour?: string;
    actualBehaviour?: string;
    affectedModule?: string;
  };
  incident?: {
    affectedService?: string;
    affectedUsersCount?: number;
    incidentStartAt?: ISODate;
    incidentEndAt?: ISODate;
    rootCause?: string;
    workaround?: string;
    postMortemUrl?: string;
  };
  change?: {
    changeType?: 'STANDARD' | 'NORMAL' | 'EMERGENCY';
    proposedChange?: string;
    justification?: string;
    riskLevel?: 'LOW' | 'MEDIUM' | 'HIGH';
    rollbackPlan?: string;
    plannedStartAt?: ISODate;
    plannedEndAt?: ISODate;
    // Set by the approve-change endpoint:
    approvalStatus?: 'PENDING' | 'APPROVED' | 'REJECTED';
    approverId?: string;
    approvedAt?: ISODate;
    approvalNote?: string;
  };
}

Ticket numbers

Auto-assigned in the form SR-YYYY-NNNNNN (zero-padded, monotonically increasing per year). Generated atomically; safe under concurrency.


Admin endpoints

Base prefix: /organisation/admin/service-requests (via gateway). All require an admin user.

POST /admin/service-requests

Create a request. Admin can target any organisation, leave organisationId = null for a standalone request, or set onBehalfOfUserId to record that the request was raised on behalf of another user.

{
  "type": "BUG",
  "title": "Pricing page 500s on Safari",
  "description": "Repro steps inside",
  "priority": "HIGH",
  "severity": "S2",
  "category": "Frontend",
  "organisationId": "1f2c…",     // optional, null for standalone
  "onBehalfOfUserId": "9a3e…",   // optional
  "assigneeId": "auser…",         // optional
  "tags": ["safari", "p1"],
  "typeMeta": {
    "bug": { "environment": "PROD", "browser": "Safari 17", "stepsToReproduce": "…" }
  }
}

Response 201 Created — full admin view of the request, including the assigned ticketNumber.

GET /admin/service-requests

List with filters (all optional, query-string):

Param Notes
type Single INCIDENT / BUG / CHANGE_REQUEST / GENERAL
status Comma-separated statuses
priority Comma-separated priorities
organisationId Filter by organisation
assigneeId, requesterId, tag Exact-match filters
q Case-insensitive substring on title/description/ticketNumber
createdFrom, createdTo ISO 8601
page (default 1), pageSize (default 20, max 100) Pagination
sort Comma-separated; prefix with - for descending. Default: -createdAt
includeDeleted true to include soft-deleted rows

Response: { items, page, pageSize, total, totalPages }.

GET /admin/service-requests/stats

Aggregated counts for dashboards. Optional organisationId, assigneeId filters.

{
  "total": 124,
  "slaBreached": 3,
  "byStatus":   { "NEW": 12, "IN_PROGRESS": 40, "RESOLVED": 60, "CLOSED": 12 },
  "byType":     { "BUG": 70, "INCIDENT": 30, "CHANGE_REQUEST": 14, "GENERAL": 10 },
  "byPriority": { "LOW": 20, "MEDIUM": 60, "HIGH": 35, "URGENT": 9 }
}

GET /admin/service-requests/:id

Returns the full admin view (including internal comments, complete history).

PATCH /admin/service-requests/:id

Update any of: title, description, priority, severity, urgency, impact, category, subCategory, tags, dueAt, assigneeId, assigneeTeam, typeMeta. Each changed field is recorded as a history entry.

POST /admin/service-requests/:id/status

{ "status": "IN_PROGRESS", "note": "Engineer assigned, investigating" }

Returns 409 if the transition isn’t allowed for admins. RESOLVED sets resolvedAt; CLOSED sets closedAt; REOPENED increments reopenCount.

POST /admin/service-requests/:id/assign

{ "assigneeId": "uuid", "assigneeTeam": "Tier-2 Support", "note": "L2 escalation" }

Either field can be supplied. Triggers an assignee.changed email notification to the assignee (if their email is on file).

POST /admin/service-requests/:id/comments & GET /admin/service-requests/:id/comments

Add or list comments. Admins can post internal comments ("isInternal": true) which are hidden from the Hub view. The first non-internal comment sets firstResponseAt if it isn’t already set.

POST /admin/service-requests/:id/attachments (multipart)

Field name: file. Up to 10 MB. Allowed types: PNG/JPG/WEBP/GIF, PDF, plain text/CSV/JSON, ZIP, DOC(X)/XLS(X). Stored on Cloudinary under zeswa/service-requests/<id>/. Returns the updated request.

DELETE /admin/service-requests/:id/attachments/:attachmentId

Removes an attachment from the request and records the action in history.

GET /admin/service-requests/:id/history

Full audit trail (every status change, field edit, comment, attachment, assignment).

POST /admin/service-requests/:id/approve-change

Only valid when type === "CHANGE_REQUEST".

{ "decision": "APPROVED", "note": "Reviewed with infra; rollback ok" }

Sets typeMeta.change.approvalStatus, approverId, approvedAt, approvalNote.

DELETE /admin/service-requests/:id

Soft delete. Sets deletedAt; the request disappears from default lists. Recoverable via ?includeDeleted=true on the list endpoint and a manual DB restore (no API to undelete).


Hub endpoints

Base prefix: /organisation/hub/service-requests. All require a logged-in user with a defaultOrganisationId. The server always scopes reads/writes to that organisation; clients cannot read other organisations’ requests.

POST /hub/service-requests

{
  "type": "BUG",
  "title": "Cannot upload avatar",
  "description": "When I select a PNG it fails with 500",
  "priority": "HIGH",
  "tags": ["avatar"],
  "typeMeta": { "bug": { "environment": "PROD", "browser": "Chrome 120" } }
}

organisationId, requesterId, requesterType are set server-side.

GET /hub/service-requests

Same filter shape as admin minus organisationId (forced to caller’s org). Add mineOnly=true to restrict to the caller’s own requests. Internal comments are excluded.

GET /hub/service-requests/:id

Returns hub view. 404 if the request does not belong to the caller’s organisation (intentionally indistinguishable from “not found”).

PATCH /hub/service-requests/:id

Edit title / description / category / subCategory / tags / typeMeta while the request is in an editable state (NEW, OPEN, REOPENED, PENDING_CUSTOMER). Only the original requester may edit.

POST /hub/service-requests/:id/comments

Public comment only (isInternal is ignored; always false).

POST /hub/service-requests/:id/attachments (multipart)

Same rules as the admin attachment endpoint.

POST /hub/service-requests/:id/cancel

Sets status to CANCELLED. Allowed only while the request is NEW or OPEN. Only the requester may cancel.

POST /hub/service-requests/:id/confirm-resolution

Confirms a RESOLVED ticket and closes it; optional CSAT:

{ "rating": 5, "comment": "Quick fix, thanks!" }

POST /hub/service-requests/:id/reopen

Re-opens a RESOLVED or CLOSED ticket; increments reopenCount.


Notifications

The platform queues an email via the existing BullMQ email-queue on these events:

Event Recipient
Ticket created Requester
Status changed Requester
Assignee changed New assignee

Templates live under emails/service-request/.


Errors

Status When
400 Validation failure (Zod returns the first error message and the full errors array).
401 Missing / invalid token, or the authenticated user record is gone.
403 Admin endpoint hit by a non-admin; or non-requester editing a hub request.
404 Request not found (or not in your organisation, on Hub routes).
409 Disallowed status transition; approving a non-CHANGE_REQUEST; or wrong type/state.