import { Injectable } from '@nestjs/common'; import { MailerService } from '@nestjs-modules/mailer'; import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity'; import { extractDomain, extractDomainSmart, formatEndTime, isTimeReached, } from '@/ultils'; import { Bid } from '@/modules/bids/entities/bid.entity'; import { InjectQueue } from '@nestjs/bull'; import { Queue } from 'bull'; @Injectable() export class MailsService { constructor( private readonly mailerService: MailerService, @InjectQueue('mail-queue') private mailQueue: Queue, ) {} 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[]): string { if (!products.length) { return ` Products

Product Listing

No matching products found for your keywords today.

`; } const rows = products .map( (p) => ` Product Image ${p.name} ${p.current_price ? '$' + p.current_price : 'None'} View ${extractDomainSmart(p.scrap_config.web_bid.origin_url)} `, ) .join(''); return ` Products

Product Listing

${rows}
Image Name Price Link Domain
`; } 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 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 `

✅ Auto Bid Started

${renderRow('Title', title)} ${renderRow('Max', max)} ${renderRow('End time', endTime)} ${renderRow('Competitor', competitor)} ${renderRow('Bid submitted', submitted)}
`; case 'out-bid': { const overLimit = bid.current_price >= nextBid; const belowReserve = bid.reserve_price > nextBid; const timeExtended = bid.close_time ? 'Time extended' : 'No extension'; if (isTimeReached(bid.close_time)) { return `

⏳ Auction Ended

${renderRow('Title', title)} ${renderRow('End time', endTime)} ${renderRow('Final price', competitor)}
`; } if (overLimit || belowReserve) { return `

⚠️ Outbid (${timeExtended})

${renderRow('Title', title)} ${renderRow('Competitor', competitor)} ${renderRow('Max', max)} ${renderRow('Next bid at', `$${nextBid}`)} ${renderRow('End time', endTime)}

⚠️ Current bid exceeds your max bid.

`; } return `

🛑 Auction Canceled (${timeExtended})

${renderRow('Title', title)} ${renderRow('Competitor', competitor)} ${renderRow('Max', max)} ${renderRow('Next bid at', `$${nextBid}`)} ${renderRow('End time', endTime)}

🛑 Auction has been canceled.

`; } case 'win-bid': return `

🎉 You Won!

${renderRow('Title', title)} ${renderRow('Price won', `$${bid.current_price}`)} ${renderRow('Max', max)}
`; default: return `

❓ Unknown Status

${renderRow('Title', title)}
`; } } getBidSubmittedEmailContent(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 maxReached = bid.max_price <= bid.max_price; return `

Bid Submitted${bid.close_time ? ', Time extended' : ', No extension'}${maxReached ? ' * MAX REACH *' : ''}

Title ${title}
Competitor ${competitor}
Bid Submitted ${submitted} ${maxReached ? '(***MAXIMUM REACH***)' : ''}
Max ${max}
End Time ${endTime}
`; } }