Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions models/BudgetVariance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
const mongoose = require('mongoose');

/**
* BudgetVariance Model
* Stores detailed variance analysis results for budget monitoring
*/
const varianceItemSchema = new mongoose.Schema({
category: String,
subcategory: String,
budgetedAmount: {
type: Number,
required: true
},
actualAmount: {
type: Number,
required: true
},
variance: {
type: Number,
required: true
},
variancePercentage: {
type: Number,
required: true
},
varianceType: {
type: String,
enum: ['favorable', 'unfavorable', 'neutral'],
required: true
},
anomalyScore: {
type: Number,
default: 0,
min: 0,
max: 100
},
isAnomaly: {
type: Boolean,
default: false
},
transactionCount: {
type: Number,
default: 0
}
}, { _id: false });

const budgetVarianceSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true
},
budgetId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Budget',
required: true,
index: true
},
budgetName: String,
analysisDate: {
type: Date,
required: true,
default: Date.now
},
period: {
startDate: {
type: Date,
required: true
},
endDate: {
type: Date,
required: true
},
periodType: {
type: String,
enum: ['daily', 'weekly', 'monthly', 'quarterly', 'yearly'],
default: 'monthly'
}
},
items: [varianceItemSchema],
summary: {
totalBudgeted: {
type: Number,
default: 0
},
totalActual: {
type: Number,
default: 0
},
totalVariance: {
type: Number,
default: 0
},
variancePercentage: {
type: Number,
default: 0
},
favorableVariances: {
type: Number,
default: 0
},
unfavorableVariances: {
type: Number,
default: 0
},
anomaliesDetected: {
type: Number,
default: 0
},
utilizationRate: {
type: Number,
default: 0
}
},
alerts: [{
severity: {
type: String,
enum: ['low', 'medium', 'high', 'critical']
},
category: String,
message: String,
recommendedAction: String,
createdAt: {
type: Date,
default: Date.now
}
}],
trends: {
isIncreasing: Boolean,
trendPercentage: Number,
projectedOverrun: Number,
daysUntilOverrun: Number
},
status: {
type: String,
enum: ['on_track', 'warning', 'critical', 'exceeded'],
default: 'on_track'
}
}, {
timestamps: true
});

// Pre-save hook to calculate summary
budgetVarianceSchema.pre('save', function (next) {
this.summary.totalBudgeted = this.items.reduce((sum, i) => sum + i.budgetedAmount, 0);
this.summary.totalActual = this.items.reduce((sum, i) => sum + i.actualAmount, 0);
this.summary.totalVariance = this.summary.totalActual - this.summary.totalBudgeted;

if (this.summary.totalBudgeted > 0) {
this.summary.variancePercentage = (this.summary.totalVariance / this.summary.totalBudgeted) * 100;
this.summary.utilizationRate = (this.summary.totalActual / this.summary.totalBudgeted) * 100;
}

this.summary.favorableVariances = this.items.filter(i => i.varianceType === 'favorable').length;
this.summary.unfavorableVariances = this.items.filter(i => i.varianceType === 'unfavorable').length;
this.summary.anomaliesDetected = this.items.filter(i => i.isAnomaly).length;

// Determine status
if (this.summary.utilizationRate >= 100) {
this.status = 'exceeded';
} else if (this.summary.utilizationRate >= 90) {
this.status = 'critical';
} else if (this.summary.utilizationRate >= 75) {
this.status = 'warning';
} else {
this.status = 'on_track';
}

next();
});

// Indexes
budgetVarianceSchema.index({ userId: 1, analysisDate: -1 });
budgetVarianceSchema.index({ budgetId: 1, 'period.startDate': 1 });
budgetVarianceSchema.index({ status: 1 });

module.exports = mongoose.model('BudgetVariance', budgetVarianceSchema);
184 changes: 184 additions & 0 deletions models/SpendForecast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
const mongoose = require('mongoose');

/**
* SpendForecast Model
* Stores predictive spend projections with confidence intervals
*/
const forecastDataPointSchema = new mongoose.Schema({
date: {
type: Date,
required: true
},
predictedAmount: {
type: Number,
required: true
},
lowerBound: {
type: Number,
required: true
},
upperBound: {
type: Number,
required: true
},
confidence: {
type: Number,
default: 95,
min: 0,
max: 100
},
actualAmount: Number,
variance: Number
}, { _id: false });

const spendForecastSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true
},
budgetId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Budget',
index: true
},
category: String,
forecastId: {
type: String,
unique: true,
required: true
},
forecastDate: {
type: Date,
required: true,
default: Date.now
},
forecastPeriod: {
startDate: {
type: Date,
required: true
},
endDate: {
type: Date,
required: true
},
periodType: {
type: String,
enum: ['daily', 'weekly', 'monthly', 'quarterly'],
default: 'monthly'
}
},
historicalPeriod: {
startDate: Date,
endDate: Date,
dataPoints: Number
},
forecastMethod: {
type: String,
enum: ['linear', 'exponential', 'seasonal', 'moving_average', 'ensemble'],
required: true
},
dataPoints: [forecastDataPointSchema],
summary: {
totalPredicted: {
type: Number,
default: 0
},
averageDaily: {
type: Number,
default: 0
},
peakPredicted: {
type: Number,
default: 0
},
peakDate: Date,
trend: {
type: String,
enum: ['increasing', 'decreasing', 'stable']
},
trendStrength: {
type: Number,
min: 0,
max: 1
},
seasonalityDetected: {
type: Boolean,
default: false
},
seasonalPattern: String
},
accuracy: {
mape: Number, // Mean Absolute Percentage Error
rmse: Number, // Root Mean Square Error
mae: Number, // Mean Absolute Error
r2Score: Number // R-squared score
},
alerts: [{
type: {
type: String,
enum: ['budget_overrun', 'unusual_spike', 'trend_change']
},
severity: {
type: String,
enum: ['low', 'medium', 'high']
},
message: String,
date: Date,
amount: Number
}],
status: {
type: String,
enum: ['active', 'expired', 'superseded'],
default: 'active'
}
}, {
timestamps: true
});

// Pre-save hook to calculate summary
spendForecastSchema.pre('save', function (next) {
if (this.dataPoints.length > 0) {
this.summary.totalPredicted = this.dataPoints.reduce((sum, dp) => sum + dp.predictedAmount, 0);
this.summary.averageDaily = this.summary.totalPredicted / this.dataPoints.length;

// Find peak
const peak = this.dataPoints.reduce((max, dp) =>
dp.predictedAmount > max.predictedAmount ? dp : max
);
this.summary.peakPredicted = peak.predictedAmount;
this.summary.peakDate = peak.date;

// Determine trend
if (this.dataPoints.length >= 3) {
const firstThird = this.dataPoints.slice(0, Math.floor(this.dataPoints.length / 3));
const lastThird = this.dataPoints.slice(-Math.floor(this.dataPoints.length / 3));

const firstAvg = firstThird.reduce((sum, dp) => sum + dp.predictedAmount, 0) / firstThird.length;
const lastAvg = lastThird.reduce((sum, dp) => sum + dp.predictedAmount, 0) / lastThird.length;

const change = ((lastAvg - firstAvg) / firstAvg) * 100;

if (change > 5) {
this.summary.trend = 'increasing';
this.summary.trendStrength = Math.min(change / 100, 1);
} else if (change < -5) {
this.summary.trend = 'decreasing';
this.summary.trendStrength = Math.min(Math.abs(change) / 100, 1);
} else {
this.summary.trend = 'stable';
this.summary.trendStrength = 0;
}
}
}

next();
});

// Indexes
spendForecastSchema.index({ userId: 1, forecastDate: -1 });
spendForecastSchema.index({ budgetId: 1, status: 1 });
spendForecastSchema.index({ category: 1, status: 1 });

module.exports = mongoose.model('SpendForecast', spendForecastSchema);
Loading