Wire up MongoDB and implement all CRUD route handlers

- Enable MongoDB connection in server
- Add Customer and Expense models
- Implement full CRUD for products, orders, customers, expenses
- Implement analytics dashboard with revenue, sales chart, top products/customers
- Add MongoDB service to docker-compose.deploy.yml with persistent volume

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dlawler489 2026-04-22 08:08:08 +10:00
parent 0ebcc26425
commit 87e4147e8c
9 changed files with 410 additions and 93 deletions

View file

@ -35,9 +35,12 @@ services:
- NODE_ENV=production - NODE_ENV=production
- PORT=8080 - PORT=8080
- CLIENT_URL=http://nginx - CLIENT_URL=http://nginx
- MONGODB_URI=mongodb://mongodb:27017/etsy-tracker
volumes: volumes:
- etsy_data:/app/data - etsy_data:/app/data
- etsy_uploads:/app/uploads - etsy_uploads:/app/uploads
depends_on:
- mongodb
restart: unless-stopped restart: unless-stopped
networks: networks:
- etsy-network - etsy-network
@ -48,11 +51,28 @@ services:
retries: 3 retries: 3
start_period: 40s start_period: 40s
mongodb:
image: mongo:7
container_name: etsy-mongodb
restart: unless-stopped
volumes:
- etsy_mongodb:/data/db
networks:
- etsy-network
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
volumes: volumes:
etsy_uploads: etsy_uploads:
driver: local driver: local
etsy_data: etsy_data:
driver: local driver: local
etsy_mongodb:
driver: local
networks: networks:
etsy-network: etsy-network:

View file

@ -54,12 +54,9 @@ app.get('/health', (req, res) => {
}); });
}); });
// Database connection (temporarily disabled for development) mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/etsy-tracker')
// mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/etsy-tracker') .then(() => console.log('Connected to MongoDB'))
// .then(() => console.log('Connected to MongoDB')) .catch((error) => console.error('MongoDB connection error:', error));
// .catch((error) => console.error('MongoDB connection error:', error));
console.log('Running in development mode without MongoDB - using in-memory storage');
// Routes // Routes
app.use('/api/auth', authRoutes); app.use('/api/auth', authRoutes);

View file

@ -0,0 +1,44 @@
import mongoose, { Document, Schema } from 'mongoose';
export interface ICustomer extends Document {
name: string;
email: string;
etsyUserId?: string;
address?: {
street1: string;
street2?: string;
city: string;
state: string;
zipCode: string;
country: string;
};
totalOrders: number;
totalSpent: number;
dateCreated: Date;
dateUpdated: Date;
}
const CustomerSchema: Schema = new Schema({
name: { type: String, required: true, trim: true },
email: { type: String, required: true, trim: true, lowercase: true },
etsyUserId: { type: String },
address: {
street1: { type: String },
street2: { type: String },
city: { type: String },
state: { type: String },
zipCode: { type: String },
country: { type: String },
},
totalOrders: { type: Number, default: 0 },
totalSpent: { type: Number, default: 0 },
dateCreated: { type: Date, default: Date.now },
dateUpdated: { type: Date, default: Date.now },
});
CustomerSchema.pre('save', function (next) {
this.dateUpdated = new Date();
next();
});
export default mongoose.model<ICustomer>('Customer', CustomerSchema);

View file

@ -0,0 +1,34 @@
import mongoose, { Document, Schema } from 'mongoose';
export interface IExpense extends Document {
category: 'materials' | 'shipping' | 'fees' | 'advertising' | 'tools' | 'other';
description: string;
amount: number;
date: Date;
receiptUrl?: string;
notes?: string;
dateCreated: Date;
dateUpdated: Date;
}
const ExpenseSchema: Schema = new Schema({
category: {
type: String,
enum: ['materials', 'shipping', 'fees', 'advertising', 'tools', 'other'],
required: true,
},
description: { type: String, required: true, trim: true },
amount: { type: Number, required: true, min: 0 },
date: { type: Date, required: true },
receiptUrl: { type: String },
notes: { type: String },
dateCreated: { type: Date, default: Date.now },
dateUpdated: { type: Date, default: Date.now },
});
ExpenseSchema.pre('save', function (next) {
this.dateUpdated = new Date();
next();
});
export default mongoose.model<IExpense>('Expense', ExpenseSchema);

View file

