Skip to main content

Customer Support Module

The decentralized, state-driven ticketing engine powering polymorphic customer inquiries, threaded conversations, and DPDP‑compliant privacy redaction.

MongoDB Cloudinary BullMQ Zod Mongoose


1. Executive Summary & Architecture

The Support Module (src/modules/support/) is the central communication hub bridging customers and administrators. It manages threaded conversations, supports photographic evidence via Cloudinary, and legally intertwines with the platform’s DPDP/GDPR compliance engines.

Base Route: /api/v1/support

Key architectural decisions:

DecisionImplementationConsequence
Embedded messagesMessages stored as sub‑document array inside Ticket model.One database read fetches entire conversation → O(1) dashboard load.
Polymorphic linkinglinkedEntity object with entityType (Order/Product/Return) and entityId.Single ticket can reference multiple domain entities without relational joins.
State machine SLAStatus transitions: OPENIN_PROGRESSWAITING_ON_CUSTOMERRESOLVEDCLOSED.Auto‑shifts based on sender role; closed tickets are locked forever.
Privacy by designanonymizeUserTickets scrubs user PII on account deletion but keeps admin replies for QA.Satisfies “Right to be Forgotten” without losing business analytics.

2. API Endpoints (Public & Admin)

All routes require a valid JWT (protect middleware).
Public routes are restricted to USER role; admin routes to ADMIN role.

Public (Customer) Endpoints

MethodRouteDescriptionValidation
POST/ticketsCreate a new ticketCreateTicketSchema
POST/tickets/:ticketId/replyReply to existing ticketReplyTicketSchema
GET/tickets/meFetch user’s ticket history (paginated)Query params: page, limit

Admin Endpoints

MethodRouteDescriptionValidation
GET/admin/ticketsView all tickets with filteringQuery: status, priority, category
POST/admin/tickets/:ticketId/replyAdmin replies to customerReplyTicketSchema + Cloudinary upload
PATCH/admin/tickets/:ticketId/stateUpdate status, priority, or assignmentUpdateTicketStateSchema

Security note: Customer-facing schemas explicitly exclude status, priority, and assignedAdmin. Malicious users cannot escalate their own tickets.


3. Request Flow Sequence Diagrams

3.1 Create a New Ticket

3.2 Admin Reply (State Machine Trigger)


4. Security & Payload Firewalls (Zod)

All incoming payloads are validated using Zod schemas (support.dto.ts). The schemas act as a strict gateway before data ever reaches the service layer.

CreateTicketSchema (Customer)

FieldTypeValidationNotes
subjectstringmin 5, max 150Trimmed automatically
categoryenum TicketCategoryPrevents arbitrary string injection
messagestringmin 10, max 3000Initial issue description
linkedEntityobject (optional)entityType + entityId (valid ObjectId)Polymorphic linking

ReplyTicketSchema (Both Roles)

FieldTypeValidation
messagestringmin 2, max 3000

UpdateTicketStateSchema (Admin Only)

FieldTypeValidation
statusenum TicketStatusoptional
priorityenum TicketPriorityoptional
assignedAdminstring (ObjectId)optional, but at least one field required

Additional security controls

  • Role hardcoding in controllersMessageSenderRole.USER is hardcoded in SupportPublicController.replyToTicket. A customer cannot spoof an admin reply even by manipulating the request body.
  • Log injection preventionSupportService.safeLog strips CR/LF characters before logging user IDs.
  • NoSQL injection – All queries use $eq operator with Mongoose, and Zod coerces types.
  • IDOR protectionverifyLinkedEntity cross‑checks that the user actually owns the linked Order or Return before ticket creation.

5. The State Machine & SLA

Tickets follow a strict state machine to keep the arbitration queue clean and prevent dead conversation threads.

State lock: If a ticket’s status is CLOSED, SupportService.replyToTicket throws a 403 Forbidden error — forcing the customer to open a new ticket.

Auto‑state shifts based on sender role:

SenderPrevious StatusNew Status
Admin (any)IN_PROGRESS, OPEN, RESOLVEDWAITING_ON_CUSTOMER
CustomerWAITING_ON_CUSTOMERIN_PROGRESS
CustomerOPENstays OPEN
CustomerRESOLVEDstays RESOLVED (no auto‑reopen)

6. DPDP / GDPR Compliance (Privacy Engine)

