const express = require('express'); const { MongoClient } = require('mongodb'); const cors = require('cors'); const rateLimit = require('express-rate-limit'); const app = express(); const PORT = process.env.PORT || 3000; // Configuration const MONGO_URI = process.env.MONGO_URI || "mongodb://admin:password123@localhost:27017"; const DB_NAME = "apartments"; const UNITS_COLLECTION = "units_migration_test"; const PRICES_COLLECTION = "unit_prices_migration_test"; const DAILY_SUMMARIES_COLLECTION = "daily_summaries"; console.log(`🔗 Connecting to MongoDB: ${MONGO_URI.includes('localhost') ? 'Local MongoDB' : 'Atlas MongoDB'}`); // Middleware app.use(cors()); app.use(express.json()); // Rate limiting const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100 // limit each IP to 100 requests per windowMs }); app.use(limiter); // MongoDB connection let db; let client; async function connectToMongoDB() { try { client = new MongoClient(MONGO_URI); await client.connect(); db = client.db(DB_NAME); console.log('✅ Successfully connected to MongoDB'); } catch (error) { console.error('❌ Failed to connect to MongoDB:', error); process.exit(1); } } // Helper function to get today's date const getTodayDate = () => new Date().toISOString().split('T')[0]; // Helper function to get yesterday's date const getYesterdayDate = () => { const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); return yesterday.toISOString().split('T')[0]; }; // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), service: 'apartment-api' }); }); // Get daily summary (matches your daily_summaries collection) app.get('/api/daily-summary', async (req, res) => { try { const today = getTodayDate(); // Try to get today's summary first let summary = await db.collection(DAILY_SUMMARIES_COLLECTION) .findOne({ date: today }); // If no summary for today, get the most recent one if (!summary) { summary = await db.collection(DAILY_SUMMARIES_COLLECTION) .findOne({}, { sort: { date: -1 } }); } // If still no summary, create a basic one if (!summary) { // Get basic counts for today const todayPriceCount = await db.collection(PRICES_COLLECTION) .countDocuments({ date_checked: today }); summary = { date: today, newUnits: 0, rentedUnits: 0, staleUnits: 0, netChange: 0, turnoverRate: 0, totalAvailable: todayPriceCount }; } res.json(summary); } catch (error) { console.error('Error fetching daily summary:', error); res.status(500).json({ error: 'Failed to fetch daily summary' }); } }); // Get price history (last 15 days of average prices) app.get('/api/price-history', async (req, res) => { try { const days = parseInt(req.query.days) || 15; // Generate date range const dates = []; for (let i = days - 1; i >= 0; i--) { const date = new Date(); date.setDate(date.getDate() - i); dates.push(date.toISOString().split('T')[0]); } const priceHistory = []; for (const date of dates) { // Get all prices for this date (excluding furnished units) const dailyPrices = await db.collection(PRICES_COLLECTION).aggregate([ { $match: { date_checked: date } }, { $lookup: { from: UNITS_COLLECTION, localField: "unit_code", foreignField: "unit_code", as: "unit_info" } }, { $match: { "unit_info.plan_name": { $not: { $regex: "Furnished", $options: "i" } } } }, { $group: { _id: null, avgPrice: { $avg: "$price" }, unitCount: { $sum: 1 } } } ]).toArray(); priceHistory.push({ date: date, avgPrice: dailyPrices.length > 0 ? Math.round(dailyPrices[0].avgPrice) : null, unitCount: dailyPrices.length > 0 ? dailyPrices[0].unitCount : 0 }); } res.json(priceHistory); } catch (error) { console.error('Error fetching price history:', error); res.status(500).json({ error: 'Failed to fetch price history' }); } }); // Get available units with current prices app.get('/api/available-units', async (req, res) => { try { const today = getTodayDate(); // Get units that have prices today (excluding furnished units) const availableUnits = await db.collection(UNITS_COLLECTION).aggregate([ { $match: { available: true, plan_name: { $not: { $regex: "Furnished", $options: "i" } } } }, { $lookup: { from: PRICES_COLLECTION, localField: "unit_code", foreignField: "unit_code", as: "price_info" } }, { $match: { "price_info.date_checked": today } }, { $addFields: { currentPrice: { $arrayElemAt: [ { $filter: { input: "$price_info", cond: { $eq: ["$$this.date_checked", today] } } }, 0 ] } } }, { $lookup: { from: PRICES_COLLECTION, localField: "unit_code", foreignField: "unit_code", as: "all_prices" } }, { $addFields: { priceStats: { $let: { vars: { prices: { $map: { input: "$all_prices", as: "p", in: "$$p.price" } } }, in: { minPrice: { $min: "$$prices" }, maxPrice: { $max: "$$prices" }, priceHistory: { $slice: [ { $sortArray: { input: "$all_prices", sortBy: { date_checked: -1 } } }, 5 ] } } } } } }, { $project: { unitCode: "$unit_code", planName: "$plan_name", bedCount: "$bed_count", bathCount: "$bath_count", area: "$area", community: "$community", available: "$available", currentPrice: "$currentPrice.price", minPrice: "$priceStats.minPrice", maxPrice: "$priceStats.maxPrice", daysAvailable: { $dateDiff: { startDate: { $dateFromString: { dateString: { $arrayElemAt: ["$priceStats.priceHistory.date_checked", -1] } } }, endDate: { $dateFromString: { dateString: today } }, unit: "day" } }, priceHistory: { $map: { input: "$priceStats.priceHistory", as: "ph", in: { date: "$$ph.date_checked", price: "$$ph.price" } } } } }, { $sort: { planName: 1, unitCode: 1 } } ]).toArray(); res.json(availableUnits); } catch (error) { console.error('Error fetching available units:', error); res.status(500).json({ error: 'Failed to fetch available units' }); } }); // Get recent activity (new units, rented units, price changes) app.get('/api/recent-activity', async (req, res) => { try { const today = getTodayDate(); const yesterday = getYesterdayDate(); // Get today's summary for new/rented units const todaySummary = await db.collection(DAILY_SUMMARIES_COLLECTION) .findOne({ date: today }); const activity = []; if (todaySummary) { // Add new units if (todaySummary.new_units && todaySummary.new_units.length > 0) { for (const unitCode of todaySummary.new_units.slice(0, 5)) { // Limit to 5 most recent const unitInfo = await db.collection(UNITS_COLLECTION) .findOne({ unit_code: unitCode }); const priceInfo = await db.collection(PRICES_COLLECTION) .findOne({ unit_code: unitCode, date_checked: today }); if (unitInfo) { activity.push({ type: 'new', unitCode: unitCode, planName: unitInfo.plan_name, price: priceInfo ? priceInfo.price : null, date: today, bedCount: unitInfo.bed_count, bathCount: unitInfo.bath_count }); } } } // Add rented units if (todaySummary.rented_units && todaySummary.rented_units.length > 0) { for (const unitCode of todaySummary.rented_units.slice(0, 5)) { // Limit to 5 most recent const unitInfo = await db.collection(UNITS_COLLECTION) .findOne({ unit_code: unitCode }); const priceInfo = await db.collection(PRICES_COLLECTION) .findOne({ unit_code: unitCode, date_checked: yesterday }); if (unitInfo) { activity.push({ type: 'rented', unitCode: unitCode, planName: unitInfo.plan_name, price: priceInfo ? priceInfo.price : null, date: today, bedCount: unitInfo.bed_count, bathCount: unitInfo.bath_count }); } } } } // Get recent price changes (units with different prices yesterday vs today) const priceChanges = await db.collection(PRICES_COLLECTION).aggregate([ { $match: { date_checked: { $in: [today, yesterday] } } }, { $group: { _id: "$unit_code", prices: { $push: { date: "$date_checked", price: "$price" } } } }, { $match: { "prices.1": { $exists: true } // Must have at least 2 price records } }, { $addFields: { todayPrice: { $arrayElemAt: [ { $map: { input: { $filter: { input: "$prices", cond: { $eq: ["$$this.date", today] } } }, as: "p", in: "$$p.price" } }, 0 ] }, yesterdayPrice: { $arrayElemAt: [ { $map: { input: { $filter: { input: "$prices", cond: { $eq: ["$$this.date", yesterday] } } }, as: "p", in: "$$p.price" } }, 0 ] } } }, { $match: { $expr: { $ne: ["$todayPrice", "$yesterdayPrice"] } } }, { $limit: 3 } // Limit to 3 price changes ]).toArray(); // Add price changes to activity for (const change of priceChanges) { const unitInfo = await db.collection(UNITS_COLLECTION) .findOne({ unit_code: change._id }); if (unitInfo && change.todayPrice < change.yesterdayPrice) { // Only show price drops activity.push({ type: 'price_drop', unitCode: change._id, planName: unitInfo.plan_name, oldPrice: change.yesterdayPrice, newPrice: change.todayPrice, date: today, bedCount: unitInfo.bed_count, bathCount: unitInfo.bath_count }); } } // Sort by most recent first activity.sort((a, b) => new Date(b.date) - new Date(a.date)); res.json(activity.slice(0, 10)); // Return max 10 activities } catch (error) { console.error('Error fetching recent activity:', error); res.status(500).json({ error: 'Failed to fetch recent activity' }); } }); // Get plan statistics app.get('/api/plan-stats', async (req, res) => { try { const today = getTodayDate(); const planStats = await db.collection(UNITS_COLLECTION).aggregate([ { $match: { available: true, plan_name: { $not: { $regex: "Furnished", $options: "i" } } } }, { $lookup: { from: PRICES_COLLECTION, localField: "unit_code", foreignField: "unit_code", as: "price_info" } }, { $match: { "price_info.date_checked": today } }, { $addFields: { currentPrice: { $arrayElemAt: [ { $map: { input: { $filter: { input: "$price_info", cond: { $eq: ["$$this.date_checked", today] } } }, as: "p", in: "$$p.price" } }, 0 ] } } }, { $group: { _id: "$plan_name", count: { $sum: 1 }, avgPrice: { $avg: "$currentPrice" }, avgArea: { $avg: "$area" } } }, { $project: { name: "$_id", count: 1, avgPrice: { $round: ["$avgPrice", 0] }, avgArea: { $round: ["$avgArea", 0] }, _id: 0 } }, { $sort: { count: -1 } } ]).toArray(); res.json(planStats); } catch (error) { console.error('Error fetching plan stats:', error); res.status(500).json({ error: 'Failed to fetch plan stats' }); } }); // Error handling middleware app.use((error, req, res, next) => { console.error('Unhandled error:', error); res.status(500).json({ error: 'Internal server error' }); }); // 404 handler app.use((req, res) => { res.status(404).json({ error: 'Endpoint not found' }); }); // Start server async function startServer() { await connectToMongoDB(); app.listen(PORT, '0.0.0.0', () => { console.log(`🚀 Apartment API server running on port ${PORT}`); console.log(`📊 Server listening on 0.0.0.0:${PORT} (accessible from Docker networks)`); console.log(`📊 Available endpoints:`); console.log(` GET /health - Health check`); console.log(` GET /api/daily-summary - Daily apartment summary`); console.log(` GET /api/price-history - Price trends (15 days)`); console.log(` GET /api/available-units - Currently available units`); console.log(` GET /api/recent-activity - Recent market activity`); console.log(` GET /api/plan-stats - Statistics by floor plan`); }); } // Graceful shutdown process.on('SIGINT', async () => { console.log('🔄 Shutting down gracefully...'); if (client) { await client.close(); console.log('✅ MongoDB connection closed'); } process.exit(0); }); process.on('SIGTERM', async () => { console.log('🔄 Received SIGTERM, shutting down gracefully...'); if (client) { await client.close(); console.log('✅ MongoDB connection closed'); } process.exit(0); }); startServer().catch(error => { console.error('Failed to start server:', error); process.exit(1); });