Spaces:
Runtime error
Runtime error
| import db from '../utils/firebaseClient.js'; | |
| import { calculateAllEmissions } from './carbonCalculator.js'; | |
| import { calculateScore } from './scoringService.js'; | |
| import { generateRecommendations } from './recommendationEngine.js'; | |
| import crypto from 'crypto'; | |
| function computeStreak(userProfile) { | |
| const today = new Date().toISOString().split('T')[0]; | |
| const streak = userProfile.streak || { current: 0, longest: 0, lastSaved: null }; | |
| if (streak.lastSaved === today) { | |
| return streak; | |
| } | |
| const yesterday = new Date(); | |
| yesterday.setDate(yesterday.getDate() - 1); | |
| const yStr = yesterday.toISOString().split('T')[0]; | |
| const newCurrent = streak.lastSaved === yStr ? streak.current + 1 : 1; | |
| return { | |
| current: newCurrent, | |
| longest: Math.max(newCurrent, streak.longest || 0), | |
| lastSaved: today, | |
| }; | |
| } | |
| function computeTitle(points) { | |
| if (points >= 25000) return 'Master Guardian'; | |
| if (points >= 15000) return 'Climate Champion'; | |
| if (points >= 10000) return 'Ocean Guardian'; | |
| if (points >= 6000) return 'Nature Enthusiast'; | |
| if (points >= 3000) return 'Carbon Reducer'; | |
| if (points >= 1000) return 'Green Sprout'; | |
| return 'Eco Novice'; | |
| } | |
| function computeBadges(data, existingBadges = []) { | |
| const badges = new Set(existingBadges); | |
| if (data.dietType === 'vegan' || data.dietType === 'vegetarian') { | |
| badges.add('Tree Planter'); | |
| } | |
| if (data.dailyCarKm === 0 || ['electric', 'hybrid', 'none'].includes(data.carFuelType)) { | |
| badges.add('Carbon Ninja'); | |
| } | |
| if (data.clothingItemsPerYear < 5 && data.electronicsItemsPerYear === 0) { | |
| badges.add('Zero Waste'); | |
| } | |
| return Array.from(badges); | |
| } | |
| /** | |
| * Create a new assessment with full emission analysis and recommendations. | |
| * | |
| * @param {Object} data - Validated assessment input (matches assessmentSchema). | |
| * @returns {Promise<{assessment: Object, emissions: Object, score: number}>} | |
| */ | |
| export async function createAssessmentWithRecommendations(data) { | |
| // ββ Step 1: Resolve user βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const userDoc = await db.collection('users').doc(data.userId).get(); | |
| let effectiveUserId = data.userId; | |
| let user = userDoc.exists ? userDoc.data() : null; | |
| if (!user) { | |
| const email = `${data.userId}@ecoguide.ai`; | |
| const qSnapshot = await db.collection('users').where('email', '==', email).get(); | |
| if (!qSnapshot.empty) { | |
| const existingUserDoc = qSnapshot.docs[0]; | |
| effectiveUserId = existingUserDoc.id; | |
| user = existingUserDoc.data(); | |
| } else { | |
| const newUserId = data.userId; | |
| user = { | |
| id: newUserId, | |
| name: 'Anonymous', | |
| email: email, | |
| points: 0, | |
| badges: [], | |
| title: 'Eco Novice', | |
| streak: { current: 0, longest: 0, lastSaved: null }, | |
| createdAt: new Date().toISOString(), | |
| updatedAt: new Date().toISOString(), | |
| }; | |
| await db.collection('users').doc(newUserId).set(user); | |
| effectiveUserId = newUserId; | |
| } | |
| } | |
| // ββ Step 2: Calculate emissions ββββββββββββββββββββββββββββββββββββββββββββ | |
| const emissions = calculateAllEmissions(data); | |
| // ββ Step 3: Score ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const score = calculateScore(emissions.totalEmission); | |
| // ββ Step 4: Generate personalised recommendations ββββββββββββββββββββββββββ | |
| const recs = generateRecommendations(data, emissions); | |
| // ββ Step 5: Update User Profile (Points, Streak, Badges, Title) ββββββββββββ | |
| const currentStreak = computeStreak(user); | |
| const isStreakMilestone = currentStreak.current > 0; | |
| const streakBonus = | |
| isStreakMilestone && currentStreak.current % 7 === 0 | |
| ? 250 | |
| : isStreakMilestone && currentStreak.current % 3 === 0 | |
| ? 50 | |
| : 0; | |
| const awardedPoints = 100 + streakBonus; | |
| const newPoints = (user.points || 0) + awardedPoints; | |
| const newBadges = computeBadges(data, user.badges || []); | |
| const newTitle = computeTitle(newPoints); | |
| const updatedUser = { | |
| ...user, | |
| points: newPoints, | |
| badges: newBadges, | |
| title: newTitle, | |
| streak: currentStreak, | |
| updatedAt: new Date().toISOString(), | |
| }; | |
| await db.collection('users').doc(effectiveUserId).set(updatedUser); | |
| // ββ Step 6: Persist Assessment βββββββββββββββββββββββββββββββββββββββββββββ | |
| const assessmentId = crypto.randomUUID(); | |
| const assessmentDocData = { | |
| id: assessmentId, | |
| userId: effectiveUserId, | |
| dailyCarKm: data.dailyCarKm, | |
| carFuelType: data.carFuelType, | |
| publicTransportKmPerWeek: data.publicTransportKmPerWeek, | |
| cyclingKmPerWeek: data.cyclingKmPerWeek, | |
| shortFlightsPerYear: data.shortFlightsPerYear, | |
| longFlightsPerYear: data.longFlightsPerYear, | |
| monthlyElectricityKwh: data.monthlyElectricityKwh, | |
| renewablePercentage: data.renewablePercentage, | |
| dietType: data.dietType, | |
| clothingItemsPerYear: data.clothingItemsPerYear, | |
| electronicsItemsPerYear: data.electronicsItemsPerYear, | |
| transportEmission: emissions.transportEmission, | |
| energyEmission: emissions.energyEmission, | |
| foodEmission: emissions.foodEmission, | |
| shoppingEmission: emissions.shoppingEmission, | |
| totalEmission: emissions.totalEmission, | |
| sustainabilityScore: score, | |
| createdAt: new Date().toISOString(), | |
| }; | |
| await db.collection('assessments').doc(assessmentId).set(assessmentDocData); | |
| const recommendationsList = []; | |
| for (const r of recs) { | |
| const recId = crypto.randomUUID(); | |
| const recDocData = { | |
| id: recId, | |
| assessmentId, | |
| title: r.title, | |
| description: r.description, | |
| estimatedSavings: r.estimatedSavings, | |
| priority: r.priority, | |
| category: r.category, | |
| createdAt: new Date().toISOString(), | |
| }; | |
| await db.collection('recommendations').doc(recId).set(recDocData); | |
| recommendationsList.push(recDocData); | |
| } | |
| recommendationsList.sort((a, b) => (b.estimatedSavings || 0) - (a.estimatedSavings || 0)); | |
| const assessment = { | |
| ...assessmentDocData, | |
| recommendations: recommendationsList, | |
| }; | |
| return { assessment, emissions, score }; | |
| } | |