Initial commit
All checks were successful
Deploy Apartment API / deploy (push) Successful in 9m48s

This commit is contained in:
2025-07-15 17:51:26 -06:00
commit 9153a2faf8
7 changed files with 2056 additions and 0 deletions

504
server.js Normal file
View 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);
});