Skip to content

feat: Implement a support ticket system with client and admin interfa…#227

Open
vendz wants to merge 8 commits into
mainfrom
feat/central-ticketing-system
Open

feat: Implement a support ticket system with client and admin interfa…#227
vendz wants to merge 8 commits into
mainfrom
feat/central-ticketing-system

Conversation

@vendz
Copy link
Copy Markdown
Member

@vendz vendz commented Nov 27, 2025

…ces, including models, controllers, routes, and tests.

…ces, including models, controllers, routes, and tests.
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @vendz, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a complete support ticket system, providing distinct interfaces for clients to submit and track issues, and for administrators to manage and resolve them. The changes encompass new database models, API endpoints, and controllers for both user roles, ensuring a structured and functional approach to handling support requests. Integration tests are also included to validate the core client-side features.

Highlights

  • New Ticket System Implementation: A comprehensive support ticket system has been introduced, enabling both clients and administrators to manage support requests efficiently.
  • Client-Side Functionality: Clients can now create new support tickets, view their existing tickets, retrieve detailed information for specific tickets, and add messages to ongoing conversations.
  • Admin-Side Management: Administrators gain the ability to view all tickets, filter them by status or service, access detailed ticket information including messages, add internal comments, and update ticket statuses.
  • New Database Models: Two new Sequelize models, Ticket and TicketMessage, have been added to the database schema to store ticket details and their associated message history.
  • Dedicated API Endpoints: New API routes have been established for both client and admin interfaces, integrating the ticket system seamlessly into the application's existing API structure.
  • Integration Tests: A new suite of integration tests has been implemented to ensure the robust functionality of the client-side ticket system, covering creation, messaging, and retrieval.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new support ticket system, which is a great feature. The implementation is comprehensive, covering models, controllers, routes, and tests for both client and admin interfaces.

My review focuses on improving efficiency, maintainability, and test robustness. I've identified a few areas for improvement:

  • Database Query Efficiency: Both client and admin controllers for fetching ticket details perform two separate database queries. This can be optimized into a single query by defining and using Sequelize associations.
  • Code Consistency: There's a minor inconsistency in the Ticket model where a status is hardcoded instead of using a constant.
  • Test Cleanup: The test cleanup logic could be more robust to avoid potential conflicts with other tests.

I've provided specific suggestions to address these points. Overall, this is a solid contribution.

Comment on lines +31 to +45
const ticket = await Ticket.findByPk(id);
if (!ticket) {
throw new ApiError(404, 'Ticket not found');
}

const messages = await TicketMessage.findAll({
where: { ticket_id: id },
order: [['createdAt', 'ASC']]
});