The Support Module is deeply hooked into the platform’s privacy engine, satisfying Right to be Forgotten and Right to Access.

6.1 Right to be Forgotten (Anonymization)

When a user deletes their account (UserService.deleteAccount), the ACID transaction triggers SupportService.anonymizeUserTickets:

  • Sets ticket.user = null (severs identity link).
  • Iterates through messages array, finds messages where senderRole === USER and senderId === userId.
  • Overwrites message with "[Redacted via DPDP/GDPR Right to be Forgotten]".
  • Clears attachments array (destroys photographic PII).
  • Leaves admin messages untouched.

Result: Business retains ticket analytics and admin replies for QA, but all customer PII is irreversibly destroyed.

6.2 Right to Access (Data Portability)

When the Data Export Worker compiles a user’s JSON footprint, it fetches all tickets where user === userId and includes them in the activity.supportTickets payload — satisfying data portability laws.

6.3 Assured Anonymity for Future Reporting

Because user is nullable and messages are scrubbed in place, analytics aggregations (e.g., “number of tickets per category”) remain accurate without exposing personal data.


7. Database Design (MongoDB)

The Ticket model uses an embedded message array for optimal read performance.

Schema Highlights

const MessageSchema = {
senderRole: String, // USER | ADMIN
senderId: ObjectId, // reference to User
message: String, // max 3000 chars
attachments: [String], // Cloudinary URLs
isRead: Boolean,
createdAt: Date
};

const TicketSchema = {
ticketId: String, // human-readable: TCK-XXXXXX
user: ObjectId | null, // null after anonymization
subject: String,
category: enum,
priority: enum,
status: enum,
assignedAdmin: ObjectId,
linkedEntity: { // polymorphic
entityType: enum,
entityId: ObjectId
},
messages: [MessageSchema]
};

Indexes (Performance & Security)

IndexPurpose
ticketId: uniqueFast lookup by human‑readable ID
user: 1Fetch user’s own tickets (used in /tickets/me)
status: 1, priority: -1, createdAt: -1Compound index for admin dashboard sorting – prevents unoptimised full collection scans (CWE‑400).

Automated ID Generation

Mongoose pre‑validate hook generates a unique ticketId (e.g., TCK-9A4B2C3D) using crypto.randomBytes(4) if not provided. This is both human‑friendly and URL‑safe.


8. Asynchronous Notifications (BullMQ)

To avoid blocking the HTTP response, all email/SMS notifications are dispatched fire‑and‑forget using setImmediate (or BullMQ in production).

EventNotificationRecipient
Ticket createdsendTicketCreatedNotificationCustomer
Admin repliessendTicketReplyNotificationCustomer
Status change (optional)(extensible)Admin / Customer

If the notification queue fails, the error is logged but the ticket operation succeeds — ensuring eventual consistency and zero downtime for support operations.


9. File Structure Reference

FileResponsibility
support.routes.tsRoute definitions, middleware chain (rate limit, auth, validation, upload)
support.public.controller.tsCustomer‑facing endpoints, hardcodes USER role in replies
support.admin.controller.tsAdmin dashboard endpoints, state management
support.service.tsCore business logic: create, reply, update state, anonymize, fetch
support.model.tsMongoose schemas and model, pre‑validate hook for ticketId
support.interface.tsTypeScript enums and interfaces (ITicket, ITicketMessage, etc.)
support.dto.tsZod validation schemas for request bodies
  • src/modules/notifications/notification.service.ts – email dispatcher
  • src/shared/middlewares/upload.middleware.ts – Cloudinary multer integration
  • src/shared/middlewares/role.middleware.tsrestrictTo guard
  • src/shared/middlewares/rate-limit.middleware.tsstandardLimiter

10. Security Hardening Summary

ThreatMitigation
Privilege escalationCreateTicketSchema excludes admin fields; restrictTo middleware; sender role hardcoded
IDOR (cross‑user ticket access)fetchTickets uses { user: { $eq: userId } }; verifyLinkedEntity checks ownership
NoSQL injectionMongoose $eq operator + Zod type coercion
Log injectionsafeLog strips \r and \n
Large payload / DoSZod caps message at 3000 chars; upload.array limits to 3 images; rate limiter (100 per 15 min)
Closed ticket revivalState machine rejects any reply when status === CLOSED
PII leakage after deletionanonymizeUserTickets scrubs messages and nullifies user reference

See Also


The Reshma-Core Team