This commit is contained in:
60
.github/workflows/deploy.yml
vendored
Normal file
60
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
name: Deploy Apartment API
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: |
|
||||||
|
docker build -t apartment-api:${{ github.sha }} .
|
||||||
|
docker tag apartment-api:${{ github.sha }} apartment-api:latest
|
||||||
|
|
||||||
|
- name: Deploy to server
|
||||||
|
run: |
|
||||||
|
# Copy files to deployment directory
|
||||||
|
mkdir -p /media/stephen/Storage_Linux/infrastructure/services/apartment-api
|
||||||
|
cp -r . /media/stephen/Storage_Linux/infrastructure/services/apartment-api/
|
||||||
|
|
||||||
|
# Navigate to deployment directory
|
||||||
|
cd /media/stephen/Storage_Linux/infrastructure/services/apartment-api
|
||||||
|
|
||||||
|
# Stop existing container if running
|
||||||
|
docker compose down || true
|
||||||
|
|
||||||
|
# Start new container
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Clean up old images
|
||||||
|
docker image prune -f
|
||||||
|
|
||||||
|
- name: Verify deployment
|
||||||
|
run: |
|
||||||
|
# Wait for container to start
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
# Check if container is running
|
||||||
|
docker ps | grep apartment-api
|
||||||
|
|
||||||
|
# Test health endpoint
|
||||||
|
curl -f http://localhost:3000/health || echo "API may still be starting..."
|
||||||
|
|
||||||
|
# Test API endpoint
|
||||||
|
curl -f http://localhost:3000/api/daily-summary || echo "API may still be loading data..."
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.DS_Store
|
||||||
31
Dockerfile
Normal file
31
Dockerfile
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -g 1001 -S nodejs
|
||||||
|
RUN adduser -S nodejs -u 1001
|
||||||
|
|
||||||
|
# Change ownership of the app directory
|
||||||
|
RUN chown -R nodejs:nodejs /app
|
||||||
|
USER nodejs
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD node -e "require('http').get('http://localhost:8080/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["npm", "start"]
|
||||||
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
services:
|
||||||
|
apartment-api:
|
||||||
|
build: .
|
||||||
|
container_name: apartment-api
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=8080 # Changed from 3000 to 8080
|
||||||
|
- MONGO_URI=mongodb://admin:password123@mongodb:27017
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.apartment-api.rule=Host(`apartments.maverickapplications.com`) && PathPrefix(`/api`)"
|
||||||
|
- "traefik.http.routers.apartment-api.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.apartment-api.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.apartment-api.loadbalancer.server.port=8080" # Updated to match
|
||||||
|
- "traefik.http.middlewares.apartment-api-stripprefix.stripprefix.prefixes=/api"
|
||||||
|
- "traefik.http.routers.apartment-api.middlewares=apartment-api-stripprefix"
|
||||||
|
networks:
|
||||||
|
- traefik
|
||||||
|
- mongodb_mongodb_network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik:
|
||||||
|
external: true
|
||||||
|
mongodb_mongodb_network:
|
||||||
|
external: true
|
||||||
1399
package-lock.json
generated
Normal file
1399
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
package.json
Normal file
31
package.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "apartment-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "API backend for Country Club Towers & Gardens apartment dashboard",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"apartments",
|
||||||
|
"api",
|
||||||
|
"real-estate",
|
||||||
|
"monitoring"
|
||||||
|
],
|
||||||
|
"author": "Stephen",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"mongodb": "^6.3.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express-rate-limit": "^7.1.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
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