diff --git a/app/api/astro-numerology/analysis/route.ts b/app/api/astro-numerology/analysis/route.ts index 2e06a229..e3722dc3 100644 --- a/app/api/astro-numerology/analysis/route.ts +++ b/app/api/astro-numerology/analysis/route.ts @@ -1,449 +1,67 @@ +/** + * POST /api/astro-numerology/analysis + * Western astro-numerology narrative. Requires a signed-in Firebase user. + * Stage B / on-demand generation calls generateAstroNumerologyAnalysis in-process. + */ + import { NextRequest, NextResponse } from 'next/server'; -import { getFirebaseDB } from '@/lib/firebase'; -import { resolveAiReportWithFallback } from '@/lib/aiFallbackRouter'; -import { callStructuredAI } from '@/lib/aiStructuredOutput'; -import { parseStructuredJsonFromResponse } from '@/lib/aiStructuredOutputParse'; -import { isGroqParsedRecord, type GroqStructuredParseInput } from '@/lib/groqStructuredParse'; -import { calculateLifePathNumber, calculateDestinyNumber } from '@/lib/numerologyCalculations'; +import { generateAstroNumerologyAnalysis } from '@/lib/astroNumerology/generateAstroNumerologyAnalysis'; +import { withRateLimit, rateLimiters } from '@/lib/rateLimit'; +import { resolveOwnedUserId, verifyUserRequest } from '@/lib/userApiAuth'; import { devLog } from '@/lib/devLogger'; -import { GROQ_DEFAULT_TEXT_MODEL } from '@/lib/groqModels'; - -// Helper to check if we're using Admin SDK -function isAdminSDK(db: any): boolean { - return db && typeof db.collection === 'function'; -} - -// Helper to get document using Admin SDK or Client SDK -async function getCachedDoc(collectionPath: string[], docId: string): Promise { - const db = getFirebaseDB(); - if (!db) return null; - - try { - if (isAdminSDK(db)) { - // Admin SDK API - handle nested collections - let ref: any = db.collection(collectionPath[0]); - for (let i = 1; i < collectionPath.length; i += 2) { - const docIdInPath = collectionPath[i]; - if (i + 1 < collectionPath.length) { - const nextCollection = collectionPath[i + 1]; - ref = ref.doc(docIdInPath).collection(nextCollection); - } else { - ref = ref.doc(docIdInPath); - } - } - if (ref.get && typeof ref.get === 'function') { - const snapshot = await ref.doc(docId).get(); - return snapshot.exists ? { exists: () => true, data: () => snapshot.data() } : { exists: () => false, data: () => null }; - } else { - const snapshot = await ref.get(); - return snapshot.exists ? { exists: () => true, data: () => snapshot.data() } : { exists: () => false, data: () => null }; - } - } else { - // Client SDK API - const { doc, getDoc } = await import('firebase/firestore'); - const docRef = doc(db, ...collectionPath, docId); - return await getDoc(docRef); - } - } catch (error) { - if (process.env.NODE_ENV === 'development') { - devLog.warn('Error getting document:', error, 'astro-numerology'); - } - return { exists: () => false, data: () => null }; - } -} -// Helper to set document using Admin SDK or Client SDK -async function setCachedDoc(collectionPath: string[], docId: string, data: any): Promise { - const db = getFirebaseDB(); - if (!db) return; +export const dynamic = 'force-dynamic'; +export const maxDuration = 60; - try { - if (isAdminSDK(db)) { - // Admin SDK API - handle nested collections - let ref: any = db.collection(collectionPath[0]); - for (let i = 1; i < collectionPath.length; i += 2) { - const docIdInPath = collectionPath[i]; - if (i + 1 < collectionPath.length) { - const nextCollection = collectionPath[i + 1]; - ref = ref.doc(docIdInPath).collection(nextCollection); - } else { - ref = ref.doc(docIdInPath); - } - } - if (ref.doc && typeof ref.doc === 'function') { - await ref.doc(docId).set(data); - } else { - await ref.set(data); - } - } else { - // Client SDK API - const { doc, setDoc } = await import('firebase/firestore'); - const docRef = doc(db, ...collectionPath, docId); - await setDoc(docRef, data); - } - } catch (error) { - if (process.env.NODE_ENV === 'development') { - devLog.warn('Error setting document:', error, 'astro-numerology'); - } - } -} - - -interface AstroNumerologyRequest { - userId: string; - birthDate: string; - fullName: string; +interface RequestBody { + userId?: string; + birthDate?: string; + fullName?: string; sunSign?: string; } -interface AstroNumerologyResponse { - success: boolean; - data?: { - sunSign: string; - lifePathNumber: number; - nameNumber: number; - comprehensiveAnalysis: { - personalitySynthesis: string; - careerGuidance: string; - relationshipInsights: string; - lifePurpose: string; - personalGrowth: string; - challenges: string[]; - opportunities: string[]; - yearlyForecast: string; - }; - timestamp: number; - }; - error?: string; -} - -// Build comprehensive Groq prompt -function buildGroqPrompt(sunSign: string, lifePathNumber: number, nameNumber: number, birthDate: string, fullName: string): string { - const currentYear = new Date().getFullYear(); - - return `You are an expert astro-numerologist specializing in combining Western Astrology (Tropical Zodiac) with Pythagorean Numerology. - -User Profile: -- Sun Sign: ${sunSign} (Western Astrology - represents core personality) -- Life Path Number: ${lifePathNumber} (from birth date - represents life journey) -- Name Number: ${nameNumber} (from full name - represents natural talents) -- Birth Date: ${birthDate} -- Full Name: ${fullName} -- Current Year: ${currentYear} - -Generate a comprehensive astro-numerology analysis covering all life areas. Provide detailed, insightful, and practical guidance. Write in a warm, empowering, and accessible tone. - -Format your response as a JSON object with the following structure: -{ - "personalitySynthesis": "Detailed paragraph explaining how the sun sign, life path number, and name number work together to create a unique personality profile. Be specific and insightful, showing how these energies blend.", - "careerGuidance": "Detailed paragraph about career paths that align with these combined energies, what the life purpose reveals, and specific vocational directions.", - "relationshipInsights": "Detailed paragraph about how these energies manifest in relationships, compatibility patterns, and interpersonal dynamics.", - "lifePurpose": "Detailed paragraph about the deeper life purpose when combining astrological and numerological insights, including destiny themes.", - "personalGrowth": "Detailed paragraph with specific recommendations for personal development based on the combined analysis, including actionable steps.", - "challenges": ["Challenge 1 description", "Challenge 2 description", "Challenge 3 description"], - "opportunities": ["Opportunity 1 description", "Opportunity 2 description", "Opportunity 3 description"], - "yearlyForecast": "Detailed paragraph about insights for ${currentYear} based on the numbers and sun sign, including key themes and timing considerations." -} - -Make each section comprehensive yet concise, providing valuable insights that help the user understand themselves better and navigate their life path.`; -} - -type AstroNumerologyComprehensiveAnalysis = NonNullable< - AstroNumerologyResponse['data'] ->['comprehensiveAnalysis']; - -function extractAstroNumerologyAnalysisFromCache( - cachedData: Record, -): AstroNumerologyComprehensiveAnalysis | null { - const data = - (cachedData.data as AstroNumerologyResponse['data'] | undefined) || - (cachedData as AstroNumerologyResponse['data']); - const analysis = data?.comprehensiveAnalysis; - if (!analysis?.personalitySynthesis?.trim()) return null; - return analysis; -} - -async function readAstroNumerologyCache( - userId: string, - birthDataKey: string, - options?: { allowStale?: boolean }, -): Promise { - try { - const docSnap = await getCachedDoc(['users', userId, 'astroNumerologyReports'], 'current'); - if (!docSnap?.exists()) return null; - const cachedData = docSnap.data() as Record; - const cachedBirthKey = cachedData.birthDataKey as string | undefined; - if (cachedBirthKey !== birthDataKey) return null; - const lastUpdated = cachedData.timestamp as number | undefined; - if (!lastUpdated) return null; - if (!options?.allowStale) { - const hoursSinceUpdate = (Date.now() - lastUpdated) / (1000 * 60 * 60); - if (hoursSinceUpdate >= 24) return null; - } - return extractAstroNumerologyAnalysisFromCache(cachedData); - } catch { - return null; - } -} - -function buildDeterministicAstroNumerology( - actualSunSign: string, - lifePathNumber: number, - nameNumber: number, -): AstroNumerologyComprehensiveAnalysis { - return { - personalitySynthesis: `Your ${actualSunSign} sun sign combines with Life Path ${lifePathNumber} and Name Number ${nameNumber} to create a unique personality blend.`, - careerGuidance: `Career paths that align with Life Path ${lifePathNumber} and your ${actualSunSign} traits would be most fulfilling.`, - relationshipInsights: `Your relationship style is influenced by both your ${actualSunSign} nature and your numerological patterns.`, - lifePurpose: 'Your life purpose is revealed through the combination of your astrological and numerological influences.', - personalGrowth: - 'Focus on developing the strengths of both your sun sign and your life path number for optimal growth.', - challenges: [ - 'Balancing different aspects of your personality', - 'Aligning actions with your life purpose', - ], - opportunities: [ - 'Leveraging your unique combination of energies', - 'Connecting with like-minded individuals', - ], - yearlyForecast: - 'This year brings opportunities to integrate your astrological and numerological influences.', - }; -} - -function mapAstroNumerologyParsed( - parsed: Record, -): AstroNumerologyComprehensiveAnalysis { - return { - personalitySynthesis: String(parsed.personalitySynthesis ?? ''), - careerGuidance: String(parsed.careerGuidance ?? ''), - relationshipInsights: String(parsed.relationshipInsights ?? ''), - lifePurpose: String(parsed.lifePurpose ?? ''), - personalGrowth: String(parsed.personalGrowth ?? ''), - challenges: Array.isArray(parsed.challenges) ? parsed.challenges.map(String) : [], - opportunities: Array.isArray(parsed.opportunities) ? parsed.opportunities.map(String) : [], - yearlyForecast: String(parsed.yearlyForecast ?? ''), - }; -} - -function textFallbackAstroNumerology(response: string): AstroNumerologyComprehensiveAnalysis { - const sections = response.split(/\n\n+/); - return { - personalitySynthesis: sections[0] || response.substring(0, 300), - careerGuidance: sections[1] || 'Career guidance based on your combined astro-numerology profile.', - relationshipInsights: sections[2] || 'Relationship insights from your astro-numerology combination.', - lifePurpose: sections[3] || 'Life purpose revealed through astro-numerology analysis.', - personalGrowth: sections[4] || 'Personal growth recommendations for your journey.', - challenges: [ - 'Balancing different aspects of your personality', - 'Navigating life transitions', - 'Developing your full potential', - ], - opportunities: [ - 'Harnessing your unique combination of energies', - 'Aligning with your life purpose', - 'Building meaningful connections', - ], - yearlyForecast: - sections[5] || `Your ${new Date().getFullYear()} forecast based on your astro-numerology profile.`, - }; -} - -function parseGroqResponse(response: GroqStructuredParseInput): AstroNumerologyComprehensiveAnalysis { - if (isGroqParsedRecord(response)) { - return mapAstroNumerologyParsed(response); - } - - const trimmed = response.trim(); - if (!trimmed) { - return textFallbackAstroNumerology(''); - } - - const structured = parseStructuredJsonFromResponse(trimmed); - if (structured.ok && structured.data) { - return mapAstroNumerologyParsed(structured.data); - } - - devLog.warn('Failed to parse JSON from Groq response, using fallback', undefined, 'astro-numerology'); - return textFallbackAstroNumerology(trimmed); -} - -export async function POST(request: NextRequest) { +async function handleAstroNumerologyAnalysis(request: NextRequest) { try { - const { userId, birthDate, fullName, sunSign }: AstroNumerologyRequest = await request.json(); - - // Validate required fields - if (!userId || !birthDate || !fullName) { - return NextResponse.json({ - success: false, - error: 'Missing required parameters: userId, birthDate, or fullName' - }, { status: 400 }); + const auth = await verifyUserRequest(request, 'astro-numerology-analysis'); + if (!auth.ok) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }); } - devLog.info('🔮 Astro-Numerology API: Generating comprehensive report for user:', userId, 'astro-numerology'); - - // Calculate numerology numbers - const lifePathNumber = calculateLifePathNumber(birthDate); - const nameNumber = calculateDestinyNumber(fullName); // Destiny Number is the Name Number - - // Get sun sign if not provided (would need to be passed from component) - const actualSunSign = sunSign || 'Unknown'; - - if (actualSunSign === 'Unknown') { - return NextResponse.json({ - success: false, - error: 'Sun sign is required. Please ensure Western astrology chart data is available.' - }, { status: 400 }); - } - - const birthDataKey = `${birthDate}_${fullName}_${actualSunSign}`; - - try { - const cached = await readAstroNumerologyCache(userId, birthDataKey); - if (cached) { - devLog.info('✅ Returning cached Astro-Numerology report for user:', userId, 'astro-numerology'); - return NextResponse.json({ - success: true, - data: { - sunSign: actualSunSign, - lifePathNumber, - nameNumber, - comprehensiveAnalysis: cached, - timestamp: Date.now(), - }, - }); + const body = (await request.json().catch(() => ({}))) as RequestBody; + if (body.userId != null && body.userId !== '') { + const owned = resolveOwnedUserId(body.userId, auth.uid); + if (!owned) { + return NextResponse.json({ success: false, error: 'Forbidden' }, { status: 403 }); } - } catch (cacheError: unknown) { - devLog.warn('⚠️ Error checking cache, proceeding with generation:', cacheError, 'astro-numerology'); - } - - if (!process.env.GROQ_API_KEY) { - devLog.error('❌ GROQ_API_KEY is not configured', undefined, 'route'); - return NextResponse.json({ - success: true, - data: { - sunSign: actualSunSign, - lifePathNumber, - nameNumber, - comprehensiveAnalysis: buildDeterministicAstroNumerology( - actualSunSign, - lifePathNumber, - nameNumber, - ), - timestamp: Date.now(), - }, - }); } - const prompt = buildGroqPrompt(actualSunSign, lifePathNumber, nameNumber, birthDate, fullName); - - devLog.info('🤖 Calling AI for comprehensive Astro-Numerology analysis...', undefined, 'astro-numerology'); - - const resolved = await resolveAiReportWithFallback({ - label: 'astro-numerology-comprehensive', - userId, - tryLlm: async () => { - const structured = await callStructuredAI({ - label: 'astro-numerology-comprehensive', - model: GROQ_DEFAULT_TEXT_MODEL, - userId, - messages: [ - { - role: 'system', - content: - 'You are an expert astro-numerologist specializing in combining Western Astrology (Tropical Zodiac) with Pythagorean Numerology. Provide comprehensive, insightful, and practical guidance. Always respond with valid JSON when requested.', - }, - { role: 'user', content: prompt }, - ], - temperature: 0.75, - maxTokens: 2500, - responseFormat: { type: 'json_object' }, - maxAttempts: 3, - }); - - if (!structured.ok && structured.failureMode !== 'none') { - devLog.warn( - `astro-numerology structured AI: ${structured.failureMode} after ${structured.attempts} attempt(s)`, - undefined, - 'astro-numerology', - ); - } - - if (structured.ok && structured.raw) { - return { - data: mapAstroNumerologyParsed(structured.raw), - attempts: structured.attempts, - failureMode: 'none', - }; - } - const recovered = structured.lastRaw - ? parseStructuredJsonFromResponse(structured.lastRaw) - : null; - if (recovered?.ok && recovered.data) { - return { - data: mapAstroNumerologyParsed(recovered.data), - attempts: structured.attempts, - failureMode: structured.failureMode, - }; - } - return { - data: null, - attempts: structured.attempts, - failureMode: structured.failureMode, - parsingFailed: true, - }; - }, - readFirestoreCache: () => - readAstroNumerologyCache(userId, birthDataKey, { allowStale: true }), - buildDeterministic: () => - buildDeterministicAstroNumerology(actualSunSign, lifePathNumber, nameNumber), + const result = await generateAstroNumerologyAnalysis({ + userId: auth.uid, + birthDate: typeof body.birthDate === 'string' ? body.birthDate : '', + fullName: typeof body.fullName === 'string' ? body.fullName : '', + sunSign: typeof body.sunSign === 'string' ? body.sunSign : undefined, + useCache: true, }); - const responseData: AstroNumerologyResponse['data'] = { - sunSign: actualSunSign, - lifePathNumber, - nameNumber, - comprehensiveAnalysis: resolved.data, - timestamp: Date.now(), - }; - - if (resolved.degraded && resolved.source !== 'llm') { - return NextResponse.json({ - success: true, - data: { - ...responseData, - parsingFailed: resolved.parsingFailed ?? true, - fallbackSource: resolved.source, - error: - resolved.source === 'firestore_cache' - ? 'Using last saved report; AI narrative refresh failed' - : 'Failed to parse AI response, using chart-based defaults', - }, - }); + if (!result.ok) { + return NextResponse.json({ success: false, error: result.error }, { status: result.status }); } - try { - await setCachedDoc(['users', userId, 'astroNumerologyReports'], 'current', { - data: responseData, - birthDataKey, - timestamp: Date.now(), - }); - devLog.info('✅ Cached Astro-Numerology report in Firebase', undefined, 'astro-numerology'); - } catch (cacheError: unknown) { - devLog.warn('⚠️ Error caching report:', cacheError, 'astro-numerology'); - } - - return NextResponse.json({ - success: true, - data: responseData, - }); - - } catch (error: any) { - devLog.error('❌ Astro-Numerology API error:', error, 'route'); - return NextResponse.json({ - success: false, - error: error.message || 'Failed to generate Astro-Numerology analysis' - }, { status: 500 }); + return NextResponse.json({ success: true, data: result.data }); + } catch (error) { + devLog.error('❌ Astro-Numerology API error:', error, 'astro-numerology'); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to generate Astro-Numerology analysis', + }, + { status: 500 }, + ); } } +export const POST = withRateLimit( + handleAstroNumerologyAnalysis, + rateLimiters.ai, + 'astro_numerology_analysis_post', +); diff --git a/app/api/vedic-astro-numerology/analysis/route.ts b/app/api/vedic-astro-numerology/analysis/route.ts index db903c05..603f3896 100644 --- a/app/api/vedic-astro-numerology/analysis/route.ts +++ b/app/api/vedic-astro-numerology/analysis/route.ts @@ -1,517 +1,74 @@ +/** + * POST /api/vedic-astro-numerology/analysis + * Vedic astro-numerology narrative. Requires a signed-in Firebase user. + * Stage B / on-demand generation calls generateVedicAstroNumerologyAnalysis in-process. + */ + import { NextRequest, NextResponse } from 'next/server'; -import { getFirebaseDB } from '@/lib/firebase'; -import { resolveAiReportWithFallback } from '@/lib/aiFallbackRouter'; -import { callStructuredAI } from '@/lib/aiStructuredOutput'; -import type { StructuredFailureMode } from '@/lib/aiStructuredOutputParse'; -import { parseStructuredJsonFromResponse } from '@/lib/aiStructuredOutputParse'; -import { isGroqParsedRecord, type GroqStructuredParseInput } from '@/lib/groqStructuredParse'; -import { type VedicNumerologyProfile } from '@/lib/vedicNumerologyCalculations'; -import { buildVedicKarmaInsights } from '@/lib/vedic/karmaChartInsights'; +import { generateVedicAstroNumerologyAnalysis } from '@/lib/vedicAstroNumerology/generateVedicAstroNumerologyAnalysis'; +import type { VedicNumerologyProfile } from '@/lib/vedicNumerologyCalculations'; +import { withRateLimit, rateLimiters } from '@/lib/rateLimit'; +import { resolveOwnedUserId, verifyUserRequest } from '@/lib/userApiAuth'; import { devLog } from '@/lib/devLogger'; -import { GROQ_DEFAULT_TEXT_MODEL } from '@/lib/groqModels'; -// Helper to check if we're using Admin SDK -function isAdminSDK(db: any): boolean { - return db && typeof db.collection === 'function'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 60; + +interface RequestBody { + userId?: string; + birthDate?: string; + fullName?: string; + moonSign?: string; + lagnaSign?: string; + sunSign?: string; + numerologyProfile?: VedicNumerologyProfile; } -// Helper to get document using Admin SDK or Client SDK -async function getCachedDoc(collectionPath: string[], docId: string): Promise { - const db = getFirebaseDB(); - if (!db) return null; - +async function handleVedicAstroNumerologyAnalysis(request: NextRequest) { try { - if (isAdminSDK(db)) { - // Admin SDK API - handle nested collections - let ref: any = db.collection(collectionPath[0]); - for (let i = 1; i < collectionPath.length; i += 2) { - const docIdInPath = collectionPath[i]; - if (i + 1 < collectionPath.length) { - const nextCollection = collectionPath[i + 1]; - ref = ref.doc(docIdInPath).collection(nextCollection); - } else { - ref = ref.doc(docIdInPath); - } - } - if (ref.get && typeof ref.get === 'function') { - const snapshot = await ref.doc(docId).get(); - return snapshot.exists ? { exists: () => true, data: () => snapshot.data() } : { exists: () => false, data: () => null }; - } else { - const snapshot = await ref.get(); - return snapshot.exists ? { exists: () => true, data: () => snapshot.data() } : { exists: () => false, data: () => null }; - } - } else { - // Client SDK API - const { doc, getDoc } = await import('firebase/firestore'); - const docRef = doc(db, ...collectionPath, docId); - return await getDoc(docRef); - } - } catch (error) { - if (process.env.NODE_ENV === 'development') { - devLog.warn('Error getting document:', error, 'vedic-astro-numerology'); + const auth = await verifyUserRequest(request, 'vedic-astro-numerology-analysis'); + if (!auth.ok) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }); } - return { exists: () => false, data: () => null }; - } -} -// Helper to set document using Admin SDK or Client SDK -async function setCachedDoc(collectionPath: string[], docId: string, data: any): Promise { - const db = getFirebaseDB(); - if (!db) return; - - try { - if (isAdminSDK(db)) { - // Admin SDK API - handle nested collections - let ref: any = db.collection(collectionPath[0]); - for (let i = 1; i < collectionPath.length; i += 2) { - const docIdInPath = collectionPath[i]; - if (i + 1 < collectionPath.length) { - const nextCollection = collectionPath[i + 1]; - ref = ref.doc(docIdInPath).collection(nextCollection); - } else { - ref = ref.doc(docIdInPath); - } + const body = (await request.json().catch(() => ({}))) as RequestBody; + if (body.userId != null && body.userId !== '') { + const owned = resolveOwnedUserId(body.userId, auth.uid); + if (!owned) { + return NextResponse.json({ success: false, error: 'Forbidden' }, { status: 403 }); } - if (ref.doc && typeof ref.doc === 'function') { - await ref.doc(docId).set(data); - } else { - await ref.set(data); - } - } else { - // Client SDK API - const { doc, setDoc } = await import('firebase/firestore'); - const docRef = doc(db, ...collectionPath, docId); - await setDoc(docRef, data); - } - } catch (error) { - if (process.env.NODE_ENV === 'development') { - devLog.warn('Error setting document:', error, 'vedic-astro-numerology'); - } - } -} - - -interface VedicAstroNumerologyRequest { - userId: string; - birthDate: string; - fullName: string; - moonSign: string; - lagnaSign: string; - sunSign: string; - numerologyProfile: VedicNumerologyProfile; -} - -interface VedicAstroNumerologyResponse { - success: boolean; - data?: { - moonSign: string; - lagnaSign: string; - sunSign: string; - lifePathNumber: number; - rulingPlanet: string; - comprehensiveAnalysis: { - personalitySynthesis: string; - karmicInsights: string; - remedies: string; - careerGuidance: string; - relationshipInsights: string; - lifePurpose: string; - personalGrowth: string; - challenges: string[]; - opportunities: string[]; - yearlyForecast: string; - }; - timestamp: number; - }; - error?: string; -} - -// Build comprehensive Groq prompt for Vedic Astro-Numerology -function buildVedicGroqPrompt( - moonSign: string, - lagnaSign: string, - sunSign: string, - numerologyProfile: VedicNumerologyProfile, - birthDate: string, - fullName: string -): string { - const currentYear = new Date().getFullYear(); - const rulingPlanet = numerologyProfile.planetaryInfluences['Life Path']?.planet || 'Unknown'; - const lifePathGemstone = numerologyProfile.planetaryInfluences['Life Path']?.gemstone || 'gemstone'; - const destinyGemstone = numerologyProfile.planetaryInfluences['Destiny']?.gemstone || 'gemstone'; - const lifePathMantra = numerologyProfile.planetaryInfluences['Life Path']?.mantra || 'mantra'; - - const karmicLessonsText = numerologyProfile.karmicLessons.length > 0 - ? numerologyProfile.karmicLessons.join('; ') - : 'None specifically identified'; - - const destinyPlanet = numerologyProfile.planetaryInfluences['Destiny']?.planet || 'Unknown'; - const soulPlanet = numerologyProfile.planetaryInfluences['Soul']?.planet || 'Unknown'; - const personalityPlanet = numerologyProfile.planetaryInfluences['Personality']?.planet || 'Unknown'; - const birthDayPlanet = numerologyProfile.planetaryInfluences['Birth Day']?.planet || 'Unknown'; - - return `You are an expert Vedic astro-numerologist specializing in combining Vedic Astrology (Jyotish - Sidereal Zodiac) with Vedic Numerology (Navagraha planetary number system). You have deep knowledge of: - -1. Vedic Astrology: Sidereal zodiac (based on actual star positions), Moon sign importance, Lagna (Ascendant), Nakshatras, Dasha system, and karmic interpretations -2. Vedic Numerology: Navagraha planetary number associations (1=Sun/Surya, 2=Moon/Chandra, 3=Jupiter/Guru, 4=Rahu, 5=Mercury/Budha, 6=Venus/Shukra, 7=Ketu, 8=Saturn/Shani, 9=Mars/Mangal) -3. Karmic insights: Reincarnation, past life influences, and spiritual growth -4. Remedial measures: Gemstones, mantras, and upayas (remedies) based on planetary influences - -User Profile: -- Moon Sign: ${moonSign} (Vedic Sidereal - MOST IMPORTANT in Vedic astrology, represents mind and emotions) -- Lagna (Ascendant): ${lagnaSign} (Vedic Sidereal - represents physical body and life path) -- Sun Sign: ${sunSign} (Vedic Sidereal - represents soul and individuality) -- Life Path Number: ${numerologyProfile.lifePathNumber} (Ruled by ${rulingPlanet}) -- Destiny Number: ${numerologyProfile.destinyNumber} (Ruled by ${destinyPlanet}) -- Soul Number: ${numerologyProfile.soulNumber} (Ruled by ${soulPlanet}) -- Personality Number: ${numerologyProfile.personalityNumber ?? numerologyProfile.nameNumber} (Ruled by ${personalityPlanet}) -- Birth Day Number: ${numerologyProfile.birthDayNumber} (Ruled by ${birthDayPlanet}) -- Karmic Lessons: ${karmicLessonsText} -- Birth Date: ${birthDate} -- Full Name: ${fullName} -- Current Year: ${currentYear} - -Generate a comprehensive Vedic Astro-Numerology analysis. Emphasize: -1. Moon sign prominence (more important than Sun in Vedic system) -2. Planetary number associations and their Navagraha connections -3. Karmic insights with reincarnation perspective -4. Dasha system connections (how numerology numbers relate to planetary periods) -5. Vedic remedies: gemstones and mantras based on ruling planets -6. Spiritual growth and dharma (life purpose) - -Format your response as a JSON object with the following structure: -{ - "personalitySynthesis": "Detailed paragraph explaining how the Moon sign (most important), Lagna, Sun sign, and numerology numbers work together. Emphasize Vedic (Sidereal) interpretations, emotional nature from Moon, and how planetary number influences create a unique personality profile. Include references to Navagraha planets.", - "karmicInsights": "Detailed paragraph about karmic lessons as chart tendencies and dasha-activated themes — not fixed punishment. Explain how Moon nakshatra, Saturn/Rahu/Ketu placements, numerology karmic lessons (${karmicLessonsText}), and current dasha chapters shape growth-through-friction. Use awareness language: what to learn, prepare for, or release. No fatalism.", - "remedies": "Detailed paragraph about Vedic remedial measures: recommended gemstones based on ruling planets (${lifePathGemstone}, ${destinyGemstone}), mantras (${lifePathMantra}), and upayas to balance planetary influences.", - "careerGuidance": "Detailed paragraph about career paths aligned with Moon sign, Lagna, and numerology numbers. Include references to dharma (life purpose) and how planetary number associations guide vocational choices.", - "relationshipInsights": "Detailed paragraph about relationship dynamics from Vedic perspective, combining Moon sign (emotional nature), Lagna (physical attraction), and numerology compatibility patterns.", - "lifePurpose": "Detailed paragraph about deeper life purpose (dharma) combining Vedic astrological insights with numerological patterns. Emphasize spiritual growth and karmic destiny.", - "personalGrowth": "Detailed paragraph with specific Vedic-based recommendations for personal development, including spiritual practices, meditation suggestions, and ways to balance planetary influences.", - "challenges": ["Challenge 1 related to planetary influences and karmic lessons", "Challenge 2 description", "Challenge 3 description"], - "opportunities": ["Opportunity 1 related to favorable planetary combinations", "Opportunity 2 description", "Opportunity 3 description"], - "yearlyForecast": "Detailed paragraph about ${currentYear} forecast based on Vedic system, including significant Dasha periods, favorable times for numerology number manifestations, and key dates to watch." -} - -Write in the voice of a Vedic seer addressing the person directly (use "you" not "he/she" or third person). Be warm, spiritual, and deeply insightful. Reference Vedic concepts naturally.`; -} - -type VedicComprehensiveAnalysis = NonNullable< - VedicAstroNumerologyResponse['data'] ->['comprehensiveAnalysis']; - -function extractVedicAstroNumerologyAnalysisFromCache( - cachedData: Record, -): VedicComprehensiveAnalysis | null { - const data = - (cachedData.data as VedicAstroNumerologyResponse['data'] | undefined) || - (cachedData as VedicAstroNumerologyResponse['data']); - const analysis = data?.comprehensiveAnalysis; - if (!analysis?.personalitySynthesis?.trim()) return null; - return analysis; -} - -async function readVedicAstroNumerologyCache( - userId: string, - birthDataKey: string, - options?: { allowStale?: boolean }, -): Promise { - try { - const docSnap = await getCachedDoc(['users', userId, 'vedicAstroNumerologyReports'], 'current'); - if (!docSnap?.exists()) return null; - const cachedData = docSnap.data() as Record; - if ((cachedData.birthDataKey as string | undefined) !== birthDataKey) return null; - const lastUpdated = cachedData.timestamp as number | undefined; - if (!lastUpdated) return null; - if (!options?.allowStale) { - const hoursSinceUpdate = (Date.now() - lastUpdated) / (1000 * 60 * 60); - if (hoursSinceUpdate >= 24) return null; } - return extractVedicAstroNumerologyAnalysisFromCache(cachedData); - } catch { - return null; - } -} - -function buildDeterministicVedicAstroNumerology( - moonSign: string, - lagnaSign: string, - numerologyProfile: VedicNumerologyProfile, -): VedicComprehensiveAnalysis { - const karmaBundle = buildVedicKarmaInsights({ - ascendant: { signName: lagnaSign }, - planets: [{ name: 'Moon', signName: moonSign }], - }); - const karmicLessons = - numerologyProfile.karmicLessons.length > 0 - ? numerologyProfile.karmicLessons.join(' ') - : 'Integrating planetary number patterns with patience and self-awareness.'; - const karmicInsights = [ - karmaBundle.philosophy, - `With ${moonSign} Moon and ${lagnaSign} Lagna, emotional habit and life approach interact with Navagraha numerology: ${karmicLessons}`, - karmaBundle.dashaTheme, - ] - .filter(Boolean) - .join(' '); - - return { - personalitySynthesis: `Your ${moonSign} Moon sign (most important in Vedic astrology) combines with Life Path ${numerologyProfile.lifePathNumber} (ruled by ${numerologyProfile.planetaryInfluences['Life Path']?.planet}) and Lagna ${lagnaSign} to create a unique Vedic personality profile.`, - karmicInsights, - remedies: `Recommended gemstones: ${numerologyProfile.planetaryInfluences['Life Path']?.gemstone || 'Ruby'} for Life Path, ${numerologyProfile.planetaryInfluences['Destiny']?.gemstone || 'Pearl'} for Destiny.`, - careerGuidance: `Career paths aligned with your ${moonSign} Moon sign and numerology numbers would be most fulfilling.`, - relationshipInsights: `Your relationship style is influenced by your ${moonSign} Moon sign and numerological patterns.`, - lifePurpose: - 'Your life purpose (dharma) is revealed through the combination of Vedic astrological and numerological influences.', - personalGrowth: - 'Focus on developing spiritual awareness and balancing planetary influences for optimal growth.', - challenges: ['Balancing different planetary influences', 'Integrating karmic lessons'], - opportunities: ['Leveraging favorable planetary combinations', 'Aligning with dharma'], - yearlyForecast: - 'This year brings opportunities to integrate your Vedic astrological and numerological influences.', - }; -} -function mapVedicAstroNumerologyParsed(parsed: Record): VedicComprehensiveAnalysis { - return { - personalitySynthesis: String(parsed.personalitySynthesis ?? ''), - karmicInsights: String(parsed.karmicInsights ?? ''), - remedies: String(parsed.remedies ?? ''), - careerGuidance: String(parsed.careerGuidance ?? ''), - relationshipInsights: String(parsed.relationshipInsights ?? ''), - lifePurpose: String(parsed.lifePurpose ?? ''), - personalGrowth: String(parsed.personalGrowth ?? ''), - challenges: Array.isArray(parsed.challenges) ? parsed.challenges.map(String) : [], - opportunities: Array.isArray(parsed.opportunities) ? parsed.opportunities.map(String) : [], - yearlyForecast: String(parsed.yearlyForecast ?? ''), - }; -} - -function textFallbackVedicAstroNumerology(response: string): VedicComprehensiveAnalysis { - const sections = response.split(/\n\n+/); - return { - personalitySynthesis: sections[0] || response.substring(0, 300), - karmicInsights: sections[1] || 'Karmic insights based on your Vedic Astro-Numerology profile.', - remedies: sections[2] || 'Vedic remedies and gemstone recommendations for your planetary influences.', - careerGuidance: sections[3] || 'Career guidance based on your combined Vedic astrological and numerological profile.', - relationshipInsights: sections[4] || 'Relationship insights from your Vedic Astro-Numerology combination.', - lifePurpose: sections[5] || 'Life purpose (dharma) revealed through Vedic Astro-Numerology analysis.', - personalGrowth: sections[6] || 'Personal growth recommendations for your Vedic journey.', - challenges: [ - 'Balancing different planetary influences', - 'Integrating karmic lessons', - 'Developing spiritual awareness', - ], - opportunities: [ - 'Harnessing favorable planetary combinations', - 'Aligning with dharma', - 'Connecting with spiritual practices', - ], - yearlyForecast: - sections[7] || `Your ${new Date().getFullYear()} forecast based on Vedic Astro-Numerology profile.`, - }; -} - -function parseGroqResponse(response: GroqStructuredParseInput): VedicComprehensiveAnalysis { - if (isGroqParsedRecord(response)) { - return mapVedicAstroNumerologyParsed(response); - } - - const trimmed = response.trim(); - if (!trimmed) { - return textFallbackVedicAstroNumerology(''); - } - - const structured = parseStructuredJsonFromResponse(trimmed); - if (structured.ok && structured.data) { - return mapVedicAstroNumerologyParsed(structured.data); - } - - devLog.warn('Failed to parse JSON from Groq response, using fallback', undefined, 'vedic-astro-numerology'); - return textFallbackVedicAstroNumerology(trimmed); -} - -export async function POST(request: NextRequest) { - try { - const { userId, birthDate, fullName, moonSign, lagnaSign, sunSign, numerologyProfile }: VedicAstroNumerologyRequest = await request.json(); + const result = await generateVedicAstroNumerologyAnalysis({ + userId: auth.uid, + birthDate: typeof body.birthDate === 'string' ? body.birthDate : '', + fullName: typeof body.fullName === 'string' ? body.fullName : '', + moonSign: typeof body.moonSign === 'string' ? body.moonSign : '', + lagnaSign: typeof body.lagnaSign === 'string' ? body.lagnaSign : '', + sunSign: typeof body.sunSign === 'string' ? body.sunSign : '', + numerologyProfile: body.numerologyProfile as VedicNumerologyProfile, + useCache: true, + }); - // Validate required fields - if (!userId || !birthDate || !fullName) { - return NextResponse.json({ - success: false, - error: 'Missing required parameters: userId, birthDate, or fullName' - }, { status: 400 }); + if (!result.ok) { + return NextResponse.json({ success: false, error: result.error }, { status: result.status }); } - if (!moonSign || moonSign === 'Unknown') { - return NextResponse.json({ + return NextResponse.json({ success: true, data: result.data }); + } catch (error) { + devLog.error('❌ Vedic Astro-Numerology API error:', error, 'vedic-astro-numerology'); + return NextResponse.json( + { success: false, - error: 'Moon sign is required. Please ensure Vedic chart data is available.' - }, { status: 400 }); - } - - devLog.info('🔮 Vedic Astro-Numerology API: Generating comprehensive report for user:', userId, 'vedic-astro-numerology'); - - const birthDataKey = `${birthDate}_${fullName}_${moonSign}_${lagnaSign}`; - - try { - const cached = await readVedicAstroNumerologyCache(userId, birthDataKey); - if (cached) { - devLog.info('✅ Returning cached Vedic Astro-Numerology report for user:', userId, 'vedic-astro-numerology'); - return NextResponse.json({ - success: true, - data: { - moonSign, - lagnaSign, - sunSign, - lifePathNumber: numerologyProfile.lifePathNumber, - rulingPlanet: numerologyProfile.planetaryInfluences['Life Path']?.planet || 'Sun', - comprehensiveAnalysis: cached, - timestamp: Date.now(), - }, - }); - } - } catch (cacheError: unknown) { - devLog.warn('⚠️ Error checking cache, proceeding with generation:', cacheError, 'vedic-astro-numerology'); - } - - if (!process.env.GROQ_API_KEY) { - devLog.error('❌ GROQ_API_KEY is not configured', undefined, 'route'); - return NextResponse.json({ - success: true, - data: { - moonSign, - lagnaSign, - sunSign, - lifePathNumber: numerologyProfile.lifePathNumber, - rulingPlanet: numerologyProfile.planetaryInfluences['Life Path']?.planet || 'Sun', - comprehensiveAnalysis: buildDeterministicVedicAstroNumerology( - moonSign, - lagnaSign, - numerologyProfile, - ), - timestamp: Date.now(), - }, - }); - } - - const prompt = buildVedicGroqPrompt( - moonSign, - lagnaSign, - sunSign, - numerologyProfile, - birthDate, - fullName, - ); - - devLog.info('🤖 Calling AI for comprehensive Vedic Astro-Numerology analysis...', undefined, 'vedic-astro-numerology'); - - const resolved = await resolveAiReportWithFallback({ - label: 'vedic-astro-numerology-comprehensive', - userId, - tryLlm: async () => { - const structured = await callStructuredAI({ - label: 'vedic-astro-numerology-comprehensive', - model: GROQ_DEFAULT_TEXT_MODEL, - userId, - messages: [ - { - role: 'system', - content: - 'You are an expert Vedic astro-numerologist with deep knowledge of Jyotish (Vedic Astrology), Navagraha planetary number associations, karmic interpretations, Dasha system, and Vedic remedies. You speak in the voice of a seer addressing the person directly. Always respond with valid JSON when requested.', - }, - { role: 'user', content: prompt }, - ], - temperature: 0.75, - maxTokens: 3000, - responseFormat: { type: 'json_object' }, - maxAttempts: 3, - }); - - if (!structured.ok && structured.failureMode !== 'none') { - devLog.warn( - `vedic-astro-numerology structured AI: ${structured.failureMode} after ${structured.attempts} attempt(s)`, - undefined, - 'vedic-astro-numerology', - ); - } - - if (structured.ok && structured.raw) { - return { - data: mapVedicAstroNumerologyParsed(structured.raw), - attempts: structured.attempts, - failureMode: 'none', - }; - } - const recovered = structured.lastRaw - ? parseStructuredJsonFromResponse(structured.lastRaw) - : null; - if (recovered?.ok && recovered.data) { - return { - data: mapVedicAstroNumerologyParsed(recovered.data), - attempts: structured.attempts, - failureMode: structured.failureMode, - }; - } - return { - data: null, - attempts: structured.attempts, - failureMode: structured.failureMode as StructuredFailureMode, - parsingFailed: true, - }; + error: error instanceof Error ? error.message : 'Failed to generate Vedic Astro-Numerology analysis', }, - readFirestoreCache: () => - readVedicAstroNumerologyCache(userId, birthDataKey, { allowStale: true }), - buildDeterministic: () => - buildDeterministicVedicAstroNumerology(moonSign, lagnaSign, numerologyProfile), - }); - - const responseData: VedicAstroNumerologyResponse['data'] = { - moonSign, - lagnaSign, - sunSign, - lifePathNumber: numerologyProfile.lifePathNumber, - rulingPlanet: numerologyProfile.planetaryInfluences['Life Path']?.planet || 'Sun', - comprehensiveAnalysis: resolved.data, - timestamp: Date.now(), - }; - - if (resolved.degraded && resolved.source !== 'llm') { - return NextResponse.json({ - success: true, - data: { - ...responseData, - parsingFailed: resolved.parsingFailed ?? true, - fallbackSource: resolved.source, - error: - resolved.source === 'firestore_cache' - ? 'Using last saved report; AI narrative refresh failed' - : 'Failed to parse AI response, using chart-based defaults', - }, - }); - } - - try { - await setCachedDoc(['users', userId, 'vedicAstroNumerologyReports'], 'current', { - data: responseData, - birthDataKey, - timestamp: Date.now(), - }); - devLog.info('✅ Cached Vedic Astro-Numerology report in Firebase', undefined, 'vedic-astro-numerology'); - } catch (cacheError: unknown) { - devLog.warn('⚠️ Error caching report:', cacheError, 'vedic-astro-numerology'); - } - - return NextResponse.json({ - success: true, - data: responseData, - }); - - } catch (error: any) { - devLog.error('❌ Vedic Astro-Numerology API error:', error, 'route'); - return NextResponse.json({ - success: false, - error: error.message || 'Failed to generate Vedic Astro-Numerology analysis' - }, { status: 500 }); + { status: 500 }, + ); } } +export const POST = withRateLimit( + handleVedicAstroNumerologyAnalysis, + rateLimiters.ai, + 'vedic_astro_numerology_analysis_post', +); diff --git a/components/vedic/VedicAstroNumerologyTab.tsx b/components/vedic/VedicAstroNumerologyTab.tsx index b387e00b..266e14cb 100644 --- a/components/vedic/VedicAstroNumerologyTab.tsx +++ b/components/vedic/VedicAstroNumerologyTab.tsx @@ -1,6 +1,7 @@ "use client" import { useMemo, useState, useEffect, useRef } from 'react' +import { fetchWithFirebaseAuthRequired } from '@/lib/clientFirebaseFetch' import { devLog } from '@/lib/devLogger'; import { motion } from 'framer-motion' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' @@ -153,7 +154,7 @@ export default function VedicAstroNumerologyTab({ setAnalysisError(null) try { - const response = await fetch('/api/vedic-astro-numerology/analysis', { + const response = await fetchWithFirebaseAuthRequired('/api/vedic-astro-numerology/analysis', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/components/western/AstroNumerologyTab.tsx b/components/western/AstroNumerologyTab.tsx index 22c18716..a1c64709 100644 --- a/components/western/AstroNumerologyTab.tsx +++ b/components/western/AstroNumerologyTab.tsx @@ -1,6 +1,7 @@ "use client" import { useMemo, useState, useEffect } from 'react' +import { fetchWithFirebaseAuthRequired } from '@/lib/clientFirebaseFetch' import { devLog } from '@/lib/devLogger'; import { motion } from 'framer-motion' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' @@ -212,7 +213,7 @@ export default function AstroNumerologyTab({ setAnalysisError(null) try { - const response = await fetch('/api/astro-numerology/analysis', { + const response = await fetchWithFirebaseAuthRequired('/api/astro-numerology/analysis', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/lib/astroNumerology/generateAstroNumerologyAnalysis.ts b/lib/astroNumerology/generateAstroNumerologyAnalysis.ts new file mode 100644 index 00000000..9ce96a65 --- /dev/null +++ b/lib/astroNumerology/generateAstroNumerologyAnalysis.ts @@ -0,0 +1,285 @@ +import { resolveAiReportWithFallback } from '@/lib/aiFallbackRouter'; +import { callStructuredAI } from '@/lib/aiStructuredOutput'; +import { parseStructuredJsonFromResponse } from '@/lib/aiStructuredOutputParse'; +import { calculateLifePathNumber, calculateDestinyNumber } from '@/lib/numerologyCalculations'; +import { userSubdocGet, userSubdocSet } from '@/lib/userSubcollectionFirestore'; +import { devLog } from '@/lib/devLogger'; +import { GROQ_DEFAULT_TEXT_MODEL } from '@/lib/groqModels'; + +export interface AstroNumerologyAnalysisData { + sunSign: string; + lifePathNumber: number; + nameNumber: number; + comprehensiveAnalysis: AstroNumerologyComprehensiveAnalysis; + timestamp: number; +} + +export interface AstroNumerologyComprehensiveAnalysis { + personalitySynthesis: string; + careerGuidance: string; + relationshipInsights: string; + lifePurpose: string; + personalGrowth: string; + challenges: string[]; + opportunities: string[]; + yearlyForecast: string; +} + +export type GenerateAstroNumerologyResult = + | { ok: true; data: AstroNumerologyAnalysisData } + | { ok: false; error: string; status: number }; + +function buildGroqPrompt( + sunSign: string, + lifePathNumber: number, + nameNumber: number, + birthDate: string, + fullName: string, +): string { + const currentYear = new Date().getFullYear(); + + return `You are an expert astro-numerologist specializing in combining Western Astrology (Tropical Zodiac) with Pythagorean Numerology. + +User Profile: +- Sun Sign: ${sunSign} (Western Astrology - represents core personality) +- Life Path Number: ${lifePathNumber} (from birth date - represents life journey) +- Name Number: ${nameNumber} (from full name - represents natural talents) +- Birth Date: ${birthDate} +- Full Name: ${fullName} +- Current Year: ${currentYear} + +Generate a comprehensive astro-numerology analysis covering all life areas. Provide detailed, insightful, and practical guidance. Write in a warm, empowering, and accessible tone. + +Format your response as a JSON object with the following structure: +{ + "personalitySynthesis": "Detailed paragraph explaining how the sun sign, life path number, and name number work together to create a unique personality profile. Be specific and insightful, showing how these energies blend.", + "careerGuidance": "Detailed paragraph about career paths that align with these combined energies, what the life purpose reveals, and specific vocational directions.", + "relationshipInsights": "Detailed paragraph about how these energies manifest in relationships, compatibility patterns, and interpersonal dynamics.", + "lifePurpose": "Detailed paragraph about the deeper life purpose when combining astrological and numerological insights, including destiny themes.", + "personalGrowth": "Detailed paragraph with specific recommendations for personal development based on the combined analysis, including actionable steps.", + "challenges": ["Challenge 1 description", "Challenge 2 description", "Challenge 3 description"], + "opportunities": ["Opportunity 1 description", "Opportunity 2 description", "Opportunity 3 description"], + "yearlyForecast": "Detailed paragraph about insights for ${currentYear} based on the numbers and sun sign, including key themes and timing considerations." +} + +Make each section comprehensive yet concise, providing valuable insights that help the user understand themselves better and navigate their life path.`; +} + +function extractAstroNumerologyAnalysisFromCache( + cachedData: Record, +): AstroNumerologyComprehensiveAnalysis | null { + const nested = cachedData.data as AstroNumerologyAnalysisData | undefined; + const analysis = nested?.comprehensiveAnalysis ?? (cachedData as Partial).comprehensiveAnalysis; + if (!analysis?.personalitySynthesis?.trim()) return null; + return analysis; +} + +async function readAstroNumerologyCache( + userId: string, + birthDataKey: string, + options?: { allowStale?: boolean }, +): Promise { + try { + const cachedData = await userSubdocGet(userId, 'astroNumerologyReports', 'current'); + if (!cachedData) return null; + const cachedBirthKey = cachedData.birthDataKey as string | undefined; + if (cachedBirthKey !== birthDataKey) return null; + const lastUpdated = cachedData.timestamp as number | undefined; + if (!lastUpdated) return null; + if (!options?.allowStale) { + const hoursSinceUpdate = (Date.now() - lastUpdated) / (1000 * 60 * 60); + if (hoursSinceUpdate >= 24) return null; + } + return extractAstroNumerologyAnalysisFromCache(cachedData); + } catch { + return null; + } +} + +function buildDeterministicAstroNumerology( + actualSunSign: string, + lifePathNumber: number, + nameNumber: number, +): AstroNumerologyComprehensiveAnalysis { + return { + personalitySynthesis: `Your ${actualSunSign} sun sign combines with Life Path ${lifePathNumber} and Name Number ${nameNumber} to create a unique personality blend.`, + careerGuidance: `Career paths that align with Life Path ${lifePathNumber} and your ${actualSunSign} traits would be most fulfilling.`, + relationshipInsights: `Your relationship style is influenced by both your ${actualSunSign} nature and your numerological patterns.`, + lifePurpose: 'Your life purpose is revealed through the combination of your astrological and numerological influences.', + personalGrowth: + 'Focus on developing the strengths of both your sun sign and your life path number for optimal growth.', + challenges: [ + 'Balancing different aspects of your personality', + 'Aligning actions with your life purpose', + ], + opportunities: [ + 'Leveraging your unique combination of energies', + 'Connecting with like-minded individuals', + ], + yearlyForecast: + 'This year brings opportunities to integrate your astrological and numerological influences.', + }; +} + +function mapAstroNumerologyParsed( + parsed: Record, +): AstroNumerologyComprehensiveAnalysis { + return { + personalitySynthesis: String(parsed.personalitySynthesis ?? ''), + careerGuidance: String(parsed.careerGuidance ?? ''), + relationshipInsights: String(parsed.relationshipInsights ?? ''), + lifePurpose: String(parsed.lifePurpose ?? ''), + personalGrowth: String(parsed.personalGrowth ?? ''), + challenges: Array.isArray(parsed.challenges) ? parsed.challenges.map(String) : [], + opportunities: Array.isArray(parsed.opportunities) ? parsed.opportunities.map(String) : [], + yearlyForecast: String(parsed.yearlyForecast ?? ''), + }; +} + +export async function generateAstroNumerologyAnalysis(params: { + userId: string; + birthDate: string; + fullName: string; + sunSign?: string; + useCache?: boolean; +}): Promise { + const { userId, birthDate, fullName, useCache = true } = params; + if (!userId || !birthDate || !fullName) { + return { ok: false, error: 'Missing required parameters: userId, birthDate, or fullName', status: 400 }; + } + + const lifePathNumber = calculateLifePathNumber(birthDate); + const nameNumber = calculateDestinyNumber(fullName); + const actualSunSign = params.sunSign || 'Unknown'; + + if (actualSunSign === 'Unknown') { + return { + ok: false, + error: 'Sun sign is required. Please ensure Western astrology chart data is available.', + status: 400, + }; + } + + const birthDataKey = `${birthDate}_${fullName}_${actualSunSign}`; + + if (useCache) { + try { + const cached = await readAstroNumerologyCache(userId, birthDataKey); + if (cached) { + return { + ok: true, + data: { + sunSign: actualSunSign, + lifePathNumber, + nameNumber, + comprehensiveAnalysis: cached, + timestamp: Date.now(), + }, + }; + } + } catch (cacheError: unknown) { + devLog.warn('⚠️ Error checking cache, proceeding with generation:', cacheError, 'astro-numerology'); + } + } + + if (!process.env.GROQ_API_KEY) { + return { + ok: true, + data: { + sunSign: actualSunSign, + lifePathNumber, + nameNumber, + comprehensiveAnalysis: buildDeterministicAstroNumerology( + actualSunSign, + lifePathNumber, + nameNumber, + ), + timestamp: Date.now(), + }, + }; + } + + const prompt = buildGroqPrompt(actualSunSign, lifePathNumber, nameNumber, birthDate, fullName); + + const resolved = await resolveAiReportWithFallback({ + label: 'astro-numerology-comprehensive', + userId, + tryLlm: async () => { + const structured = await callStructuredAI({ + label: 'astro-numerology-comprehensive', + model: GROQ_DEFAULT_TEXT_MODEL, + userId, + messages: [ + { + role: 'system', + content: + 'You are an expert astro-numerologist specializing in combining Western Astrology (Tropical Zodiac) with Pythagorean Numerology. Provide comprehensive, insightful, and practical guidance. Always respond with valid JSON when requested.', + }, + { role: 'user', content: prompt }, + ], + temperature: 0.75, + maxTokens: 2500, + responseFormat: { type: 'json_object' }, + maxAttempts: 3, + }); + + if (!structured.ok && structured.failureMode !== 'none') { + devLog.warn( + `astro-numerology structured AI: ${structured.failureMode} after ${structured.attempts} attempt(s)`, + undefined, + 'astro-numerology', + ); + } + + if (structured.ok && structured.raw) { + return { + data: mapAstroNumerologyParsed(structured.raw), + attempts: structured.attempts, + failureMode: 'none' as const, + }; + } + const recovered = structured.lastRaw + ? parseStructuredJsonFromResponse(structured.lastRaw) + : null; + if (recovered?.ok && recovered.data) { + return { + data: mapAstroNumerologyParsed(recovered.data), + attempts: structured.attempts, + failureMode: structured.failureMode, + }; + } + return { + data: null, + attempts: structured.attempts, + failureMode: structured.failureMode, + parsingFailed: true, + }; + }, + readFirestoreCache: () => + useCache ? readAstroNumerologyCache(userId, birthDataKey, { allowStale: true }) : Promise.resolve(null), + buildDeterministic: () => + buildDeterministicAstroNumerology(actualSunSign, lifePathNumber, nameNumber), + }); + + const responseData: AstroNumerologyAnalysisData = { + sunSign: actualSunSign, + lifePathNumber, + nameNumber, + comprehensiveAnalysis: resolved.data, + timestamp: Date.now(), + }; + + if (useCache && !(resolved.degraded && resolved.source !== 'llm')) { + try { + await userSubdocSet(userId, 'astroNumerologyReports', 'current', { + data: responseData, + birthDataKey, + timestamp: Date.now(), + }); + } catch (cacheError: unknown) { + devLog.warn('⚠️ Error caching report:', cacheError, 'astro-numerology'); + } + } + + return { ok: true, data: responseData }; +} diff --git a/lib/profileGenerationOrchestrator.ts b/lib/profileGenerationOrchestrator.ts index 688c608b..e700414a 100644 --- a/lib/profileGenerationOrchestrator.ts +++ b/lib/profileGenerationOrchestrator.ts @@ -1718,7 +1718,6 @@ export async function finalizeProfileGenerationFromToolReports( toolReports: ToolReports, existingProfile?: Record, ): Promise { - const baseUrl = getServerBaseUrl(); const profile: UserProfile = { ...userProfile, birthTime: normalizeBirthTime(userProfile.birthTime) || userProfile.birthTime || '12:00:00', @@ -1777,34 +1776,30 @@ export async function finalizeProfileGenerationFromToolReports( const fullName = (profile.displayName ?? (profile as unknown as Record).fullName ?? '') as string; if (birthDate && fullName && moonSign !== 'Unknown') { const numerologyProfile = calculateVedicNumerologyProfile(fullName, birthDate); - const res = await fetch(`${baseUrl}/api/vedic-astro-numerology/analysis`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - userId, - birthDate, - fullName, - moonSign, - lagnaSign: lagnaSign !== 'Unknown' ? lagnaSign : 'Aries', - sunSign: sunSign !== 'Unknown' ? sunSign : 'Aries', - numerologyProfile, - }), + // Lazy import: avoid loading Groq/Admin into Jest suites that only import the orchestrator. + const { generateVedicAstroNumerologyAnalysis } = await import( + './vedicAstroNumerology/generateVedicAstroNumerologyAnalysis' + ); + const result = await generateVedicAstroNumerologyAnalysis({ + userId, + birthDate, + fullName, + moonSign, + lagnaSign: lagnaSign !== 'Unknown' ? lagnaSign : 'Aries', + sunSign: sunSign !== 'Unknown' ? sunSign : 'Aries', + numerologyProfile, + useCache: true, }); - if (res.ok) { - const result = await res.json(); - addResponseUsage(aggregateUsage, result); - const data = result?.data ?? result; + if (result.ok) { toolReports.vedicAstroNumerology = { status: 'success', - data: data as Record, + data: result.data as unknown as Record, generatedAt: vedicAstroNumGeneratedAt, - _usage: result._usage ?? result.usage, }; } else { - const err = await res.json().catch(() => ({})); toolReports.vedicAstroNumerology = { status: 'failed', - error: err?.error ?? `API ${res.status}`, + error: result.error, generatedAt: vedicAstroNumGeneratedAt, }; failedTools.push('vedicAstroNumerology'); @@ -1849,26 +1844,27 @@ export async function finalizeProfileGenerationFromToolReports( const birthDate = profile.birthDate ?? ''; const fullName = (profile.displayName ?? (profile as unknown as Record).fullName ?? '') as string; if (birthDate && fullName && sunSign !== 'Unknown') { - const res = await fetch(`${baseUrl}/api/astro-numerology/analysis`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ userId, birthDate, fullName, sunSign }), + // Lazy import: avoid loading Groq/Admin into Jest suites that only import the orchestrator. + const { generateAstroNumerologyAnalysis } = await import( + './astroNumerology/generateAstroNumerologyAnalysis' + ); + const result = await generateAstroNumerologyAnalysis({ + userId, + birthDate, + fullName, + sunSign, + useCache: true, }); - if (res.ok) { - const result = await res.json(); - addResponseUsage(aggregateUsage, result); - const data = result?.data ?? result; + if (result.ok) { toolReports.astroNumerology = { status: 'success', - data: data as Record, + data: result.data as unknown as Record, generatedAt: astroNumGeneratedAt, - _usage: result._usage ?? result.usage, }; } else { - const err = await res.json().catch(() => ({})); toolReports.astroNumerology = { status: 'failed', - error: (err as { error?: string })?.error ?? `API ${res.status}`, + error: result.error, generatedAt: astroNumGeneratedAt, }; failedTools.push('astroNumerology'); diff --git a/lib/vedicAstroNumerology/generateVedicAstroNumerologyAnalysis.ts b/lib/vedicAstroNumerology/generateVedicAstroNumerologyAnalysis.ts new file mode 100644 index 00000000..df8f5cf8 --- /dev/null +++ b/lib/vedicAstroNumerology/generateVedicAstroNumerologyAnalysis.ts @@ -0,0 +1,368 @@ +import { resolveAiReportWithFallback } from '@/lib/aiFallbackRouter'; +import { callStructuredAI } from '@/lib/aiStructuredOutput'; +import { + parseStructuredJsonFromResponse, + type StructuredFailureMode, +} from '@/lib/aiStructuredOutputParse'; +import { type VedicNumerologyProfile } from '@/lib/vedicNumerologyCalculations'; +import { buildVedicKarmaInsights } from '@/lib/vedic/karmaChartInsights'; +import { userSubdocGet, userSubdocSet } from '@/lib/userSubcollectionFirestore'; +import { devLog } from '@/lib/devLogger'; +import { GROQ_DEFAULT_TEXT_MODEL } from '@/lib/groqModels'; + +export interface VedicAstroNumerologyAnalysisData { + moonSign: string; + lagnaSign: string; + sunSign: string; + lifePathNumber: number; + rulingPlanet: string; + comprehensiveAnalysis: VedicAstroNumerologyComprehensiveAnalysis; + timestamp: number; +} + +export interface VedicAstroNumerologyComprehensiveAnalysis { + personalitySynthesis: string; + karmicInsights: string; + remedies: string; + careerGuidance: string; + relationshipInsights: string; + lifePurpose: string; + personalGrowth: string; + challenges: string[]; + opportunities: string[]; + yearlyForecast: string; +} + +export type GenerateVedicAstroNumerologyResult = + | { ok: true; data: VedicAstroNumerologyAnalysisData } + | { ok: false; error: string; status: number }; + +function buildVedicGroqPrompt( + moonSign: string, + lagnaSign: string, + sunSign: string, + numerologyProfile: VedicNumerologyProfile, + birthDate: string, + fullName: string, +): string { + const currentYear = new Date().getFullYear(); + const rulingPlanet = numerologyProfile.planetaryInfluences['Life Path']?.planet || 'Unknown'; + const lifePathGemstone = numerologyProfile.planetaryInfluences['Life Path']?.gemstone || 'gemstone'; + const destinyGemstone = numerologyProfile.planetaryInfluences['Destiny']?.gemstone || 'gemstone'; + const lifePathMantra = numerologyProfile.planetaryInfluences['Life Path']?.mantra || 'mantra'; + + const karmicLessonsText = numerologyProfile.karmicLessons.length > 0 + ? numerologyProfile.karmicLessons.join('; ') + : 'None specifically identified'; + + const destinyPlanet = numerologyProfile.planetaryInfluences['Destiny']?.planet || 'Unknown'; + const soulPlanet = numerologyProfile.planetaryInfluences['Soul']?.planet || 'Unknown'; + const personalityPlanet = numerologyProfile.planetaryInfluences['Personality']?.planet || 'Unknown'; + const birthDayPlanet = numerologyProfile.planetaryInfluences['Birth Day']?.planet || 'Unknown'; + + return `You are an expert Vedic astro-numerologist specializing in combining Vedic Astrology (Jyotish - Sidereal Zodiac) with Vedic Numerology (Navagraha planetary number system). You have deep knowledge of: + +1. Vedic Astrology: Sidereal zodiac (based on actual star positions), Moon sign importance, Lagna (Ascendant), Nakshatras, Dasha system, and karmic interpretations +2. Vedic Numerology: Navagraha planetary number associations (1=Sun/Surya, 2=Moon/Chandra, 3=Jupiter/Guru, 4=Rahu, 5=Mercury/Budha, 6=Venus/Shukra, 7=Ketu, 8=Saturn/Shani, 9=Mars/Mangal) +3. Karmic insights: Reincarnation, past life influences, and spiritual growth +4. Remedial measures: Gemstones, mantras, and upayas (remedies) based on planetary influences + +User Profile: +- Moon Sign: ${moonSign} (Vedic Sidereal - MOST IMPORTANT in Vedic astrology, represents mind and emotions) +- Lagna (Ascendant): ${lagnaSign} (Vedic Sidereal - represents physical body and life path) +- Sun Sign: ${sunSign} (Vedic Sidereal - represents soul and individuality) +- Life Path Number: ${numerologyProfile.lifePathNumber} (Ruled by ${rulingPlanet}) +- Destiny Number: ${numerologyProfile.destinyNumber} (Ruled by ${destinyPlanet}) +- Soul Number: ${numerologyProfile.soulNumber} (Ruled by ${soulPlanet}) +- Personality Number: ${numerologyProfile.personalityNumber ?? numerologyProfile.nameNumber} (Ruled by ${personalityPlanet}) +- Birth Day Number: ${numerologyProfile.birthDayNumber} (Ruled by ${birthDayPlanet}) +- Karmic Lessons: ${karmicLessonsText} +- Birth Date: ${birthDate} +- Full Name: ${fullName} +- Current Year: ${currentYear} + +Generate a comprehensive Vedic Astro-Numerology analysis. Emphasize: +1. Moon sign prominence (more important than Sun in Vedic system) +2. Planetary number associations and their Navagraha connections +3. Karmic insights with reincarnation perspective +4. Dasha system connections (how numerology numbers relate to planetary periods) +5. Vedic remedies: gemstones and mantras based on ruling planets +6. Spiritual growth and dharma (life purpose) + +Format your response as a JSON object with the following structure: +{ + "personalitySynthesis": "Detailed paragraph explaining how the Moon sign (most important), Lagna, Sun sign, and numerology numbers work together. Emphasize Vedic (Sidereal) interpretations, emotional nature from Moon, and how planetary number influences create a unique personality profile. Include references to Navagraha planets.", + "karmicInsights": "Detailed paragraph about karmic lessons as chart tendencies and dasha-activated themes — not fixed punishment. Explain how Moon nakshatra, Saturn/Rahu/Ketu placements, numerology karmic lessons (${karmicLessonsText}), and current dasha chapters shape growth-through-friction. Use awareness language: what to learn, prepare for, or release. No fatalism.", + "remedies": "Detailed paragraph about Vedic remedial measures: recommended gemstones based on ruling planets (${lifePathGemstone}, ${destinyGemstone}), mantras (${lifePathMantra}), and upayas to balance planetary influences.", + "careerGuidance": "Detailed paragraph about career paths aligned with Moon sign, Lagna, and numerology numbers. Include references to dharma (life purpose) and how planetary number associations guide vocational choices.", + "relationshipInsights": "Detailed paragraph about relationship dynamics from Vedic perspective, combining Moon sign (emotional nature), Lagna (physical attraction), and numerology compatibility patterns.", + "lifePurpose": "Detailed paragraph about deeper life purpose (dharma) combining Vedic astrological insights with numerological patterns. Emphasize spiritual growth and karmic destiny.", + "personalGrowth": "Detailed paragraph with specific Vedic-based recommendations for personal development, including spiritual practices, meditation suggestions, and ways to balance planetary influences.", + "challenges": ["Challenge 1 related to planetary influences and karmic lessons", "Challenge 2 description", "Challenge 3 description"], + "opportunities": ["Opportunity 1 related to favorable planetary combinations", "Opportunity 2 description", "Opportunity 3 description"], + "yearlyForecast": "Detailed paragraph about ${currentYear} forecast based on Vedic system, including significant Dasha periods, favorable times for numerology number manifestations, and key dates to watch." +} + +Write in the voice of a Vedic seer addressing the person directly (use "you" not "he/she" or third person). Be warm, spiritual, and deeply insightful. Reference Vedic concepts naturally.`; +} + +function extractVedicAstroNumerologyAnalysisFromCache( + cachedData: Record, +): VedicAstroNumerologyComprehensiveAnalysis | null { + const nested = cachedData.data as VedicAstroNumerologyAnalysisData | undefined; + const analysis = + nested?.comprehensiveAnalysis ?? + (cachedData as Partial).comprehensiveAnalysis; + if (!analysis?.personalitySynthesis?.trim()) return null; + return analysis; +} + +async function readVedicAstroNumerologyCache( + userId: string, + birthDataKey: string, + options?: { allowStale?: boolean }, +): Promise { + try { + const cachedData = await userSubdocGet(userId, 'vedicAstroNumerologyReports', 'current'); + if (!cachedData) return null; + if ((cachedData.birthDataKey as string | undefined) !== birthDataKey) return null; + const lastUpdated = cachedData.timestamp as number | undefined; + if (!lastUpdated) return null; + if (!options?.allowStale) { + const hoursSinceUpdate = (Date.now() - lastUpdated) / (1000 * 60 * 60); + if (hoursSinceUpdate >= 24) return null; + } + return extractVedicAstroNumerologyAnalysisFromCache(cachedData); + } catch { + return null; + } +} + +function buildDeterministicVedicAstroNumerology( + moonSign: string, + lagnaSign: string, + numerologyProfile: VedicNumerologyProfile, +): VedicAstroNumerologyComprehensiveAnalysis { + const karmaBundle = buildVedicKarmaInsights({ + ascendant: { signName: lagnaSign }, + planets: [{ name: 'Moon', signName: moonSign }], + }); + const karmicLessons = + numerologyProfile.karmicLessons.length > 0 + ? numerologyProfile.karmicLessons.join(' ') + : 'Integrating planetary number patterns with patience and self-awareness.'; + const karmicInsights = [ + karmaBundle.philosophy, + `With ${moonSign} Moon and ${lagnaSign} Lagna, emotional habit and life approach interact with Navagraha numerology: ${karmicLessons}`, + karmaBundle.dashaTheme, + ] + .filter(Boolean) + .join(' '); + + return { + personalitySynthesis: `Your ${moonSign} Moon sign (most important in Vedic astrology) combines with Life Path ${numerologyProfile.lifePathNumber} (ruled by ${numerologyProfile.planetaryInfluences['Life Path']?.planet}) and Lagna ${lagnaSign} to create a unique Vedic personality profile.`, + karmicInsights, + remedies: `Recommended gemstones: ${numerologyProfile.planetaryInfluences['Life Path']?.gemstone || 'Ruby'} for Life Path, ${numerologyProfile.planetaryInfluences['Destiny']?.gemstone || 'Pearl'} for Destiny.`, + careerGuidance: `Career paths aligned with your ${moonSign} Moon sign and numerology numbers would be most fulfilling.`, + relationshipInsights: `Your relationship style is influenced by your ${moonSign} Moon sign and numerological patterns.`, + lifePurpose: + 'Your life purpose (dharma) is revealed through the combination of Vedic astrological and numerological influences.', + personalGrowth: + 'Focus on developing spiritual awareness and balancing planetary influences for optimal growth.', + challenges: ['Balancing different planetary influences', 'Integrating karmic lessons'], + opportunities: ['Leveraging favorable planetary combinations', 'Aligning with dharma'], + yearlyForecast: + 'This year brings opportunities to integrate your Vedic astrological and numerological influences.', + }; +} + +function mapVedicAstroNumerologyParsed( + parsed: Record, +): VedicAstroNumerologyComprehensiveAnalysis { + return { + personalitySynthesis: String(parsed.personalitySynthesis ?? ''), + karmicInsights: String(parsed.karmicInsights ?? ''), + remedies: String(parsed.remedies ?? ''), + careerGuidance: String(parsed.careerGuidance ?? ''), + relationshipInsights: String(parsed.relationshipInsights ?? ''), + lifePurpose: String(parsed.lifePurpose ?? ''), + personalGrowth: String(parsed.personalGrowth ?? ''), + challenges: Array.isArray(parsed.challenges) ? parsed.challenges.map(String) : [], + opportunities: Array.isArray(parsed.opportunities) ? parsed.opportunities.map(String) : [], + yearlyForecast: String(parsed.yearlyForecast ?? ''), + }; +} + +export async function generateVedicAstroNumerologyAnalysis(params: { + userId: string; + birthDate: string; + fullName: string; + moonSign: string; + lagnaSign: string; + sunSign: string; + numerologyProfile: VedicNumerologyProfile; + useCache?: boolean; +}): Promise { + const { + userId, + birthDate, + fullName, + moonSign, + lagnaSign, + sunSign, + numerologyProfile, + useCache = true, + } = params; + + if (!userId || !birthDate || !fullName) { + return { ok: false, error: 'Missing required parameters: userId, birthDate, or fullName', status: 400 }; + } + if (!moonSign || moonSign === 'Unknown') { + return { + ok: false, + error: 'Moon sign is required. Please ensure Vedic chart data is available.', + status: 400, + }; + } + if (!numerologyProfile || typeof numerologyProfile.lifePathNumber !== 'number') { + return { ok: false, error: 'Numerology profile is required.', status: 400 }; + } + + const birthDataKey = `${birthDate}_${fullName}_${moonSign}_${lagnaSign}`; + + if (useCache) { + try { + const cached = await readVedicAstroNumerologyCache(userId, birthDataKey); + if (cached) { + return { + ok: true, + data: { + moonSign, + lagnaSign, + sunSign, + lifePathNumber: numerologyProfile.lifePathNumber, + rulingPlanet: numerologyProfile.planetaryInfluences['Life Path']?.planet || 'Sun', + comprehensiveAnalysis: cached, + timestamp: Date.now(), + }, + }; + } + } catch (cacheError: unknown) { + devLog.warn('⚠️ Error checking cache, proceeding with generation:', cacheError, 'vedic-astro-numerology'); + } + } + + if (!process.env.GROQ_API_KEY) { + return { + ok: true, + data: { + moonSign, + lagnaSign, + sunSign, + lifePathNumber: numerologyProfile.lifePathNumber, + rulingPlanet: numerologyProfile.planetaryInfluences['Life Path']?.planet || 'Sun', + comprehensiveAnalysis: buildDeterministicVedicAstroNumerology( + moonSign, + lagnaSign, + numerologyProfile, + ), + timestamp: Date.now(), + }, + }; + } + + const prompt = buildVedicGroqPrompt( + moonSign, + lagnaSign, + sunSign, + numerologyProfile, + birthDate, + fullName, + ); + + const resolved = await resolveAiReportWithFallback({ + label: 'vedic-astro-numerology-comprehensive', + userId, + tryLlm: async () => { + const structured = await callStructuredAI({ + label: 'vedic-astro-numerology-comprehensive', + model: GROQ_DEFAULT_TEXT_MODEL, + userId, + messages: [ + { + role: 'system', + content: + 'You are an expert Vedic astro-numerologist with deep knowledge of Jyotish (Vedic Astrology), Navagraha planetary number associations, karmic interpretations, Dasha system, and Vedic remedies. You speak in the voice of a seer addressing the person directly. Always respond with valid JSON when requested.', + }, + { role: 'user', content: prompt }, + ], + temperature: 0.75, + maxTokens: 3000, + responseFormat: { type: 'json_object' }, + maxAttempts: 3, + }); + + if (!structured.ok && structured.failureMode !== 'none') { + devLog.warn( + `vedic-astro-numerology structured AI: ${structured.failureMode} after ${structured.attempts} attempt(s)`, + undefined, + 'vedic-astro-numerology', + ); + } + + if (structured.ok && structured.raw) { + return { + data: mapVedicAstroNumerologyParsed(structured.raw), + attempts: structured.attempts, + failureMode: 'none' as const, + }; + } + const recovered = structured.lastRaw + ? parseStructuredJsonFromResponse(structured.lastRaw) + : null; + if (recovered?.ok && recovered.data) { + return { + data: mapVedicAstroNumerologyParsed(recovered.data), + attempts: structured.attempts, + failureMode: structured.failureMode, + }; + } + return { + data: null, + attempts: structured.attempts, + failureMode: structured.failureMode as StructuredFailureMode, + parsingFailed: true, + }; + }, + readFirestoreCache: () => + useCache + ? readVedicAstroNumerologyCache(userId, birthDataKey, { allowStale: true }) + : Promise.resolve(null), + buildDeterministic: () => + buildDeterministicVedicAstroNumerology(moonSign, lagnaSign, numerologyProfile), + }); + + const responseData: VedicAstroNumerologyAnalysisData = { + moonSign, + lagnaSign, + sunSign, + lifePathNumber: numerologyProfile.lifePathNumber, + rulingPlanet: numerologyProfile.planetaryInfluences['Life Path']?.planet || 'Sun', + comprehensiveAnalysis: resolved.data, + timestamp: Date.now(), + }; + + if (useCache && !(resolved.degraded && resolved.source !== 'llm')) { + try { + await userSubdocSet(userId, 'vedicAstroNumerologyReports', 'current', { + data: responseData, + birthDataKey, + timestamp: Date.now(), + }); + } catch (cacheError: unknown) { + devLog.warn('⚠️ Error caching report:', cacheError, 'vedic-astro-numerology'); + } + } + + return { ok: true, data: responseData }; +} diff --git a/tests/integration/astro-numerology-analysis-auth.test.ts b/tests/integration/astro-numerology-analysis-auth.test.ts new file mode 100644 index 00000000..c550e452 --- /dev/null +++ b/tests/integration/astro-numerology-analysis-auth.test.ts @@ -0,0 +1,133 @@ +/** + * Astro-numerology analysis must not be an unauthenticated paid proxy (Groq) + * or an Admin-cache IDOR on users/{userId}/astroNumerologyReports. + * @jest-environment node + */ + +import { NextRequest } from 'next/server'; + +const mockVerifyIdToken = jest.fn(); +const mockGenerateAstroNumerologyAnalysis = jest.fn(); + +jest.mock('@/lib/firebase-admin', () => ({ + getAuth: () => ({ verifyIdToken: mockVerifyIdToken }), +})); + +jest.mock('@/lib/astroNumerology/generateAstroNumerologyAnalysis', () => ({ + generateAstroNumerologyAnalysis: (...args: unknown[]) => mockGenerateAstroNumerologyAnalysis(...args), +})); + +jest.mock('@/lib/rateLimitFirestore', () => ({ + checkRateLimitWithOptionalFirestore: async ( + limiter: { check: (identifier: string) => { allowed: boolean; remaining: number; resetTime: number } }, + _logicalKey: string, + identifier: string, + ) => limiter.check(identifier), +})); + +import { POST } from '@/app/api/astro-numerology/analysis/route'; + +describe('POST /api/astro-numerology/analysis auth', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGenerateAstroNumerologyAnalysis.mockResolvedValue({ + ok: true, + data: { + sunSign: 'Aries', + lifePathNumber: 7, + nameNumber: 3, + comprehensiveAnalysis: { personalitySynthesis: 'Owned report' }, + timestamp: Date.now(), + }, + }); + }); + + it('rejects missing Authorization without calling Groq', async () => { + const req = new NextRequest('http://localhost/api/astro-numerology/analysis', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: 'victim-uid', + birthDate: '1990-01-15', + fullName: 'Victim', + sunSign: 'Aries', + }), + }); + + const res = await POST(req); + expect(res.status).toBe(401); + expect(mockGenerateAstroNumerologyAnalysis).not.toHaveBeenCalled(); + expect(mockVerifyIdToken).not.toHaveBeenCalled(); + }); + + it('rejects invalid token without calling Groq', async () => { + mockVerifyIdToken.mockRejectedValueOnce(new Error('bad token')); + const req = new NextRequest('http://localhost/api/astro-numerology/analysis', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer bad', + }, + body: JSON.stringify({ + userId: 'victim-uid', + birthDate: '1990-01-15', + fullName: 'Victim', + sunSign: 'Aries', + }), + }); + + const res = await POST(req); + expect(res.status).toBe(401); + expect(mockGenerateAstroNumerologyAnalysis).not.toHaveBeenCalled(); + }); + + it('rejects mismatched userId without reading victim cache', async () => { + mockVerifyIdToken.mockResolvedValueOnce({ uid: 'attacker', email: 'a@b.c' }); + const req = new NextRequest('http://localhost/api/astro-numerology/analysis', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer good', + }, + body: JSON.stringify({ + userId: 'victim-uid', + birthDate: '1990-01-15', + fullName: 'Victim', + sunSign: 'Aries', + }), + }); + + const res = await POST(req); + expect(res.status).toBe(403); + expect(mockGenerateAstroNumerologyAnalysis).not.toHaveBeenCalled(); + }); + + it('allows owned auth and generates with the authenticated uid', async () => { + mockVerifyIdToken.mockResolvedValueOnce({ uid: 'user-1', email: 'a@b.c' }); + const req = new NextRequest('http://localhost/api/astro-numerology/analysis', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer good', + }, + body: JSON.stringify({ + userId: 'user-1', + birthDate: '1990-01-15', + fullName: 'Seeker', + sunSign: 'Aries', + }), + }); + + const res = await POST(req); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.success).toBe(true); + expect(mockGenerateAstroNumerologyAnalysis).toHaveBeenCalledWith({ + userId: 'user-1', + birthDate: '1990-01-15', + fullName: 'Seeker', + sunSign: 'Aries', + useCache: true, + }); + }); +}); diff --git a/tests/integration/vedic-astro-numerology-analysis-auth.test.ts b/tests/integration/vedic-astro-numerology-analysis-auth.test.ts new file mode 100644 index 00000000..84047b3b --- /dev/null +++ b/tests/integration/vedic-astro-numerology-analysis-auth.test.ts @@ -0,0 +1,161 @@ +/** + * Vedic astro-numerology analysis must not be an unauthenticated paid proxy (Groq) + * or an Admin-cache IDOR on users/{userId}/vedicAstroNumerologyReports. + * @jest-environment node + */ + +import { NextRequest } from 'next/server'; + +const mockVerifyIdToken = jest.fn(); +const mockGenerateVedicAstroNumerologyAnalysis = jest.fn(); + +jest.mock('@/lib/firebase-admin', () => ({ + getAuth: () => ({ verifyIdToken: mockVerifyIdToken }), +})); + +jest.mock('@/lib/vedicAstroNumerology/generateVedicAstroNumerologyAnalysis', () => ({ + generateVedicAstroNumerologyAnalysis: (...args: unknown[]) => + mockGenerateVedicAstroNumerologyAnalysis(...args), +})); + +jest.mock('@/lib/rateLimitFirestore', () => ({ + checkRateLimitWithOptionalFirestore: async ( + limiter: { check: (identifier: string) => { allowed: boolean; remaining: number; resetTime: number } }, + _logicalKey: string, + identifier: string, + ) => limiter.check(identifier), +})); + +import { POST } from '@/app/api/vedic-astro-numerology/analysis/route'; + +const numerologyProfile = { + lifePathNumber: 3, + destinyNumber: 6, + soulNumber: 9, + nameNumber: 6, + birthDayNumber: 15, + rulingPlanet: 'Jupiter', + planetaryInfluences: { + 'Life Path': { planet: 'Jupiter', number: 3, significance: 'expansion' }, + }, + karmicLessons: [], + dashaConnections: [], +}; + +describe('POST /api/vedic-astro-numerology/analysis auth', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGenerateVedicAstroNumerologyAnalysis.mockResolvedValue({ + ok: true, + data: { + moonSign: 'Taurus', + lagnaSign: 'Aries', + sunSign: 'Pisces', + lifePathNumber: 3, + rulingPlanet: 'Jupiter', + comprehensiveAnalysis: { personalitySynthesis: 'Owned report' }, + timestamp: Date.now(), + }, + }); + }); + + it('rejects missing Authorization without calling Groq', async () => { + const req = new NextRequest('http://localhost/api/vedic-astro-numerology/analysis', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: 'victim-uid', + birthDate: '1990-01-15', + fullName: 'Victim', + moonSign: 'Taurus', + lagnaSign: 'Aries', + sunSign: 'Pisces', + numerologyProfile, + }), + }); + + const res = await POST(req); + expect(res.status).toBe(401); + expect(mockGenerateVedicAstroNumerologyAnalysis).not.toHaveBeenCalled(); + expect(mockVerifyIdToken).not.toHaveBeenCalled(); + }); + + it('rejects invalid token without calling Groq', async () => { + mockVerifyIdToken.mockRejectedValueOnce(new Error('bad token')); + const req = new NextRequest('http://localhost/api/vedic-astro-numerology/analysis', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer bad', + }, + body: JSON.stringify({ + userId: 'victim-uid', + birthDate: '1990-01-15', + fullName: 'Victim', + moonSign: 'Taurus', + numerologyProfile, + }), + }); + + const res = await POST(req); + expect(res.status).toBe(401); + expect(mockGenerateVedicAstroNumerologyAnalysis).not.toHaveBeenCalled(); + }); + + it('rejects mismatched userId without reading victim cache', async () => { + mockVerifyIdToken.mockResolvedValueOnce({ uid: 'attacker', email: 'a@b.c' }); + const req = new NextRequest('http://localhost/api/vedic-astro-numerology/analysis', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer good', + }, + body: JSON.stringify({ + userId: 'victim-uid', + birthDate: '1990-01-15', + fullName: 'Victim', + moonSign: 'Taurus', + numerologyProfile, + }), + }); + + const res = await POST(req); + expect(res.status).toBe(403); + expect(mockGenerateVedicAstroNumerologyAnalysis).not.toHaveBeenCalled(); + }); + + it('allows owned auth and generates with the authenticated uid', async () => { + mockVerifyIdToken.mockResolvedValueOnce({ uid: 'user-1', email: 'a@b.c' }); + const req = new NextRequest('http://localhost/api/vedic-astro-numerology/analysis', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer good', + }, + body: JSON.stringify({ + userId: 'user-1', + birthDate: '1990-01-15', + fullName: 'Seeker', + moonSign: 'Taurus', + lagnaSign: 'Aries', + sunSign: 'Pisces', + numerologyProfile, + }), + }); + + const res = await POST(req); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.success).toBe(true); + expect(mockGenerateVedicAstroNumerologyAnalysis).toHaveBeenCalledWith({ + userId: 'user-1', + birthDate: '1990-01-15', + fullName: 'Seeker', + moonSign: 'Taurus', + lagnaSign: 'Aries', + sunSign: 'Pisces', + numerologyProfile, + useCache: true, + }); + }); +});