PriceGhost/backend/src/routes/prices.ts
clucraft 6c2aece1e8 Add stock status history tracking and timeline visualization
- Create stock_status_history table to track status changes over time
- Add stockStatusHistoryQueries with getByProductId, recordChange, getStats
- Update scheduler to record status changes
- Update product creation and manual refresh to record initial/changed status
- Add GET /products/:id/stock-history API endpoint
- Create StockTimeline component with:
  - Visual timeline bar showing in-stock (green) vs out-of-stock (red)
  - Availability percentage
  - Outage count and duration stats
- Integrate timeline into ProductDetail page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 14:23:55 -05:00

134 lines
4.1 KiB
TypeScript

import { Router, Response } from 'express';
import { AuthRequest, authMiddleware } from '../middleware/auth';
import { productQueries, priceHistoryQueries, stockStatusHistoryQueries } from '../models';
import { scrapeProduct } from '../services/scraper';
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;
}
// Scrape product data including price and stock status
const scrapedData = await scrapeProduct(product.url);
// 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);
}
// Record new price if available
let newPrice = null;
if (scrapedData.price) {
newPrice = await priceHistoryQueries.create(
productId,
scrapedData.price.price,
scrapedData.price.currency
);
}
// Update last_checked timestamp and schedule next check
await productQueries.updateLastChecked(productId, product.refresh_interval);
res.json({
message: scrapedData.stockStatus === 'out_of_stock'
? 'Product is currently out of stock'
: 'Price refreshed successfully',
price: newPrice,
stockStatus: scrapedData.stockStatus,
});
} catch (error) {
console.error('Error refreshing price:', error);
res.status(500).json({ error: 'Failed to refresh price' });
}
});
// 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' });
}
});
export default router;