import { EventEmitter } from "events"; import { HydratedDocument, Types } from "mongoose"; import { ThemeValueModel } from "../modules/category/models/themeValue.model"; import { IPriceHistory } from "../modules/product/models/Abstraction/IPriceHistory"; import { PriceHistoryModel } from "../modules/product/models/priceHistory.model"; import { ProductObserveModel } from "../modules/product/models/productObserve.model"; import { IUser } from "../modules/user/models/Abstraction/IUser"; import { EmailService } from "../utils/email.service"; import { TimeService } from "../utils/time.service"; // Use DI to inject TimeService for better testability enum PriceChangeEventType { PRICE_CHANGE = "priceChange", OBSERVER_NOTIFICATION = "observerNotification", } type PriceDetails = { product: number; shop: string; selling_price: number; retail_price: number; variantId: string; isSpecial?: boolean; themeValueId?: string; }; type historyDetail = { shop: string; selling_price: number; retail_price: number }; class PriceChangeEventService extends EventEmitter { private static instance: PriceChangeEventService; private constructor() { super(); this.subscribeToEvents(); } //**** */ public static getInstance(): PriceChangeEventService { if (!PriceChangeEventService.instance) { PriceChangeEventService.instance = new PriceChangeEventService(); } return PriceChangeEventService.instance; } //**** */ public emitPriceChange(priceDetails: PriceDetails): void { this.emit(PriceChangeEventType.PRICE_CHANGE, priceDetails); if (priceDetails.isSpecial) { this.emitObserverNotification(priceDetails); } } //**** */ private emitObserverNotification(priceDetails: PriceDetails): void { this.emit(PriceChangeEventType.OBSERVER_NOTIFICATION, priceDetails); } //**** */ private subscribeToEvents(): void { this.on(PriceChangeEventType.PRICE_CHANGE, this.handlePriceChange); this.on(PriceChangeEventType.OBSERVER_NOTIFICATION, this.handleObserverNotification); } //**** */ private async handlePriceChange(priceDetails: PriceDetails): Promise { try { const title = await this.findVariantTitle(priceDetails); const now = TimeService.getCurrentPersianDate(); const { themeValueId, product, ...history } = priceDetails; const existingHistory = await PriceHistoryModel.findOne({ title, product }); if (existingHistory) { await this.appendPriceHistory(existingHistory, history, now); console.log(`Updated price history for product ${product} with price: ${history.selling_price}`); } else { await this.createPriceHistory(title, product, history, now); console.log(`Created new price history for product ${product} with price: ${history.selling_price}`); } } catch (error) { console.error("Failed to record price change:", error); } } //**** */ private async handleObserverNotification(priceDetails: PriceDetails): Promise { try { const observers = await this.findObserversForProduct(priceDetails.product, priceDetails.variantId); for (const observer of observers) { const user = observer.user as unknown as IUser; const emailContent = this.generateEmailContent(user.fullName, observer.registeredPrice, priceDetails); await EmailService.sendEmail({ subject: "تخفیف ویژه", to: user.email, text: emailContent, }); console.log(`Sent notification to observer: ${user.email}`); } } catch (error) { console.error("Failed to send observer notifications:", error); } } //**** */ private async findObserversForProduct(productId: number, variantId: string) { return await ProductObserveModel.find({ product: productId, variant: variantId }).populate("user").lean(); } //**** */ private generateEmailContent(userName: string, observerPrice: number, priceDetails: PriceDetails): string { const priceDiff = observerPrice - priceDetails.selling_price; const message = `کاربر گرامی ${userName},\n\nقیمت محصولی که شما برای آن منتظر شده‌اید، به ${priceDetails.selling_price} تغییر کرده است.`; if (priceDiff > 0) { return message + `\n\nاین قیمت ${priceDiff} تومان کمتر از قیمت هدف شما است. این فرصت را از دست ندهید!`; } else { return message + `\n\nاین یک پیشنهاد ویژه است که ممکن است برای شما جذاب باشد!`; } } //**** */ private async appendPriceHistory(existHistory: HydratedDocument, history: historyDetail, now: string) { existHistory.history.push({ ...history, date: now, shop: new Types.ObjectId(history.shop), }); await existHistory.save(); } //**** */ private async createPriceHistory(title: string, product: number, history: historyDetail, now: string): Promise { const priceHistory = new PriceHistoryModel({ title, product }); priceHistory.history.push({ ...history, date: now, shop: new Types.ObjectId(history.shop), }); await priceHistory.save(); } //**** */ private async findVariantTitle(priceDetails: PriceDetails): Promise { if (priceDetails.themeValueId) { const themeValue = await ThemeValueModel.findById(priceDetails.themeValueId).lean(); return themeValue?.name || "Unknown variant"; } return ""; } } const instance = PriceChangeEventService.getInstance(); export { instance as PriceChangeEvent, PriceChangeEventService, PriceChangeEventType };