diff --git a/package-lock.json b/package-lock.json index b3b078a..3635319 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1891,7 +1890,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -5408,7 +5406,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", diff --git a/server.js b/server.js index a3b66a6..fa743b0 100644 --- a/server.js +++ b/server.js @@ -6,6 +6,7 @@ import healthRoutes from './src/routes/health.js'; import { setupSocket } from './src/socket/socket.js'; import issueRoutes from './src/routes/issues.js'; import userRoutes from './src/routes/users.js'; +import branchRoutes from './src/routes/branch.js'; import thirdPartiesRoutes from './src/routes/thirdparties.js'; import cashRequestRoutes from './src/routes/cashRequestRoutes.js'; @@ -30,6 +31,7 @@ app.use('/api/v1/cash-requests', cashRequestRoutes); app.use('/api/v1/issues', issueRoutes); app.use('/api/v1/users', userRoutes); +app.use('/api/v1/branches', branchRoutes); app.use('/api/v1/thirdparties', thirdPartiesRoutes); // Basic route diff --git a/src/__tests__/branch.test.js b/src/__tests__/branch.test.js new file mode 100644 index 0000000..220be70 --- /dev/null +++ b/src/__tests__/branch.test.js @@ -0,0 +1,141 @@ +// tests/branch.test.js +import request from 'supertest'; +import express from 'express'; +import { getSequelizeInstance } from '../services/connectionService.js'; +import branchRoutes from '../routes/branch.js'; +import Branch from '../models/branch.js'; + +const app = express(); +app.use(express.json()); +app.use('/api/v1/branches', branchRoutes); + +let sequelize; + +beforeAll(async () => { + sequelize = getSequelizeInstance(); + await sequelize.sync({ force: true }); +}); + +afterAll(async () => { + await sequelize.close(); +}); + +describe('Branch Endpoints', () => { + let branchId; + + describe('POST /api/v1/branches', () => { + it('should create a new branch', async () => { + const res = await request(app).post('/api/v1/branches').send({ + name: 'Colombo Branch', + location: 'Colombo 07', + manager_id: 10, + }); + + expect(res.statusCode).toBe(201); + expect(res.body.success).toBe(true); + expect(res.body.data).toHaveProperty('id'); + expect(res.body.data.name).toBe('Colombo Branch'); + expect(res.body.data.location).toBe('Colombo 07'); + expect(res.body.data.manager_id).toBe(10); + + branchId = res.body.data.id; + }); + + it('should reject branch with missing name', async () => { + const res = await request(app).post('/api/v1/branches').send({ + location: 'Colombo 07', + }); + + expect(res.statusCode).toBe(400); + expect(res.body.success).toBe(false); + expect(res.body.message).toBe('Name and location are required'); + }); + + it('should reject branch with missing location', async () => { + const res = await request(app).post('/api/v1/branches').send({ + name: 'Test Branch', + }); + + expect(res.statusCode).toBe(400); + expect(res.body.success).toBe(false); + expect(res.body.message).toBe('Name and location are required'); + }); + }); + + describe('GET /api/v1/branches', () => { + it('should get all branches', async () => { + const res = await request(app).get('/api/v1/branches'); + + expect(res.statusCode).toBe(200); + expect(res.body.success).toBe(true); + expect(Array.isArray(res.body.data)).toBe(true); + expect(res.body.data.length).toBeGreaterThan(0); + }); + }); + + describe('GET /api/v1/branches/:id', () => { + it('should get branch by valid ID', async () => { + const res = await request(app).get(`/api/v1/branches/${branchId}`); + + expect(res.statusCode).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.data.id).toBe(branchId); + expect(res.body.data.name).toBe('Colombo Branch'); + }); + + it('should return 404 for non-existent branch ID', async () => { + const res = await request(app).get('/api/v1/branches/9999'); + + expect(res.statusCode).toBe(404); + expect(res.body.success).toBe(false); + }); + }); + + describe('PUT /api/v1/branches/:id', () => { + it('should update branch name and manager_id', async () => { + const res = await request(app).put(`/api/v1/branches/${branchId}`).send({ + name: 'Colombo Updated', + manager_id: 20 + }); + + expect(res.statusCode).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.data.name).toBe('Colombo Updated'); + expect(res.body.data.manager_id).toBe(20); + expect(res.body.data.location).toBe('Colombo 07'); + }); + + it('should return 404 for non-existent branch update', async () => { + const res = await request(app).put('/api/v1/branches/9999').send({ + name: 'No Branch' + }); + + expect(res.statusCode).toBe(404); + expect(res.body.success).toBe(false); + }); + }); + + describe('DELETE /api/v1/branches/:id', () => { + it('should delete branch', async () => { + const res = await request(app).delete(`/api/v1/branches/${branchId}`); + + expect(res.statusCode).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.message).toBe('Branch deleted successfully'); + }); + + it('should not find deleted branch', async () => { + const res = await request(app).get(`/api/v1/branches/${branchId}`); + + expect(res.statusCode).toBe(404); + expect(res.body.success).toBe(false); + }); + + it('should return 404 when deleting non-existent branch', async () => { + const res = await request(app).delete('/api/v1/branches/9999'); + + expect(res.statusCode).toBe(404); + expect(res.body.success).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/src/controllers/branchController.js b/src/controllers/branchController.js new file mode 100644 index 0000000..ef5cd17 --- /dev/null +++ b/src/controllers/branchController.js @@ -0,0 +1,127 @@ +import branchService from '../services/branchService.js'; + +class BranchController { + // POST /api/v1/branches - Create a new branch + async addBranch(req, res) { + try { + const { name, location, manager_id } = req.body; // Removed contactNumber + + // Validation + if (!name || !location) { + return res.status(400).json({ + success: false, + message: 'Name and location are required', + }); + } + + const result = await branchService.addBranch({ + name, + location, + manager_id + // contactNumber removed + }); + + if (result.success) { + return res.status(201).json(result); + } else { + return res.status(400).json(result); + } + } catch (error) { + return res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } + } + + // GET /api/v1/branches - Get all branches + async getAllBranches(req, res) { + try { + const result = await branchService.getAllBranches(); + + if (result.success) { + return res.status(200).json(result); + } else { + return res.status(404).json(result); + } + } catch (error) { + return res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } + } + + // GET /api/v1/branches/:id - Get branch by ID + async getBranchById(req, res) { + try { + const { id } = req.params; + + const result = await branchService.getBranchById(parseInt(id)); + + if (result.success) { + return res.status(200).json(result); + } else { + return res.status(404).json(result); + } + } catch (error) { + return res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } + } + + // PUT /api/v1/branches/:id - Update branch by ID + async updateBranch(req, res) { + try { + const { id } = req.params; + const { name, location, manager_id } = req.body; // Removed contactNumber + + const result = await branchService.updateBranch(id, { + name, + location, + manager_id + // contactNumber removed + }); + + if (result.success) { + return res.status(200).json(result); + } else { + return res.status(404).json(result); + } + } catch (error) { + return res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } + } + + // DELETE /api/v1/branches/:id - Delete branch by ID + async deleteBranch(req, res) { + try { + const { id } = req.params; + + const result = await branchService.deleteBranch(parseInt(id)); + + if (result.success) { + return res.status(200).json(result); + } else { + return res.status(404).json(result); + } + } catch (error) { + return res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } + } +} + +export default new BranchController(); \ No newline at end of file diff --git a/src/controllers/cashRequestController.js b/src/controllers/cashRequestController.js index a23c106..6dc36a5 100644 --- a/src/controllers/cashRequestController.js +++ b/src/controllers/cashRequestController.js @@ -309,17 +309,17 @@ export const getTechnicianStats = async (req, res) => { /** * Get cash requests by ticket ID - * @route GET /api/v1/cash-requests?ticket_id= - * @query {string} ticket_id - Ticket ID to filter cash requests + * @route GET /api/v1/cash-requests/by-ticket/:ticket_id + * @param {string} ticket_id - Ticket ID to filter cash requests */ export const getCashRequestsByTicketId = async (req, res) => { try { - const { ticket_id } = req.query; + const { ticket_id } = req.params; if (!ticket_id) { return res.status(400).json({ success: false, - message: 'ticket_id query parameter is required' + message: 'ticket_id route parameter is required' }); } diff --git a/src/database/migrations/20251014052343-create-branches-table.js b/src/database/migrations/20251014052343-create-branches-table.js new file mode 100644 index 0000000..9e8cdfd --- /dev/null +++ b/src/database/migrations/20251014052343-create-branches-table.js @@ -0,0 +1,45 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +export default { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('branches', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + allowNull: false, + primaryKey: true, + }, + name: { + type: Sequelize.STRING, + allowNull: false, + }, + location: { + type: Sequelize.STRING, + allowNull: false, + }, + manager_id: { + type: Sequelize.INTEGER, + allowNull: true, + }, + contactNumber: { + type: Sequelize.STRING, + allowNull: true, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('branches'); + }, +}; \ No newline at end of file diff --git a/src/database/migrations/20251015034439-remove-contact-number-from-branches.js b/src/database/migrations/20251015034439-remove-contact-number-from-branches.js new file mode 100644 index 0000000..8aaa8d0 --- /dev/null +++ b/src/database/migrations/20251015034439-remove-contact-number-from-branches.js @@ -0,0 +1,15 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +export default { + async up(queryInterface, Sequelize) { + await queryInterface.removeColumn('branches', 'contactNumber'); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.addColumn('branches', 'contactNumber', { + type: Sequelize.STRING, + allowNull: true, + }); + }, +}; diff --git a/src/models/branch.js b/src/models/branch.js new file mode 100644 index 0000000..7c94256 --- /dev/null +++ b/src/models/branch.js @@ -0,0 +1,33 @@ +import { DataTypes } from 'sequelize'; +import { getSequelizeInstance } from '../services/connectionService.js'; + +const sequelize = getSequelizeInstance(); + +const Branch = sequelize.define( + 'Branch', + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + location: { + type: DataTypes.STRING, + allowNull: false, + }, + manager_id: { + type: DataTypes.INTEGER, + allowNull: true, + }, + }, + { + tableName: 'branches', + timestamps: true, + } +); + +export default Branch; \ No newline at end of file diff --git a/src/models/index.js b/src/models/index.js index 552d4ce..271a621 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -1,6 +1,7 @@ import { getSequelizeInstance } from '../services/connectionService.js'; import Issue from './issue.js'; import User from './user.js'; +import Branch from './branch.js'; import PettyCashRequest from './pettyCashRequest.js'; import Technician from './technician.js'; import BranchManager from './branchManager.js'; @@ -70,7 +71,8 @@ export { User, PettyCashRequest, Technician, - BranchManager, + BranchManager, + Branch, MaintenanceExecutive, sequelize -}; \ No newline at end of file +}; diff --git a/src/routes/branch.js b/src/routes/branch.js new file mode 100644 index 0000000..d220806 --- /dev/null +++ b/src/routes/branch.js @@ -0,0 +1,22 @@ +// src/routes/branchRoutes.js +import { Router } from 'express'; +import branchController from '../controllers/branchController.js'; + +const router = Router(); + +// POST /api/v1/branches - Add a new branch +router.post('/', branchController.addBranch); + +// GET /api/v1/branches - Get all branches +router.get('/', branchController.getAllBranches); + +// GET /api/v1/branches/:id - Get branch by ID +router.get('/:id', branchController.getBranchById); + +// PUT /api/v1/branches/:id - Update branch by ID +router.put('/:id', branchController.updateBranch); + +// DELETE /api/v1/branches/:id - Delete branch by ID +router.delete('/:id', branchController.deleteBranch); + +export default router; \ No newline at end of file diff --git a/src/routes/cashRequestRoutes.js b/src/routes/cashRequestRoutes.js index 26c5015..1da851b 100644 --- a/src/routes/cashRequestRoutes.js +++ b/src/routes/cashRequestRoutes.js @@ -68,7 +68,7 @@ const updateCashRequestValidation = [ ]; router.get('/', getAllCashRequests); -router.get('/by-ticket', getCashRequestsByTicketId); +router.get('/by-ticket/:ticket_id', validateId('ticket_id'), getCashRequestsByTicketId); router.get('/stats/:technician_id', validateId('technician_id'), getTechnicianStats); router.get('/:id', validateUUID('id'), getCashRequestById); router.post('/', createCashRequestValidation, createCashRequest); diff --git a/src/services/branchService.js b/src/services/branchService.js new file mode 100644 index 0000000..58e6472 --- /dev/null +++ b/src/services/branchService.js @@ -0,0 +1,111 @@ +import { Branch } from '../models/index.js'; + +class BranchService { + // Create a new branch + async addBranch(branchData) { + try { + console.log('branchData:', branchData); // Debug log + const branch = await Branch.create(branchData); + return { + success: true, + data: branch, + message: 'Branch created successfully' + }; + } catch (error) { + return { + success: false, + error: error.message, + message: 'Failed to create branch' + }; + } + } + + // Get all branches + async getAllBranches() { + try { + const branches = await Branch.findAll(); + return { + success: true, + data: branches, + count: branches.length, + message: 'Branches retrieved successfully' + }; + } catch (error) { + return { + success: false, + error: error.message, + message: 'Failed to retrieve branches' + }; + } + } + + // Get single branch by ID + async getBranchById(id) { + try { + const branch = await Branch.findByPk(id); + if (!branch) { + return { success: false, message: 'Branch not found' }; + } + return { + success: true, + data: branch, + message: 'Branch retrieved successfully' + }; + } catch (error) { + return { + success: false, + error: error.message, + message: 'Failed to retrieve branch' + }; + } + } + + // Update a branch + async updateBranch(id, updateData) { + try { + const branch = await Branch.findByPk(id); + if (!branch) { + return { success: false, message: 'Branch not found' }; + } + + await branch.update(updateData); + + return { + success: true, + data: branch, + message: 'Branch updated successfully' + }; + } catch (error) { + return { + success: false, + error: error.message, + message: 'Failed to update branch' + }; + } + } + + // Delete a branch + async deleteBranch(id) { + try { + const branch = await Branch.findByPk(id); + if (!branch) { + return { success: false, message: 'Branch not found' }; + } + + await branch.destroy(); + + return { + success: true, + message: 'Branch deleted successfully' + }; + } catch (error) { + return { + success: false, + error: error.message, + message: 'Failed to delete branch' + }; + } + } +} + +export default new BranchService(); \ No newline at end of file