Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 101 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"express": "^5.1.0",
"express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2",
"mathjs": "^15.1.0",
"minio": "^8.0.6",
"multer": "^2.0.2",
"pg": "^8.16.3",
Expand Down
2 changes: 2 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import messageRoutes from './src/routes/messages.js';
import branchRoutes from './src/routes/branch.js';
import thirdPartiesRoutes from './src/routes/thirdparties.js';
import cashRequestRoutes from './src/routes/cashRequestRoutes.js';
import ahpRoutes from './src/routes/ahpRoutes.js';
import authRoutes from './src/routes/auth.js';

const app = express();
Expand All @@ -32,6 +33,7 @@ app.use('/api/v1/users', userRoutes);
app.use('/api/v1/messages', messageRoutes);
app.use('/api/v1/branches', branchRoutes);
app.use('/api/v1/thirdparties', thirdPartiesRoutes);
app.use('/api/v1/ahp', ahpRoutes);

// Basic route
app.get('/api/', (req, res) => {
Expand Down
28 changes: 28 additions & 0 deletions src/controllers/ahpController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { computeDataDrivenAHP } from "../utils/pairwiseMatrix.js";

export const dataDrivenAHP = (req, res) => {
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

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

The new AHP controller lacks test coverage. All other controllers in the codebase (branchController, messageController, thirdPartiesController, etc.) have corresponding test files in src/tests/. Consider adding comprehensive tests for the AHP controller covering successful computation, validation errors, and error handling.

Copilot uses AI. Check for mistakes.
const { criteriaNames, alternativeNames, dataMatrix, benefitFlags } = req.body || {};

if (!req.body) {
return res.status(400).json({ error: 'Request body is required. Please send JSON with dataMatrix, criteriaNames, alternativeNames, and benefitFlags.' });
}

Comment on lines +4 to +9
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

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

The check on line 6 is redundant since req.body is already accessed on line 4 with a fallback to an empty object. If req.body is null/undefined, the destructuring on line 4 will use {} and the variables will be undefined. The check should be moved before the destructuring, or specific parameter validation should be added instead.

Suggested change
const { criteriaNames, alternativeNames, dataMatrix, benefitFlags } = req.body || {};
if (!req.body) {
return res.status(400).json({ error: 'Request body is required. Please send JSON with dataMatrix, criteriaNames, alternativeNames, and benefitFlags.' });
}
const body = req.body;
if (!body || typeof body !== 'object') {
return res.status(400).json({ error: 'Request body is required. Please send JSON with dataMatrix, criteriaNames, alternativeNames, and benefitFlags.' });
}
const { criteriaNames, alternativeNames, dataMatrix, benefitFlags } = body;

Copilot uses AI. Check for mistakes.
if (!Array.isArray(dataMatrix)) {
return res.status(400).json({ error: 'dataMatrix must be an array.' });
}

Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

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

Incomplete input validation. The controller only validates that dataMatrix is an array but doesn't validate other required parameters (criteriaNames, alternativeNames). These are required by computeDataDrivenAHP and will cause unclear error messages if missing. Add validation for all required parameters.

Suggested change
if (!Array.isArray(criteriaNames) || criteriaNames.length === 0) {
return res.status(400).json({ error: 'criteriaNames must be a non-empty array.' });
}
if (!Array.isArray(alternativeNames) || alternativeNames.length === 0) {
return res.status(400).json({ error: 'alternativeNames must be a non-empty array.' });
}

Copilot uses AI. Check for mistakes.
try {
console.log("Start running AHP computation");
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

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

Debug console.log statement should be removed from production code. Consider using a proper logging library consistent with the rest of the application, or remove this debug statement.

Suggested change
console.log("Start running AHP computation");

Copilot uses AI. Check for mistakes.

const ahpResults = computeDataDrivenAHP({
dataMatrix,
criteriaNames,
alternativeNames,
benefitFlags
});

res.status(200).json(ahpResults);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
8 changes: 8 additions & 0 deletions src/routes/ahpRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import express from 'express';
import { dataDrivenAHP } from '../controllers/ahpController.js';

const router = express.Router();
Comment on lines +1 to +4
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

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

Import style is inconsistent with other route files in the codebase. Other route files use destructured imports like import { Router } from 'express' (see branch.js line 2). Consider using the same pattern for consistency.

Suggested change
import express from 'express';
import { dataDrivenAHP } from '../controllers/ahpController.js';
const router = express.Router();
import { Router } from 'express';
import { dataDrivenAHP } from '../controllers/ahpController.js';
const router = Router();

Copilot uses AI. Check for mistakes.

router.post('/', dataDrivenAHP)
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

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

Missing semicolon at the end of the statement. While JavaScript has automatic semicolon insertion, the codebase appears to use semicolons consistently (see other route files). This should include a semicolon for consistency.

Suggested change
router.post('/', dataDrivenAHP)
router.post('/', dataDrivenAHP);

Copilot uses AI. Check for mistakes.

export default router;
Empty file removed src/utils/.gitkeep
Empty file.
116 changes: 116 additions & 0 deletions src/utils/ahs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { create, all } from "mathjs";
import matrixValidation from "./validateMatrix.js";
import weightVectorGeometricMean from "./computePriorityVector.js";
import { consistencyRatio } from "./computeConsistancyRatio.js";

const math = create(all, {});

Comment on lines +1 to +7
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

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

The mathjs instance is created but never used in this file. Since no mathjs operations are performed directly in this module, consider removing this unused import and constant.

Suggested change
import { create, all } from "mathjs";
import matrixValidation from "./validateMatrix.js";
import weightVectorGeometricMean from "./computePriorityVector.js";
import { consistencyRatio } from "./computeConsistancyRatio.js";
const math = create(all, {});
import matrixValidation from "./validateMatrix.js";
import weightVectorGeometricMean from "./computePriorityVector.js";
import { consistencyRatio } from "./computeConsistancyRatio.js";

Copilot uses AI. Check for mistakes.
/**
* Compute AHP results.
* @param {number[][]} criteriaMatrix n x n
* @param {Object.<string, number[][]>} alternativeMatrices for each criterion key: m x m
* @param {string[]} criteriaNames length n
* @param {string[]} alternativeNames length m
* @returns structured results
*/

export function computeAHS(
criteriaMatrix,
alternativeMatrices,
criteriaNames,
alternativeNames
) {

// --- validate criteria matrix ---
matrixValidation(criteriaMatrix, 'criteriaMatrix');
const n = criteriaMatrix.length;

// --- validate criteria names matrix ---
if (!Array.isArray(criteriaNames) || criteriaNames.length !== n) {
throw new Error("criteriaNames length must match criteriaMatrix size.");
}

// --- weights for criteria (GM method) + CR ---
const criteriaWeights = weightVectorGeometricMean(criteriaMatrix);
const criteriaCR = consistencyRatio(criteriaMatrix, criteriaWeights);

// --- for each criterion, compute alt weights + CR ---
const altLocalWeightsByCriterion = {}; // key -> vector length m
const altCRByCriterion = {}; // key -> number
let m = null;

for (let k = 0; k < n; k++) {
const key = criteriaNames[k];
const M = alternativeMatrices[key];

if (!M) {
throw new Error(`Missing alternative matrix for criterion: ${key}`);
}

matrixValidation(M, `alternativeMatrices["${key}"]`);

if (m === null) m = M.length;
if (M.length !== m) {
throw new Error("All alternative matrices must be the same size (m x m)");
}

const w = weightVectorGeometricMean(M);
const cr = consistencyRatio(M, w);

altLocalWeightsByCriterion[key] = w;
altCRByCriterion[key] = cr;

}

if (!Array.isArray(alternativeNames) || alternativeNames.length !== m) {
throw new Error("alternativeNames length must match alternative matrix size");
}


// --- aggregate global scores: sum_k (criteriaWeight[k] * altWeight_k[i]) ---
const globalScores = new Array(m).fill(0);
for (let i = 0; i < m; i++) {
let s = 0;
for (let k = 0; k < n; k++) {
const key = criteriaNames[k];
s += criteriaWeights[k] * altLocalWeightsByCriterion[key][i];
}
globalScores[i] = s;
}

// --- ranking indices sorted desc ---
const idx = globalScores.map((v, i) => i)
.sort((i, j) => globalScores[j] - globalScores[i]);

const ranking = idx.map((i) => ({
name: alternativeNames[i],
score: Number(globalScores[i].toFixed(6))
}));

return {
criteria: criteriaNames.map((name, i) => ({
name,
weight: Number(criteriaWeights[i].toFixed(6))
})),
criteriaCR: Number(criteriaCR.toFixed(6)),
alternatives: alternativeNames,
localWeights: Object.fromEntries(
Object.entries(altLocalWeightsByCriterion).map(([k, w]) => [
k,
w.map((x) => Number(x.toFixed(6)))
])
),
localCR: Object.fromEntries(
Object.entries(altCRByCriterion).map(([k, cr]) => [k, Number(cr.toFixed(6))])
),
globalScores: alternativeNames.map((name, i) => ({
name,
score: Number(globalScores[i].toFixed(6))
})),
ranking
};

}



30 changes: 30 additions & 0 deletions src/utils/computeConsistancyRatio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { create, all } from "mathjs";
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

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

Filename has a typo: 'Consistancy' should be 'Consistency'. The file is named 'computeConsistancyRatio.js' but should be 'computeConsistencyRatio.js'.

Copilot uses AI. Check for mistakes.

const math = create(all, {});
Comment on lines +1 to +3
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

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

The mathjs instance is created but never used in this file. The math.multiply function is called on line 14, but since mathjs is imported and configured, you should verify if this configuration is necessary. If the multiply function works without the custom math instance, consider removing the unused constant.

Copilot uses AI. Check for mistakes.

const RI = {
1: 0.00, 2: 0.00, 3: 0.58, 4: 0.90, 5: 1.12,
6: 1.24, 7: 1.32, 8: 1.41, 9: 1.45, 10: 1.49,
11: 1.51, 12: 1.48, 13: 1.56, 14: 1.57, 15: 1.59
};


/** Consistency Ratio for a pairwise matrix M with weights w */
export function lambdaMax(M, w) {
const Aw = math.multiply(M, w); // vector
let sum = 0;
for (let i = 0; i < w.length; i++) {
sum += (Aw[i] / w[i]);
}
return sum / w.length;
}

/** Consistency Ratio for a pairwise matrix M with weights w */
export function consistencyRatio(M, w) {
const n = M.length;
if (n <= 2) return 0; // CR undefined but effectively 0
const lam = lambdaMax(M, w);
const CI = (lam - n) / (n - 1);
const ri = RI[n] ?? RI[15]; // cap if very large n
return ri === 0 ? 0 : CI / ri;
}
Loading