2026-01-20 13:58:13 -05:00
|
|
|
import { Router, Response } from 'express';
|
|
|
|
|
import { AuthRequest, authMiddleware } from '../middleware/auth';
|
2026-01-22 14:23:55 -05:00
|
|
|
import { productQueries, priceHistoryQueries, stockStatusHistoryQueries } from '../models';
|
2026-01-20 20:54:12 -05:00
|
|
|
import { scrapeProduct } from '../services/scraper';
|
2026-01-20 13:58:13 -05:00
|
|
|
|
|
|
|
|
const router = Router();
|
|
|
|
|
|
|
|
|
|
// All routes require authentication
|
|
|
|
|
router.use(authMiddleware);
|
|
|
|
|
|
|
|
|
|
// Get price history for a product
|
|
|
|
|
router.get('/:productId/prices', async (req: AuthRequest, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const userId = req.userId!;
|
|
|
|
|
const productId = parseInt(req.params.productId, 10);
|
|
|
|
|
|
|
|
|
|
if (isNaN(productId)) {
|
|
|
|
|
res.status(400).json({ error: 'Invalid product ID' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify product belongs to user
|
|
|
|
|
const product = await productQueries.findById(productId, userId);
|
|
|
|
|
if (!product) {
|
|
|
|
|
res.status(404).json({ error: 'Product not found' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get optional days filter from query
|
|
|
|
|
const days = req.query.days ? parseInt(req.query.days as string, 10) : undefined;
|
|
|
|
|
|
|
|
|
|
const priceHistory = await priceHistoryQueries.findByProductId(
|
|
|
|
|
productId,
|
|
|
|
|
days
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
product,
|
|
|
|
|
prices: priceHistory,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error fetching price history:', error);
|
|
|
|
|
res.status(500).json({ error: 'Failed to fetch price history' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Force immediate price refresh
|
|
|
|
|
router.post('/:productId/refresh', async (req: AuthRequest, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const userId = req.userId!;
|
|
|
|
|
const productId = parseInt(req.params.productId, 10);
|
|
|
|
|
|
|
|
|
|
if (isNaN(productId)) {
|
|
|
|
|
res.status(400).json({ error: 'Invalid product ID' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify product belongs to user
|
|
|
|
|
const product = await productQueries.findById(productId, userId);
|
|
|
|
|
if (!product) {
|
|
|
|
|
res.status(404).json({ error: 'Product not found' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 20:54:12 -05:00
|
|
|
// Scrape product data including price and stock status
|
|
|
|
|
const scrapedData = await scrapeProduct(product.url);
|
2026-01-20 13:58:13 -05:00
|
|
|
|
2026-01-22 14:23:55 -05:00
|
|
|
// Update stock status and record change if different
|
|
|
|
|
if (scrapedData.stockStatus !== product.stock_status) {
|
|
|
|
|
await productQueries.updateStockStatus(productId, scrapedData.stockStatus);
|
|
|
|
|
await stockStatusHistoryQueries.recordChange(productId, scrapedData.stockStatus);
|
|
|
|
|
}
|
2026-01-20 13:58:13 -05:00
|
|
|
|
2026-01-20 20:54:12 -05:00
|
|
|
// Record new price if available
|
|
|
|
|
let newPrice = null;
|
|
|
|
|
if (scrapedData.price) {
|
|
|
|
|
newPrice = await priceHistoryQueries.create(
|
|
|
|
|
productId,
|
|
|
|
|
scrapedData.price.price,
|
|
|
|
|
scrapedData.price.currency
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-20 13:58:13 -05:00
|
|
|
|
2026-01-20 21:44:28 -05:00
|
|
|
// Update last_checked timestamp and schedule next check
|
|
|
|
|
await productQueries.updateLastChecked(productId, product.refresh_interval);
|
2026-01-20 13:58:13 -05:00
|
|
|
|
|
|
|
|
res.json({
|
2026-01-20 20:54:12 -05:00
|
|
|
message: scrapedData.stockStatus === 'out_of_stock'
|
|
|
|
|
? 'Product is currently out of stock'
|
|
|
|
|
: 'Price refreshed successfully',
|
2026-01-20 13:58:13 -05:00
|
|
|
price: newPrice,
|
2026-01-20 20:54:12 -05:00
|
|
|
stockStatus: scrapedData.stockStatus,
|
2026-01-20 13:58:13 -05:00
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error refreshing price:', error);
|
|
|
|
|
res.status(500).json({ error: 'Failed to refresh price' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-22 14:23:55 -05:00
|
|
|
// Get stock status history for a product
|
|
|
|
|
router.get('/:productId/stock-history', async (req: AuthRequest, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const userId = req.userId!;
|
|
|
|
|
const productId = parseInt(req.params.productId, 10);
|
|
|
|
|
|
|
|
|
|
if (isNaN(productId)) {
|
|
|
|
|
res.status(400).json({ error: 'Invalid product ID' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify product belongs to user
|
|
|
|
|
const product = await productQueries.findById(productId, userId);
|
|
|
|
|
if (!product) {
|
|
|
|
|
res.status(404).json({ error: 'Product not found' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get optional days filter from query (default 30 days)
|
|
|
|
|
const days = req.query.days ? parseInt(req.query.days as string, 10) : 30;
|
|
|
|
|
|
|
|
|
|
const stockHistory = await stockStatusHistoryQueries.getByProductId(productId, days);
|
|
|
|
|
const stats = await stockStatusHistoryQueries.getStats(productId, days);
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
history: stockHistory,
|
|
|
|
|
stats,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error fetching stock status history:', error);
|
|
|
|
|
res.status(500).json({ error: 'Failed to fetch stock status history' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-20 13:58:13 -05:00
|
|
|
export default router;
|