@ -1,35 +1,126 @@
import { Router } from 'express'; import { Router, Request, Response } from 'express';
import Order from '../models/Order';
import Product from '../models/Product';
import Customer from '../models/Customer';
import Expense from '../models/Expense';
const router = Router(); const router = Router();
// GET /api/analytics/dashboard router.get('/dashboard', async (req: Request, res: Response) => {
router.get('/dashboard', (req, res) => { try {
res.json({ const [totalOrders, totalProducts, totalCustomers, revenueResult, expenseResult, recentOrders] = await Promise.all([
message: 'Dashboard analytics endpoint', Order.countDocuments({ paymentStatus: 'paid' }),
data: { Product.countDocuments({ isActive: true }),
totalRevenue: 0, Customer.countDocuments(),
totalOrders: 0, Order.aggregate([
totalProducts: 0, { $match: { paymentStatus: 'paid' } },
totalCustomers: 0, { $group: { _id: null, total: { $sum: '$total' } } },
recentOrders: [], ]),
salesChart: [], Expense.aggregate([
{ $group: { _id: null, total: { $sum: '$amount' } } },
]),
Order.find({ paymentStatus: 'paid' })
.populate('customerId', 'name email')
.sort({ dateOrdered: -1 })
.limit(5),
]);
const totalRevenue = revenueResult[0]?.total ?? 0;
const totalExpenses = expenseResult[0]?.total ?? 0;
const twelveMonthsAgo = new Date();
twelveMonthsAgo.setMonth(twelveMonthsAgo.getMonth() - 11);
twelveMonthsAgo.setDate(1);
twelveMonthsAgo.setHours(0, 0, 0, 0);
const salesChart = await Order.aggregate([
{ $match: { paymentStatus: 'paid', dateOrdered: { $gte: twelveMonthsAgo } } },
{
$group: {
_id: { year: { $year: '$dateOrdered' }, month: { $month: '$dateOrdered' } },
revenue: { $sum: '$total' },
orders: { $sum: 1 },
},
},
{ $sort: { '_id.year': 1, '_id.month': 1 } },
]);
res.json({
totalRevenue,
totalExpenses,
totalOrders,
totalProducts,
totalCustomers,
recentOrders,
salesChart,
});
} catch (err) {
res.status(500).json({ message: 'Failed to fetch dashboard data', error: err });
}
});
router.get('/sales', async (req: Request, res: Response) => {
try {
const { from, to } = req.query;
const match: any = { paymentStatus: 'paid' };
if (from || to) {
match.dateOrdered = {};
if (from) match.dateOrdered.$gte = new Date(from as string);
if (to) match.dateOrdered.$lte = new Date(to as string);
} }
});
const data = await Order.aggregate([
{ $match: match },
{
$group: {
_id: { year: { $year: '$dateOrdered' }, month: { $month: '$dateOrdered' } },
revenue: { $sum: '$total' },
orders: { $sum: 1 },
},
},
{ $sort: { '_id.year': 1, '_id.month': 1 } },
]);
res.json(data);
} catch (err) {
res.status(500).json({ message: 'Failed to fetch sales data', error: err });
}
}); });
// GET /api/analytics/sales router.get('/products', async (req: Request, res: Response) => {
router.get('/sales', (req, res) => { try {
res.json({ message: 'Sales analytics endpoint - coming soon!' }); const topProducts = await Order.aggregate([
{ $match: { paymentStatus: 'paid' } },
{ $unwind: '$items' },
{
$group: {
_id: '$items.productId',
title: { $first: '$items.title' },
totalSold: { $sum: '$items.quantity' },
totalRevenue: { $sum: { $multiply: ['$items.quantity', '$items.price'] } },
},
},
{ $sort: { totalRevenue: -1 } },
{ $limit: 10 },
]);
res.json(topProducts);
} catch (err) {
res.status(500).json({ message: 'Failed to fetch product analytics', error: err });
}
}); });
// GET /api/analytics/products router.get('/customers', async (req: Request, res: Response) => {
router.get('/products', (req, res) => { try {
res.json({ message: 'Product analytics endpoint - coming soon!' }); const topCustomers = await Customer.find()
.sort({ totalSpent: -1 })
.limit(10)
.select('name email totalOrders totalSpent');
res.json(topCustomers);
} catch (err) {
res.status(500).json({ message: 'Failed to fetch customer analytics', error: err });
}
}); });
// GET /api/analytics/customers export default router;
router.get('/customers', (req, res) => {
res.json({ message: 'Customer analytics endpoint - coming soon!' });
});
export default router;

View file

