import { Bid } from '@/modules/bids/entities/bid.entity'; import { BidMetadatasService } from '@/modules/bids/services/bid-metadatas.service'; import { BidsService } from '@/modules/bids/services/bids.service'; import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity'; import { extractDomain, extractDomainSmart, formatEndTime, isHotItemFn, isTimeReached, } from '@/ultils'; import { MailerService } from '@nestjs-modules/mailer'; import { InjectQueue } from '@nestjs/bull'; import { Injectable } from '@nestjs/common'; import { Queue } from 'bull'; import * as _ from 'lodash'; @Injectable() export class MailsService { constructor( private readonly mailerService: MailerService, @InjectQueue('mail-queue') private mailQueue: Queue, private readonly bidsService: BidsService, private readonly bidMetadataService: BidMetadatasService, ) {} async sendPlainText(to: string, subject: string, content: string) { await this.mailerService.sendMail({ to, subject, text: content, }); } async sendHtmlMailJob(mailData: { to: string; subject: string; html: string; }) { await this.mailQueue.add('send-mail', mailData); } async sendPlainHtml(to: string, subject: string, html: string) { const emails = to .split(',') .map((e) => e.trim()) .filter(Boolean); const mainRecipient = emails[0]; const ccRecipients = emails.slice(1); // Những email còn lại await this.mailerService.sendMail({ to: mainRecipient, cc: ccRecipients.length > 0 ? ccRecipients : undefined, subject, html, }); } generateProductTableHTML( products: ScrapItem[], hotItems: { name: string }[], ): { html: string; hasHotItem: boolean; } { const from = process.env.MAIL_USER || 'no-reply@example.com'; let hasHotItem = false; if (!products.length) { return { html: `
No matching products found for your keywords today.
From: ${from}
`, hasHotItem, }; } // Decorate products with isHotItem const decorated = products.map((p) => { const isHotItem = hotItems.some((obj) => p.name.toLowerCase().includes(obj.name.toLowerCase()), ); if (isHotItem) hasHotItem = true; return { ...p, isHotItem, isNew: new Date(p.created_at).getTime() === new Date(p.updated_at).getTime(), }; }); // Separate hot and non-hot items, preserving original order const hotItemsList = decorated.filter((p) => p.isHotItem); const nonHotItems = decorated.filter((p) => !p.isHotItem); // Đưa các item mới (isNew) lên đầu trong nhóm không phải hot item const sortedNonHotItems = [ ...nonHotItems.filter((p) => p.isNew), ...nonHotItems.filter((p) => !p.isNew), ]; // Separate hot and non-hot items, preserving original order const hotItemsFirst = [...hotItemsList, ...sortedNonHotItems]; const rows = hotItemsFirst .map((p) => { const isHotItem = isHotItemFn(p, hotItems); if (isHotItem) { hasHotItem = true; } const isNew = new Date(p.created_at).getTime() === new Date(p.updated_at).getTime(); const nameStyle = isNew ? 'color: #2f9e44; font-weight: bold;' : ''; const namePrefix = isHotItem ? '[HOT ITEM] ' : ''; return `| Image | Name | Price | Link | Domain |
|---|
From: ${from}
`, hasHotItem, }; } getAuctionStatusEmailContent(bid: Bid): string { const webname = extractDomain(bid.web_bid.origin_url); const title = `[${webname}] ${bid.name || 'Unnamed Item'}`; const endTime = formatEndTime(bid.close_time, false); const competitor = `$${bid.current_price}`; const max = `$${bid.max_price}`; const submitted = `$${bid.max_price}`; const nextBid = bid.max_price + bid.plus_price; const from = process.env.MAIL_USER || 'no-reply@example.com'; const cardStyle = ` max-width: 600px; margin: 20px auto; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #ffffff; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); color: #333; padding: 20px; `; const headerStyle = (color: string) => `font-size: 22px; font-weight: 700; color: ${color}; margin-bottom: 15px;`; const labelStyle = `font-weight: 600; width: 120px; display: inline-block; color: #555;`; const valueStyle = `color: #222;`; const renderRow = (label: string, value: string) => `${label}: ${value}
`; switch (bid.status) { case 'biding': return `⚠️ Current bid exceeds your max bid.
🛑 Auction has been canceled.
| Title | ${title} |
|---|---|
| Competitor | ${competitor} |
| Bid Submitted | ${submitted} ${maxReached ? '(***MAXIMUM REACH***)' : ''} |
| Max | ${max} |
| End Time | ${endTime} |
| From | ${from} |
Name: ${bid?.name}
Recorded: ${new Date().toLocaleString()}
`; } else if (type === 'api') { content = `${JSON.stringify(json, null, 2)}
`;
}
return `
⏰ Close Time: ${closeTime}
${ content ? content : `⚠️ No record available for this bid.
` }This is an automated message. Please do not reply.