Spaces:
Runtime error
Runtime error
| 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); | |
| } | |
| }; | |