@ -1,25 +1,50 @@
import { Router } from 'express'; import { Router, Request, Response } from 'express';
import Customer from '../models/Customer';
const router = Router(); const router = Router();
// GET /api/customers router.get('/', async (req: Request, res: Response) => {
router.get('/', (req, res) => { try {
res.json({ message: 'Get customers endpoint - coming soon!', customers: [] }); const { page = 1, limit = 20 } = req.query;
const customers = await Customer.find()
.sort({ dateCreated: -1 })
.limit(Number(limit))
.skip((Number(page) - 1) * Number(limit));
const total = await Customer.countDocuments();
res.json({ customers, total, page: Number(page), limit: Number(limit) });
} catch (err) {
res.status(500).json({ message: 'Failed to fetch customers', error: err });
}
}); });
// POST /api/customers router.post('/', async (req: Request, res: Response) => {
router.post('/', (req, res) => { try {
res.json({ message: 'Create customer endpoint - coming soon!' }); const customer = new Customer(req.body);
await customer.save();
res.status(201).json(customer);
} catch (err) {
res.status(400).json({ message: 'Failed to create customer', error: err });
}
}); });
// GET /api/customers/:id router.get('/:id', async (req: Request, res: Response) => {
router.get('/:id', (req, res) => { try {
res.json({ message: 'Get customer by ID endpoint - coming soon!' }); const customer = await Customer.findById(req.params.id);
if (!customer) return res.status(404).json({ message: 'Customer not found' });
res.json(customer);
} catch (err) {
res.status(500).json({ message: 'Failed to fetch customer', error: err });
}
}); });
// PUT /api/customers/:id router.put('/:id', async (req: Request, res: Response) => {
router.put('/:id', (req, res) => { try {
res.json({ message: 'Update customer endpoint - coming soon!' }); const customer = await Customer.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
if (!customer) return res.status(404).json({ message: 'Customer not found' });
res.json(customer);
} catch (err) {
res.status(400).json({ message: 'Failed to update customer', error: err });
}
}); });
export default router; export default router;

View file

@ -1,30 +1,64 @@
import { Router } from 'express'; import { Router, Request, Response } from 'express';
import Expense from '../models/Expense';
const router = Router(); const router = Router();
// GET /api/expenses router.get('/', async (req: Request, res: Response) => {
router.get('/', (req, res) => { try {
res.json({ message: 'Get expenses endpoint - coming soon!', expenses: [] }); const { page = 1, limit = 20, category } = req.query;
const filter: any = {};
if (category) filter.category = category;
const expenses = await Expense.find(filter)
.sort({ date: -1 })
.limit(Number(limit))
.skip((Number(page) - 1) * Number(limit));
const total = await Expense.countDocuments(filter);
res.json({ expenses, total, page: Number(page), limit: Number(limit) });
} catch (err) {
res.status(500).json({ message: 'Failed to fetch expenses', error: err });
}
}); });
// POST /api/expenses router.post('/', async (req: Request, res: Response) => {
router.post('/', (req, res) => { try {
res.json({ message: 'Create expense endpoint - coming soon!' }); const expense = new Expense(req.body);
await expense.save();
res.status(201).json(expense);
} catch (err) {
res.status(400).json({ message: 'Failed to create expense', error: err });
}
}); });
// GET /api/expenses/:id router.get('/:id', async (req: Request, res: Response) => {
router.get('/:id', (req, res) => { try {
res.json({ message: 'Get expense by ID endpoint - coming soon!' }); const expense = await Expense.findById(req.params.id);
if (!expense) return res.status(404).json({ message: 'Expense not found' });
res.json(expense);
} catch (err) {
res.status(500).json({ message: 'Failed to fetch expense', error: err });
}
}); });
// PUT /api/expenses/:id router.put('/:id', async (req: Request, res: Response) => {
router.put('/:id', (req, res) => { try {
res.json({ message: 'Update expense endpoint - coming soon!' }); const expense = await Expense.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
if (!expense) return res.status(404).json({ message: 'Expense not found' });
res.json(expense);
} catch (err) {
res.status(400).json({ message: 'Failed to update expense', error: err });
}
}); });
// DELETE /api/expenses/:id router.delete('/:id', async (req: Request, res: Response) => {
router.delete('/:id', (req, res) => { try {
res.json({ message: 'Delete expense endpoint - coming soon!' }); const expense = await Expense.findByIdAndDelete(req.params.id);
if (!expense) return res.status(404).json({ message: 'Expense not found' });
res.json({ message: 'Expense deleted' });
} catch (err) {
res.status(500).json({ message: 'Failed to delete expense', error: err });
}
}); });
export default router; export default router;

View file

