← back to Watches

database/services/WatchService.js

303 lines

/**
 * Watch Service Layer
 * Business logic layer with caching integration
 */

import WatchRepository from '../repositories/WatchRepository.js';
import PriceHistoryRepository from '../repositories/PriceHistoryRepository.js';
import cache from '../cache/RedisCache.js';

class WatchService {
    constructor() {
        this.watchRepo = WatchRepository;
        this.priceRepo = PriceHistoryRepository;
        this.cache = cache;
    }

    /**
     * Get all watches with caching
     */
    async getAllWatches(options = {}) {
        const cacheKey = this.cache.generateKey(
            'watches:all',
            JSON.stringify(options)
        );

        return await this.cache.getOrSet(
            cacheKey,
            async () => await this.watchRepo.findAll(options),
            this.cache.TTL.MEDIUM
        );
    }

    /**
     * Get watch by ID with full details and caching
     */
    async getWatchById(watchId, includeHistory = false) {
        const cacheKey = this.cache.generateKey(
            'watch:full',
            `${watchId}:${includeHistory}`
        );

        const result = await this.cache.getOrSet(
            cacheKey,
            async () => {
                if (includeHistory) {
                    return await this.watchRepo.findByIdWithPriceHistory(watchId);
                }
                return await this.watchRepo.findById(watchId);
            },
            this.cache.TTL.LONG
        );

        // Track view asynchronously (fire and forget)
        if (result.value) {
            this.watchRepo.trackView(result.value.id, {
                sessionId: null, // Would come from request
                userAgent: null,
                ipAddress: null
            }).catch(err => console.error('View tracking failed:', err));
        }

        return result.value;
    }

    /**
     * Search watches with caching
     */
    async searchWatches(searchTerm, options = {}) {
        const cacheKey = this.cache.generateKey(
            'search',
            `${searchTerm}:${JSON.stringify(options)}`
        );

        return await this.cache.getOrSet(
            cacheKey,
            async () => await this.watchRepo.search(searchTerm, options),
            this.cache.TTL.SHORT
        );
    }

    /**
     * Get watch statistics with caching
     */
    async getWatchStatistics(watchId) {
        const cacheKey = this.cache.generateKey('watch:stats', watchId);

        return await this.cache.getOrSet(
            cacheKey,
            async () => await this.watchRepo.getStatistics(watchId),
            this.cache.TTL.LONG
        );
    }

    /**
     * Get all statistics (leaderboard)
     */
    async getAllStatistics(options = {}) {
        const cacheKey = this.cache.generateKey(
            'stats:all',
            JSON.stringify(options)
        );

        return await this.cache.getOrSet(
            cacheKey,
            async () => await this.watchRepo.getAllStatistics(options),
            this.cache.TTL.LONG
        );
    }

    /**
     * Get trending watches with caching
     */
    async getTrendingWatches(daysBack = 7, limit = 10) {
        const cacheKey = this.cache.generateKey('trending', `${daysBack}:${limit}`);

        return await this.cache.getOrSet(
            cacheKey,
            async () => await this.watchRepo.getTrending(daysBack, limit),
            this.cache.TTL.SHORT // Shorter TTL for trending data
        );
    }

    /**
     * Get watches by series with caching
     */
    async getWatchesBySeries(series) {
        const cacheKey = this.cache.generateKey('series', series);

        return await this.cache.getOrSet(
            cacheKey,
            async () => await this.watchRepo.findBySeries(series),
            this.cache.TTL.LONG
        );
    }

    /**
     * Get price history with caching
     */
    async getPriceHistory(watchId, options = {}) {
        const cacheKey = this.cache.generateKey(
            'price-history',
            `${watchId}:${JSON.stringify(options)}`
        );

        return await this.cache.getOrSet(
            cacheKey,
            async () => await this.priceRepo.findByWatchId(watchId, options),
            this.cache.TTL.LONG
        );
    }

    /**
     * Calculate appreciation with caching
     */
    async calculateAppreciation(watchId, startYear, endYear) {
        const cacheKey = this.cache.generateKey(
            'appreciation',
            `${watchId}:${startYear}:${endYear}`
        );

        return await this.cache.getOrSet(
            cacheKey,
            async () => await this.priceRepo.calculateAppreciation(watchId, startYear, endYear),
            this.cache.TTL.VERY_LONG
        );
    }

    /**
     * Get price trends with caching
     */
    async getPriceTrends(options = {}) {
        const cacheKey = this.cache.generateKey('trends', JSON.stringify(options));

        return await this.cache.getOrSet(
            cacheKey,
            async () => await this.priceRepo.getPriceTrends(options),
            this.cache.TTL.LONG
        );
    }

    /**
     * Get series comparison with caching
     */
    async getSeriesComparison(series, startYear, endYear) {
        const cacheKey = this.cache.generateKey(
            'comparison',
            `${series}:${startYear}:${endYear}`
        );

        return await this.cache.getOrSet(
            cacheKey,
            async () => await this.priceRepo.getSeriesComparison(series, startYear, endYear),
            this.cache.TTL.VERY_LONG
        );
    }

    /**
     * Create watch and invalidate cache
     */
    async createWatch(watchData) {
        const watch = await this.watchRepo.create(watchData);

        // Invalidate relevant caches
        await this.cache.deletePattern('omega:watches:*');
        await this.cache.deletePattern('omega:stats:*');

        return watch;
    }

    /**
     * Update watch and invalidate cache
     */
    async updateWatch(watchId, updateData) {
        const watch = await this.watchRepo.update(watchId, updateData);

        // Invalidate watch-specific and list caches
        await this.cache.invalidateWatch(watchId);
        await this.cache.deletePattern('omega:watches:*');
        await this.cache.deletePattern('omega:stats:*');

        return watch;
    }

    /**
     * Add price point and invalidate cache
     */
    async addPricePoint(watchId, priceData) {
        const price = await this.priceRepo.create({
            watchId,
            ...priceData
        });

        // Invalidate caches
        await this.cache.invalidateWatch(watchId);
        await this.cache.deletePattern('omega:stats:*');
        await this.cache.deletePattern('omega:trends:*');

        return price;
    }

    /**
     * Refresh statistics materialized view
     */
    async refreshStatistics() {
        await this.watchRepo.refreshStatistics();
        await this.cache.deletePattern('omega:stats:*');
        console.log('✅ Statistics refreshed and cache invalidated');
    }

    /**
     * Warm cache with frequently accessed data
     */
    async warmCache() {
        console.log('🔥 Warming cache...');

        try {
            // Warm all watches
            const watches = await this.watchRepo.findAll({ limit: 100 });
            const watchData = {};

            for (const watch of watches) {
                const cacheKey = this.cache.generateKey('watch:full', watch.id);
                watchData[cacheKey] = watch;
            }

            await this.cache.warmCache(watchData, 'watch', this.cache.TTL.LONG);

            // Warm statistics
            const stats = await this.watchRepo.getAllStatistics({ limit: 100 });
            await this.cache.set(
                this.cache.generateKey('stats:all', 'default'),
                stats,
                this.cache.TTL.LONG
            );

            console.log('✅ Cache warmed successfully');
        } catch (error) {
            console.error('❌ Cache warming failed:', error);
        }
    }

    /**
     * Get service health
     */
    async getHealth() {
        const dbHealth = await import('../connection.js').then(mod =>
            mod.checkDatabaseHealth()
        );

        const cacheHealth = await this.cache.healthCheck();

        return {
            database: dbHealth,
            cache: {
                healthy: cacheHealth,
                connected: cacheHealth
            }
        };
    }
}

export default new WatchService();