EcoTracker-II / backend /src /controllers /assessmentController.js
Akash-Dragon's picture
refactor: restructure project as a monorepo and implement backend API
7e49c4d
Raw
History Blame Contribute Delete
6.75 kB
import { z } from 'zod';
import db from '../utils/firebaseClient.js';
import { compareToAverages } from '../services/scoringService.js';
import { createAssessmentWithRecommendations } from '../services/assessmentService.js';
import { AppError } from '../middleware/errorHandler.js';
import { sanitizeText } from '../utils/sanitize.js';
const assessmentSchema = z
.object({
userId: z.string().min(1, 'userId is required'),
// Transportation
dailyCarKm: z.number().min(0).max(1000).default(0),
carFuelType: z.enum(['petrol', 'diesel', 'electric', 'hybrid', 'none']).default('none'),
publicTransportKmPerWeek: z.number().min(0).max(10000).default(0),
cyclingKmPerWeek: z.number().min(0).max(1000).default(0),
shortFlightsPerYear: z.number().int().min(0).max(100).default(0),
longFlightsPerYear: z.number().int().min(0).max(50).default(0),
// Energy
monthlyElectricityKwh: z.number().min(0).max(10000).default(0),
renewablePercentage: z.number().min(0).max(100).default(0),
// Food
dietType: z.enum(['vegan', 'vegetarian', 'mixed', 'heavy_meat']).default('mixed'),
// Shopping
clothingItemsPerYear: z.number().int().min(0).max(500).default(0),
electronicsItemsPerYear: z.number().int().min(0).max(50).default(0),
})
.strict();
const PERCENTAGE_MULTIPLIER = 100;
function buildUnknownKeysMessage(zodError, allowedKeys) {
const unknownKeyIssues = zodError.issues.filter((i) => i.code === 'unrecognized_keys');
if (unknownKeyIssues.length === 0) return zodError.message;
const unknownKeys = unknownKeyIssues.flatMap((i) => i.keys);
return (
`Unknown fields: ${unknownKeys.join(', ')}. ` +
`Only these fields are accepted: ${allowedKeys.join(', ')}`
);
}
const ALLOWED_KEYS = Object.keys(assessmentSchema._def.shape());
export const createAssessment = async (req, res, next) => {
try {
const parseResult = assessmentSchema.safeParse(req.body);
if (!parseResult.success) {
const message = buildUnknownKeysMessage(parseResult.error, ALLOWED_KEYS);
throw new AppError(message, 400);
}
const data = parseResult.data;
data.userId = sanitizeText(data.userId);
const { assessment, emissions, score } = await createAssessmentWithRecommendations(data);
res.status(201).json({
success: true,
data: {
...assessment,
breakdown: emissions.breakdown,
scoreInfo: { score, ...emissions },
},
});
} catch (err) {
next(err);
}
};
export const getAssessmentById = async (req, res, next) => {
try {
const { id } = req.params;
const sanitizedId = sanitizeText(id);
const assessmentDoc = await db.collection('assessments').doc(sanitizedId).get();
if (!assessmentDoc.exists) throw new AppError('Assessment not found', 404);
const assessment = assessmentDoc.data();
// Fetch recommendations
const recsSnap = await db
.collection('recommendations')
.where('assessmentId', '==', sanitizedId)
.get();
const recommendations = recsSnap.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
recommendations.sort((a, b) => (b.estimatedSavings || 0) - (a.estimatedSavings || 0));
// Fetch simulations
const simsSnap = await db
.collection('simulations')
.where('assessmentId', '==', sanitizedId)
.get();
const simulations = simsSnap.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
simulations.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
// Fetch user
const userDoc = await db.collection('users').doc(assessment.userId).get();
const user = userDoc.exists
? { id: userDoc.id, name: userDoc.data().name, email: userDoc.data().email }
: null;
const total = assessment.totalEmission;
const safeTotal = total === 0 ? 1 : total;
res.json({
success: true,
data: {
...assessment,
id: sanitizedId,
recommendations,
simulations,
user,
breakdown: {
transport: parseFloat(
((assessment.transportEmission / safeTotal) * PERCENTAGE_MULTIPLIER).toFixed(1)
),
energy: parseFloat(
((assessment.energyEmission / safeTotal) * PERCENTAGE_MULTIPLIER).toFixed(1)
),
food: parseFloat(
((assessment.foodEmission / safeTotal) * PERCENTAGE_MULTIPLIER).toFixed(1)
),
shopping: parseFloat(
((assessment.shoppingEmission / safeTotal) * PERCENTAGE_MULTIPLIER).toFixed(1)
),
},
},
});
} catch (err) {
next(err);
}
};
export const getAssessmentsByUser = async (req, res, next) => {
try {
const { userId } = req.params;
const sanitizedUserId = sanitizeText(userId);
const userDoc = await db.collection('users').doc(sanitizedUserId).get();
if (!userDoc.exists) throw new AppError('User not found', 404);
const assessmentsSnap = await db
.collection('assessments')
.where('userId', '==', sanitizedUserId)
.get();
const assessments = assessmentsSnap.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
assessments.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
for (const assessment of assessments) {
const recsSnap = await db
.collection('recommendations')
.where('assessmentId', '==', assessment.id)
.get();
const recommendations = recsSnap.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
recommendations.sort((a, b) => (b.estimatedSavings || 0) - (a.estimatedSavings || 0));
assessment.recommendations = recommendations.slice(0, 3);
const simsSnap = await db
.collection('simulations')
.where('assessmentId', '==', assessment.id)
.get();
assessment._count = { simulations: simsSnap.docs.length };
}
res.json({ success: true, data: assessments, count: assessments.length });
} catch (err) {
next(err);
}
};
export const compareAssessment = async (req, res, next) => {
try {
const { id } = req.params;
const sanitizedId = sanitizeText(id);
const assessmentDoc = await db.collection('assessments').doc(sanitizedId).get();
if (!assessmentDoc.exists) throw new AppError('Assessment not found', 404);
const assessment = assessmentDoc.data();
const comparison = compareToAverages(assessment.totalEmission);
res.set('Cache-Control', 'public, max-age=300, stale-while-revalidate=60');
res.json({
success: true,
data: {
assessmentId: sanitizedId,
totalEmission: assessment.totalEmission,
sustainabilityScore: assessment.sustainabilityScore,
comparison,
},
});
} catch (err) {
next(err);
}
};