147 lines
5.6 KiB
TypeScript
147 lines
5.6 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
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<IPriceHistory>, 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<void> {
|
|
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<string> {
|
|
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 };
|