All checks were successful
Deploy Apartment API / deploy (push) Successful in 9m48s
504 lines
14 KiB
JavaScript
504 lines
14 KiB
JavaScript
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);
|
|
}); |