@ -1,25 +1,62 @@
import { Router } from 'express'; import { Router, Request, Response } from 'express';
import Order from '../models/Order';
import Customer from '../models/Customer';
const router = Router(); const router = Router();
// GET /api/orders router.get('/', async (req: Request, res: Response) => {
router.get('/', (req, res) => { try {
res.json({ message: 'Get orders endpoint - coming soon!', orders: [] }); const { page = 1, limit = 20, status, paymentStatus } = req.query;
const filter: any = {};
if (status) filter.status = status;
if (paymentStatus) filter.paymentStatus = paymentStatus;
const orders = await Order.find(filter)
.populate('customerId', 'name email')
.sort({ dateOrdered: -1 })
.limit(Number(limit))
.skip((Number(page) - 1) * Number(limit));
const total = await Order.countDocuments(filter);
res.json({ orders, total, page: Number(page), limit: Number(limit) });
} catch (err) {
res.status(500).json({ message: 'Failed to fetch orders', error: err });
}
}); });
// POST /api/orders router.post('/', async (req: Request, res: Response) => {
router.post('/', (req, res) => { try {
res.json({ message: 'Create order endpoint - coming soon!' }); const order = new Order(req.body);
await order.save();
await Customer.findByIdAndUpdate(order.customerId, {
$inc: { totalOrders: 1, totalSpent: order.total },
});
res.status(201).json(order);
} catch (err) {
res.status(400).json({ message: 'Failed to create order', error: err });
}
}); });
// GET /api/orders/:id router.get('/:id', async (req: Request, res: Response) => {
router.get('/:id', (req, res) => { try {
res.json({ message: 'Get order by ID endpoint - coming soon!' }); const order = await Order.findById(req.params.id).populate('customerId', 'name email');
if (!order) return res.status(404).json({ message: 'Order not found' });
res.json(order);
} catch (err) {
res.status(500).json({ message: 'Failed to fetch order', error: err });
}
}); });
// PUT /api/orders/:id router.put('/:id', async (req: Request, res: Response) => {
router.put('/:id', (req, res) => { try {
res.json({ message: 'Update order endpoint - coming soon!' }); const order = await Order.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
if (!order) return res.status(404).json({ message: 'Order not found' });
res.json(order);
} catch (err) {
res.status(400).json({ message: 'Failed to update order', error: err });
}
}); });
export default router; export default router;

View file

@ -1,30 +1,65 @@
import { Router } from 'express'; import { Router, Request, Response } from 'express';
import Product from '../models/Product';
const router = Router(); const router = Router();
// GET /api/products router.get('/', async (req: Request, res: Response) => {
router.get('/', (req, res) => { try {
res.json({ message: 'Get products endpoint - coming soon!', products: [] }); const { page = 1, limit = 20, category, active } = req.query;
const filter: any = {};
if (category) filter.category = category;
if (active !== undefined) filter.isActive = active === 'true';
const products = await Product.find(filter)
.sort({ dateCreated: -1 })
.limit(Number(limit))
.skip((Number(page) - 1) * Number(limit));
const total = await Product.countDocuments(filter);
res.json({ products, total, page: Number(page), limit: Number(limit) });
} catch (err) {
res.status(500).json({ message: 'Failed to fetch products', error: err });
}
}); });
// POST /api/products router.post('/', async (req: Request, res: Response) => {
router.post('/', (req, res) => { try {
res.json({ message: 'Create product endpoint - coming soon!' }); const product = new Product(req.body);
await product.save();
res.status(201).json(product);
} catch (err) {
res.status(400).json({ message: 'Failed to create product', error: err });
}
}); });
// GET /api/products/:id router.get('/:id', async (req: Request, res: Response) => {
router.get('/:id', (req, res) => { try {
res.json({ message: 'Get product by ID endpoint - coming soon!' }); const product = await Product.findById(req.params.id);
if (!product) return res.status(404).json({ message: 'Product not found' });
res.json(product);
} catch (err) {
res.status(500).json({ message: 'Failed to fetch product', error: err });
}
}); });
// PUT /api/products/:id router.put('/:id', async (req: Request, res: Response) => {
router.put('/:id', (req, res) => { try {
res.json({ message: 'Update product endpoint - coming soon!' }); const product = await Product.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
if (!product) return res.status(404).json({ message: 'Product not found' });
res.json(product);
} catch (err) {
res.status(400).json({ message: 'Failed to update product', error: err });
}
}); });
// DELETE /api/products/:id router.delete('/:id', async (req: Request, res: Response) => {
router.delete('/:id', (req, res) => { try {
res.json({ message: 'Delete product endpoint - coming soon!' }); const product = await Product.findByIdAndDelete(req.params.id);
if (!product) return res.status(404).json({ message: 'Product not found' });
res.json({ message: 'Product deleted' });
} catch (err) {
res.status(500).json({ message: 'Failed to delete product', error: err });
}
}); });
export default router; export default router;