Spaces:
Running
Running
| const express = require('express'); | |
| const cors = require('cors'); | |
| const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); | |
| const { calculatePlanetaryLines, getTimezone } = require('./astrology-engine'); | |
| const { geocodeLocation } = require('./geocode'); | |
| const PDFDocument = require('pdfkit'); | |
| const path = require('path'); | |
| const app = express(); | |
| app.use(cors()); | |
| app.use(express.json()); | |
| app.use(express.static('.')); | |
| // Database simulation (use PostgreSQL in production) | |
| const readings = new Map(); | |
| const payments = new Map(); | |
| // Calculate astrocartography using real Swiss Ephemeris calculations | |
| app.post('/api/calculate-astrocartography', async (req, res) => { | |
| try { | |
| const { date, time, location, email } = req.body; | |
| // 1. Geocode birth location | |
| const geoData = await geocodeLocation(location); | |
| if (!geoData || !geoData.lat || !geoData.lng) { | |
| throw new Error('Failed to geocode location'); | |
| } | |
| // 2. Create proper UTC date from birth data | |
| // Parse date and time | |
| const [year, month, day] = date.split('-').map(Number); | |
| const [hours, minutes] = time.split(':').map(Number); | |
| // Create date in local timezone then convert to UTC equivalent | |
| // For precise calculations, we need the exact moment in UTC | |
| const birthDate = new Date(Date.UTC(year, month - 1, day, hours - geoData.offset, minutes)); | |
| // 3. Calculate planetary lines using Swiss Ephemeris | |
| console.log(`Calculating chart for ${email} - ${date} ${time} at ${geoData.lat},${geoData.lng}`); | |
| const planetaryData = await calculatePlanetaryLines({ | |
| date: birthDate, | |
| lat: geoData.lat, | |
| lng: geoData.lng, | |
| timezone: geoData.timezone | |
| }); | |
| // 4. Generate summary recommendations | |
| const recommendations = generateRecommendations(planetaryData, geoData); | |
| // 5. Save reading to database | |
| const readingId = Date.now().toString(36) + Math.random().toString(36).substr(2); | |
| readings.set(readingId, { | |
| email, | |
| date: new Date(), | |
| birthData: { date, time, location, lat: geoData.lat, lng: geoData.lng }, | |
| planetaryData, | |
| recommendations | |
| }); | |
| // 6. Format response for frontend | |
| const formattedLines = planetaryData.map(planet => ({ | |
| planet: planet.planet, | |
| color: planet.color, | |
| longitude: planet.longitude, | |
| latitude: planet.latitude, | |
| retrograde: planet.retrograde, | |
| strongestAngle: planet.strongestAngle, | |
| closestPoint: planet.coordinates, | |
| meaning: planet.meaning, | |
| nearbyCities: planet.nearbyCities, | |
| interpretations: planet.interpretations, | |
| // Include line coordinates for mapping | |
| lines: { | |
| MC: planet.lines.MC.slice(0, 35), // Sample every 5 degrees from 85 to -85 | |
| IC: planet.lines.IC.slice(0, 35), | |
| ASC: planet.lines.ASC, // Variable length based on visibility | |
| DSC: planet.lines.DSC | |
| } | |
| })); | |
| res.json({ | |
| success: true, | |
| reportId: readingId, | |
| birthLocation: { | |
| name: geoData.displayName, | |
| lat: geoData.lat, | |
| lng: geoData.lng, | |
| timezone: geoData.timezone | |
| }, | |
| planetaryLines: formattedLines, | |
| recommendations, | |
| calculationMethod: 'Swiss Ephemeris 2.10', | |
| ephemerisDate: new Date().toISOString() | |
| }); | |
| } catch (error) { | |
| console.error('Calculation error:', error); | |
| res.status(500).json({ | |
| success: false, | |
| error: 'Calculation failed: ' + error.message | |
| }); | |
| } | |
| }); | |
| // Stripe payment intent creation | |
| app.post('/api/create-payment-intent', async (req, res) => { | |
| try { | |
| const { amount, email, readingId } = req.body; | |
| const paymentIntent = await stripe.paymentIntents.create({ | |
| amount: amount, // amount in cents (2900 for $29) | |
| currency: 'usd', | |
| receipt_email: email, | |
| metadata: { | |
| service: 'astrocartography', | |
| readingId: readingId || 'pending' | |
| } | |
| }); | |
| // Store payment intent mapping | |
| payments.set(paymentIntent.id, { | |
| email, | |
| readingId: readingId || null, | |
| status: 'pending' | |
| }); | |
| res.json({ | |
| success: true, | |
| clientSecret: paymentIntent.client_secret | |
| }); | |
| } catch (error) { | |
| console.error('Payment error:', error); | |
| res.status(500).json({ | |
| success: false, | |
| error: error.message | |
| }); | |
| } | |
| }); | |
| // Stripe webhook for payment confirmation | |
| app.post('/api/webhook', express.raw({type: 'application/json'}), async (req, res) => { | |
| const sig = req.headers['stripe-signature']; | |
| let event; | |
| try { | |
| event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET); | |
| } catch (err) { | |
| return res.status(400).send(`Webhook Error: ${err.message}`); | |
| } | |
| if (event.type === 'payment_intent.succeeded') { | |
| const paymentIntent = event.data.object; | |
| const paymentRecord = payments.get(paymentIntent.id); | |
| if (paymentRecord && paymentRecord.readingId) { | |
| // Update reading as paid | |
| const reading = readings.get(paymentRecord.readingId); | |
| if (reading) { | |
| reading.paid = true; | |
| reading.paymentDate = new Date(); | |
| } | |
| } | |
| } | |
| res.json({received: true}); | |
| }); | |
| // Generate and download PDF report | |
| app.get('/api/download-report/:id', async (req, res) => { | |
| const reading = readings.get(req.params.id); | |
| if (!reading) return res.status(404).send('Reading not found'); | |
| if (!reading.paid) return res.status(403).send('Payment required'); | |
| const doc = new PDFDocument({ margin: 50 }); | |
| res.setHeader('Content-Type', 'application/pdf'); | |
| res.setHeader('Content-Disposition', `attachment; filename=cosmic-map-${req.params.id}.pdf`); | |
| doc.pipe(res); | |
| // Header | |
| doc.fontSize(28).fillColor('#1e1b4b').text('My Cosmic Map', 50, 50); | |
| doc.fontSize(16).fillColor('#666').text('Personal Astrocartography Report', 50, 85); | |
| doc.moveTo(50, 110).lineTo(550, 110).stroke('#8b5cf6'); | |
| // Meta info | |
| doc.fontSize(12).fillColor('#333'); | |
| doc.text(`Generated: ${reading.date.toLocaleDateString()}`, 50, 130); | |
| doc.text(`Birth Location: ${reading.birthData.location}`, 50, 145); | |
| doc.text(`Birth Time: ${reading.birthData.date} ${reading.birthData.time}`, 50, 160); | |
| doc.text(`Calculation System: Swiss Ephemeris 2.10`, 50, 175); | |
| let y = 210; | |
| // Planetary Lines | |
| reading.planetaryData.forEach((planet, index) => { | |
| if (y > 700) { | |
| doc.addPage(); | |
| y = 50; | |
| } | |
| // Planet header with color indicator | |
| doc.circle(60, y + 8, 5).fill(planet.color || '#666'); | |
| doc.fontSize(18).fillColor('#1e1b4b').text(`${planet.planet} Line`, 75, y); | |
| doc.fontSize(10).fillColor('#666').text(`Ecliptic: ${planet.longitude}°${planet.retrograde ? ' (R)' : ''}`, 200, y + 5); | |
| y += 30; | |
| // Strongest angle | |
| doc.fontSize(12).fillColor('#8b5cf6').text(`Strongest Influence: ${planet.strongestAngle} Angle`, 75, y); | |
| y += 20; | |
| // Nearby cities | |
| if (planet.nearbyCities && planet.nearbyCities.length > 0) { | |
| doc.fontSize(11).fillColor('#333').text('Key Locations:', 75, y); | |
| y += 15; | |
| planet.nearbyCities.forEach(city => { | |
| doc.fontSize(10).fillColor('#666').text(`• ${city.name} (${city.angle} line, ${city.distance}° away)`, 90, y); | |
| y += 12; | |
| }); | |
| y += 10; | |
| } | |
| // Interpretation for strongest angle | |
| const interp = planet.interpretations[planet.strongestAngle]; | |
| doc.fontSize(10).fillColor('#333').text(interp, 75, y, { | |
| width: 450, | |
| align: 'left' | |
| }); | |
| y += 60; | |
| // Separator | |
| if (index < reading.planetaryData.length - 1) { | |
| doc.moveTo(50, y - 10).lineTo(550, y - 10).stroke('#eee'); | |
| } | |
| }); | |
| // Recommendations | |
| if (y > 650) { | |
| doc.addPage(); | |
| y = 50; | |
| } | |
| doc.fontSize(18).fillColor('#1e1b4b').text('Summary Recommendations', 50, y); | |
| y += 30; | |
| const recs = reading.recommendations; | |
| doc.fontSize(12).fillColor('#333'); | |
| doc.text('Career & Public Life:', 50, y); | |
| doc.fontSize(10).fillColor('#666').text(recs.bestForCareer, 70, y + 15, { width: 480 }); | |
| y += 50; | |
| doc.fontSize(12).fillColor('#333').text('Love & Relationships:', 50, y); | |
| doc.fontSize(10).fillColor('#666').text(recs.bestForLove, 70, y + 15, { width: 480 }); | |
| y += 50; | |
| doc.fontSize(12).fillColor('#333').text('Energy & Vitality:', 50, y); | |
| doc.fontSize(10).fillColor('#666').text(recs.bestForEnergy, 70, y + 15, { width: 480 }); | |
| // Disclaimer | |
| doc.addPage(); | |
| doc.fontSize(10).fillColor('#999').text( | |
| 'Disclaimer: This astrocartography report is for entertainment and self-exploration purposes only. ' + | |
| 'Astrological insights should not replace professional legal, medical, or financial advice. ' + | |
| 'Relocation decisions should consider practical factors alongside astrological indications.', | |
| 50, 50, { width: 500, align: 'justify' } | |
| ); | |
| doc.end(); | |
| }); | |
| // Get reading status | |
| app.get('/api/reading/:id', (req, res) => { | |
| const reading = readings.get(req.params.id); | |
| if (!reading) return res.status(404).json({ error: 'Not found' }); | |
| res.json({ | |
| paid: reading.paid || false, | |
| created: reading.date, | |
| planetaryData: reading.planetaryData ? true : false | |
| }); | |
| }); | |
| function generateRecommendations(planetaryData, geoData) { | |
| // Find best planets for each life area based on angles | |
| const sunData = planetaryData.find(p => p.planet === 'Sun'); | |
| const venusData = planetaryData.find(p => p.planet === 'Venus'); | |
| const marsData = planetaryData.find(p => p.planet === 'Mars'); | |
| const jupiterData = planetaryData.find(p => p.planet === 'Jupiter'); | |
| const saturnData = planetaryData.find(p => p.planet === 'Saturn'); | |
| const careerPlanet = jupiterData || sunData; | |
| const lovePlanet = venusData; | |
| const energyPlanet = marsData || sunData; | |
| // Find best cities for each | |
| const getBestCity = (planetData) => { | |
| if (!planetData || !planetData.nearbyCities || planetData.nearbyCities.length === 0) { | |
| return null; | |
| } | |
| return planetData.nearbyCities[0]; | |
| }; | |
| const careerCity = getBestCity(careerPlanet); | |
| const loveCity = getBestCity(lovePlanet); | |
| const energyCity = getBestCity(energyPlanet); | |
| return { | |
| bestForCareer: careerCity ? | |
| `Your ${careerPlanet.planet} line (${careerPlanet.strongestAngle}) runs near ${careerCity.name}. ` + | |
| `${careerPlanet.interpretations[careerPlanet.strongestAngle]}` : | |
| 'Major metropolitan areas align with your career potential. Look for cities where you feel naturally confident.', | |
| bestForLove: loveCity ? | |
| `Your Venus line (${venusData.strongestAngle}) influences ${loveCity.name}. ` + | |
| `${venusData.interpretations[venusData.strongestAngle]}` : | |
| 'Coastal and culturally rich cities may enhance your romantic connections and artistic expression.', | |
| bestForEnergy: energyCity ? | |
| `Your ${energyPlanet.planet} line (${energyPlanet.strongestAngle}) activates ${energyCity.name}. ` + | |
| `${energyPlanet.interpretations[energyPlanet.strongestAngle]}` : | |
| 'Locations with active lifestyles and outdoor recreation align with your Mars energy.', | |
| cautionaryNote: saturnData && saturnData.nearbyCities.length > 0 ? | |
| `Note: Your Saturn line (${saturnData.strongestAngle}) passes near ${saturnData.nearbyCities[0].name}. ` + | |
| 'This brings lessons and maturity but may feel heavy initially.' : | |
| null | |
| }; | |
| } | |
| const PORT = process.env.PORT || 3000; | |
| app.listen(PORT, () => { | |
| console.log(`🌟 Cosmic Map Server running on port ${PORT}`); | |
| console.log(`📡 Swiss Ephemeris path: ${path.join(__dirname, 'ephemeris')}`); | |
| }); |