res.status(200).json({
status: 'success',
message: MSG_FETCH_SUCCESSFUL,
data: { ...ticket.toJSON(), messages }
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This function is inefficient as it performs two separate database queries to fetch a ticket and its associated messages. This can be optimized into a single query by properly defining the Sequelize associations between Ticket and TicketMessage models.

Once the association is defined (e.g., Ticket.hasMany(TicketMessage, { as: 'messages', ... })), you can use include to fetch everything at once. This simplifies the code, removes the need for manual data merging, and improves performance.

Suggested change
const ticket = await Ticket.findByPk(id);
if (!ticket) {
throw new ApiError(404, 'Ticket not found');
}
const messages = await TicketMessage.findAll({
where: { ticket_id: id },
order: [['createdAt', 'ASC']]
});
res.status(200).json({
status: 'success',
message: MSG_FETCH_SUCCESSFUL,
data: { ...ticket.toJSON(), messages }
});
const ticket = await Ticket.findByPk(id, {
include: [
{
model: TicketMessage,
as: 'messages', // This alias must match the one in the association definition
},
],
order: [[{ model: TicketMessage, as: 'messages' }, 'createdAt', 'ASC']],
});
if (!ticket) {
throw new ApiError(404, 'Ticket not found');
}
res.status(200).json({
status: 'success',
message: MSG_FETCH_SUCCESSFUL,
data: ticket,
});

Comment on lines +53 to +80
const ticket = await Ticket.findOne({
where: { id, issued_by: cardno },
include: [
{
model: TicketMessage,
as: 'messages', // Note: We need to define associations
order: [['createdAt', 'ASC']]
}
]
});

if (!ticket) {
throw new ApiError(404, 'Ticket not found');
}

// Fetch messages separately if association issue persists, but ideally association should work.
// For now, let's assume we'll add association in a separate step or here.
// Actually, let's fetch messages manually to be safe if associations aren't set up in models yet.
const messages = await TicketMessage.findAll({
where: { ticket_id: id },
order: [['createdAt', 'ASC']]
});

res.status(200).json({
status: 'success',
message: MSG_FETCH_SUCCESSFUL,
data: { ...ticket.toJSON(), messages }
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This function is inefficient as it performs two separate database queries to fetch a ticket and its associated messages. This can be optimized into a single query by properly defining the Sequelize associations between Ticket and TicketMessage models.

Once the association is defined (e.g., Ticket.hasMany(TicketMessage, { as: 'messages', ... })), you can use include to fetch everything at once. This simplifies the code, removes the need for manual data merging, and improves performance.

  const ticket = await Ticket.findOne({
    where: { id, issued_by: cardno },
    include: [
      {
        model: TicketMessage,
        as: 'messages', // This alias must match the one in the association definition
      },
    ],
    // To order nested models, you need to specify the model and alias
    order: [[{ model: TicketMessage, as: 'messages' }, 'createdAt', 'ASC']],
  });

  if (!ticket) {
    throw new ApiError(404, 'Ticket not found');
  }

  res.status(200).json({
    status: 'success',
    message: MSG_FETCH_SUCCESSFUL,
    data: ticket,
  });

Comment thread models/ticket.model.js
},
status: {
type: DataTypes.ENUM,
values: [STATUS_OPEN, STATUS_INPROGRESS, 'resolved', STATUS_CLOSED],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The status enum values are a mix of constants (e.g., STATUS_OPEN) and a hardcoded string 'resolved'. For consistency and to avoid magic strings, it's better to define a constant for 'resolved' in config/constants.js (e.g., STATUS_RESOLVED) and use it here.

Suggested change
values: [STATUS_OPEN, STATUS_INPROGRESS, 'resolved', STATUS_CLOSED],
values: [STATUS_OPEN, STATUS_INPROGRESS, STATUS_RESOLVED, STATUS_CLOSED],

Comment thread tests/tickets.test.js
Comment on lines +18 to +28
afterAll(async () => {
// Cleanup
if (user) {
await TicketMessage.destroy({ where: {} });
await Ticket.destroy({ where: {} });
// We might not want to delete the user if other tests rely on it, but for this isolated test it's fine.
// However, CardFactory creates real records.
// Let's just leave it or try to clean up.
}
await sequelize.close();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The afterAll hook uses destroy({ where: {} }), which deletes all records from the TicketMessage and Ticket tables. This is a brittle approach that can cause other tests to fail if they run in parallel or rely on pre-existing data. The cleanup should be scoped to only the data created within this test suite.

A more robust approach is to delete only the specific records created during the test.

  afterAll(async () => {
    // Cleanup
    if (ticketId) {
      // To avoid deleting data from other tests, we should be specific.
      await TicketMessage.destroy({ where: { ticket_id: ticketId } });
      await Ticket.destroy({ where: { id: ticketId } });
    }
    await sequelize.close();
  });

…et status to 'in progress' upon new messages.
… management to use `req.user.username` for sender and updater, and reorder admin status check.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a comprehensive support ticket system with separate client and admin interfaces, including database models, API controllers, routes, and test coverage.

  • Creates a two-way communication system between users and administrators through tickets and messages
  • Implements role-based access control with separate client and admin endpoints
  • Adds complete test coverage for the ticket creation and messaging workflows

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
models/ticket.model.js Defines the Ticket model with fields for tracking support requests including service type, description, device info, and status
models/ticket_message.model.js Defines the TicketMessage model for storing conversation threads between users and admins
models/associations.js Establishes relationships between Ticket, TicketMessage, and CardDb models
controllers/client/ticket.controller.js Implements client-facing endpoints for creating tickets, viewing tickets, adding messages, and resolving tickets
controllers/admin/ticketManagement.controller.js Implements admin endpoints for viewing all tickets, managing ticket status, and responding to users
routes/client/ticket.routes.js Defines client API routes with card validation middleware
routes/admin/ticketManagement.routes.js Defines admin API routes with authentication and role-based authorization
app.js Registers the new ticket routes for both client and admin interfaces
controllers/admin/auth.controller.js Code formatting improvements (unrelated to ticket system)
tests/tickets.test.js Provides comprehensive test coverage for ticket creation, messaging, and retrieval functionality

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread models/ticket.model.js
Comment on lines +41 to +46
status: {
type: DataTypes.ENUM,
values: [STATUS_OPEN, STATUS_INPROGRESS, 'resolved', STATUS_CLOSED],
defaultValue: STATUS_OPEN,
allowNull: false
}
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Ticket model is missing an 'updatedBy' field, but both controllers (client/ticket.controller.js lines 104, 134 and admin/ticketManagement.controller.js lines 73, 96) attempt to update this field. This will cause the updates to fail silently or be ignored by Sequelize. Additionally, the status enum includes a hard-coded 'resolved' string instead of using a constant from config/constants.js (like STATUS_INPROGRESS and STATUS_CLOSED). This is inconsistent with the codebase convention seen in other models like maintenance_db.model.js.

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +107
if (ticket.status === 'closed') {
throw new ApiError(400, 'Cannot reply to a closed ticket');
}

await TicketMessage.create({
ticket_id,
sender_id: cardno,
sender_type: 'user',
message
});

// If ticket was resolved, move back to in progress since user is replying
const updates = { updatedBy: cardno };
if (ticket.status === 'resolved') {
updates.status = 'in progress';
}
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded status strings 'closed' and 'in progress' are used instead of the constants STATUS_CLOSED and STATUS_INPROGRESS from config/constants.js. This is inconsistent with the codebase convention as seen in other controllers like admin/maintenanceManagement.controller.js which consistently uses STATUS_OPEN, STATUS_CLOSED, and STATUS_INPROGRESS constants. Using hard-coded strings makes the code more error-prone and harder to maintain.

Copilot uses AI. Check for mistakes.
Comment on lines +128 to +134
if (ticket.status === 'closed') {
throw new ApiError(400, 'Ticket is already closed');
}

await ticket.update({
status: 'closed',
updatedBy: cardno
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded status strings 'closed' and 'resolved' are used instead of constants. The model defines 'resolved' as a valid status, but there's no corresponding STATUS_RESOLVED constant in config/constants.js. Either add STATUS_RESOLVED to constants.js or remove 'resolved' as a status option. Additionally, 'closed' should use STATUS_CLOSED constant for consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +29
await Ticket.create({
id: generateTicketId(),
issued_by: cardno,
service,
description,
os,
app_version
});

res.status(201).send({
message: 'Successfully created ticket'
});
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The createTicket response doesn't return the created ticket data, but the test expects it (lines 44-47 check response.body.data.service, response.body.data.issued_by, and response.body.data.id). The response should include the created ticket in the data field. Consider storing the result of Ticket.create() and including it in the response.

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +48
res.status(201).send({
message: 'Successfully created ticket'
});
};

export const getTickets = async (req, res) => {
const { cardno } = req.user;
const page = parseInt(req.query.page) || 1;
const pageSize = parseInt(req.query.page_size) || 10;
const offset = (page - 1) * pageSize;

const tickets = await Ticket.findAll({
where: { issued_by: cardno },
order: [['createdAt', 'DESC']],
offset,
limit: pageSize
});

res.status(200).send({
message: MSG_FETCH_SUCCESSFUL,
data: tickets
});
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response format is inconsistent with the client controller responses. While this admin controller correctly includes 'status', 'message', and 'data' fields, the client ticket controller (ticket.controller.js) only includes 'message' and 'data'. For consistent API design, both client and admin endpoints should use the same response structure. The admin format with the 'status' field is the better choice as it matches what the tests expect.

Copilot uses AI. Check for mistakes.
Comment thread tests/tickets.test.js
Comment on lines +71 to +72
expect(response.body.data.message).toBe('Is anyone looking at this?');
expect(response.body.data.sender_id).toBe(user.cardno);
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test expects response.body.data.message and response.body.data.sender_id (lines 71-72), but the controller doesn't return the created message in the response. The controller only returns a message field with MSG_UPDATE_SUCCESSFUL. The test will fail because response.body.data will be undefined. The controller should return the created message object, similar to how the admin controller does (admin/ticketManagement.controller.js lines 80-83).

Suggested change
expect(response.body.data.message).toBe('Is anyone looking at this?');
expect(response.body.data.sender_id).toBe(user.cardno);
expect(response.body.message).toBe('MSG_UPDATE_SUCCESSFUL');
// Verify that the message was actually created in the database
const createdMessage = await TicketMessage.findOne({
where: {
ticket_id: ticketId,
sender_id: user.cardno,
message: 'Is anyone looking at this?'
}
});
expect(createdMessage).not.toBeNull();

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +63
as: 'messages',
order: [['createdAt', 'ASC']]
}
]
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order clause is incorrectly placed inside the include object. In Sequelize, when ordering included associations, the order should be specified at the top-level findOne options or using a nested array format like: order: [[{model: TicketMessage, as: 'messages'}, 'createdAt', 'ASC']]. The current placement inside the include object will be ignored by Sequelize, and messages will not be ordered as intended.

Suggested change
as: 'messages',
order: [['createdAt', 'ASC']]
}
]
as: 'messages'
}
],
order: [[{ model: TicketMessage, as: 'messages' }, 'createdAt', 'ASC']]

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +76
if (ticket.status === 'closed') {
throw new ApiError(400, 'ticket is closed');
}

const newMessage = await TicketMessage.create({
ticket_id: id,
sender_id: req.user.username,
sender_type: 'admin',
message
});

// Update ticket updatedBy and status if needed
const updates = { updatedBy: req.user.username };
if (ticket.status === 'open') {
updates.status = 'in progress';
}
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded status strings 'closed', 'open', and 'in progress' should use constants STATUS_CLOSED, STATUS_OPEN, and STATUS_INPROGRESS from config/constants.js. This is inconsistent with codebase conventions as seen in other maintenance controllers.

Copilot uses AI. Check for mistakes.
}

if (ticket.status === 'closed') {
throw new ApiError(400, 'ticket is closed');
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message starts with a lowercase letter 'ticket is closed', which is inconsistent with other error messages in the same file (line 53: 'Message is required', line 58: 'Ticket not found'). Error messages should be consistently capitalized for better readability and professionalism.

Suggested change
throw new ApiError(400, 'ticket is closed');
throw new ApiError(400, 'Ticket is closed');

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants