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
18 changes: 18 additions & 0 deletions src/controllers/ai.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { aiService } from '../services/ai.service.js';
import { ApiResponse } from '../utils/apiResponse.js';

export const handleAiChat = async (req, res, next) => {
try {
const userId = req.user?.id || 'user_123';
const { prompt } = req.body;

if (!prompt || !prompt.trim()) {
return ApiResponse.error(res, 'Prompt text is required for AI spend inquiry.', 400);
}

const aiResult = await aiService.processChatQuery(userId, prompt.trim());
return ApiResponse.success(res, 'AI Spend Q&A response generated successfully', aiResult);
} catch (error) {
next(error);
}
};
34 changes: 34 additions & 0 deletions src/controllers/category.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { categorizationService } from '../services/categorization.service.js';

export const categoryController = {
recategorizeTransaction: async (req, res) => {
try {
const userId = req.user.id || req.user._id;
const { transactionId, newCategory, applyToFuture } = req.body;

if (!transactionId || !newCategory) {
return res.status(400).json({
success: false,
message: 'transactionId and newCategory are required.',
});
}

const result = await categorizationService.recategorizeTransaction({
userId,
transactionId,
newCategory,
applyToFuture: applyToFuture !== false,
});

return res.status(200).json({
success: true,
data: result,
});
} catch (err) {
return res.status(500).json({
success: false,
message: err.message || 'Failed to recategorize transaction',
});
}
},
};
40 changes: 40 additions & 0 deletions src/controllers/insight.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { insightEngineService } from '../services/insightEngine.service.js';

export const insightController = {
getNudges: async (req, res) => {
try {
const userId = req.user.id || req.user._id;
const nudges = await insightEngineService.getProactiveNudges(userId);

return res.status(200).json({
success: true,
count: nudges.length,
data: nudges,
});
} catch (err) {
return res.status(500).json({
success: false,
message: err.message || 'Failed to fetch proactive nudges',
});
}
},

dismissNudge: async (req, res) => {
try {
const userId = req.user.id || req.user._id;
const { id } = req.params;

const updated = await insightEngineService.dismissNudge(userId, id);
return res.status(200).json({
success: true,
message: 'Nudge dismissed',
data: updated,
});
} catch (err) {
return res.status(500).json({
success: false,
message: err.message || 'Failed to dismiss nudge',
});
}
},
};
32 changes: 32 additions & 0 deletions src/models/categoryRule.model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import mongoose from 'mongoose';

const CategoryRuleSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true,
},
merchantPattern: {
type: String,
required: true,
lowercase: true,
trim: true,
},
category: {
type: String,
required: true,
},
userCreated: {
type: Boolean,
default: true,
},
createdAt: {
type: Date,
default: Date.now,
},
});

CategoryRuleSchema.index({ userId: 1, merchantPattern: 1 }, { unique: true });

export const CategoryRuleModel = mongoose.model('CategoryRule', CategoryRuleSchema);
49 changes: 49 additions & 0 deletions src/models/nudge.model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import mongoose from 'mongoose';

const NudgeSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true,
},
type: {
type: String,
enum: ['SPIKE', 'SUBSCRIPTION', 'SAVINGS'],
required: true,
},
title: {
type: String,
required: true,
},
message: {
type: String,
required: true,
},
amount: {
type: Number,
default: 0,
},
category: {
type: String,
default: 'General',
},
actionText: {
type: String,
default: null,
},
read: {
type: Boolean,
default: false,
},
dismissed: {
type: Boolean,
default: false,
},
createdAt: {
type: Date,
default: Date.now,
},
});

export const NudgeModel = mongoose.model('Nudge', NudgeSchema);
8 changes: 8 additions & 0 deletions src/routes/ai.routes.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { Router } from 'express';
import { authenticate } from '../middlewares/auth.middleware.js';
import { requireConsent } from '../middlewares/consent.middleware.js';
import { handleAiChat } from '../controllers/ai.controller.js';
import { insightController } from '../controllers/insight.controller.js';
import { ApiResponse } from '../utils/apiResponse.js';

const router = Router();

router.use(authenticate);
router.use(requireConsent('aiUsageConsent'));

router.post('/chat', handleAiChat);

router.get('/nudges', insightController.getNudges);
router.patch('/nudges/:id/dismiss', insightController.dismissNudge);

router.post('/insights', (req, res) => {
return ApiResponse.success(res, 'AI Financial insights generated successfully', {
insights: 'Your spending in dining out decreased by 15% this month. Great job!',
});
});

export default router;

11 changes: 11 additions & 0 deletions src/routes/category.routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Router } from 'express';
import { authenticate } from '../middlewares/auth.middleware.js';
import { categoryController } from '../controllers/category.controller.js';

const router = Router();

router.use(authenticate);

router.post('/recategorize', categoryController.recategorizeTransaction);

export default router;
2 changes: 2 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import splitRoutes from './split.routes.js';
import notificationRoutes from './notification.routes.js';
import auditRoutes from './audit.routes.js';
import retentionRoutes from './retention.routes.js';
import categoryRoutes from './category.routes.js';

const router = Router();

Expand All @@ -34,5 +35,6 @@ router.use('/split', splitRoutes);
router.use('/notifications', notificationRoutes);
router.use('/audit', auditRoutes);
router.use('/retention', retentionRoutes);
router.use('/categories', categoryRoutes);

export default router;
Loading
Loading