This commit is contained in:
504
server.js
Normal file
504
server.js
Normal file
@ -0,0 +1,504 @@
|
||||
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(`📊 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);
|
||||
});
|
||||
Reference in New Issue
Block a user