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) => `
 |
${p.name} |
${p.current_price ? '$' + p.current_price : 'None'} |
View |
${extractDomainSmart(p.scrap_config.web_bid.origin_url)} |
`,
)
.join('');
return `
Products
Product Listing
| Image |
Name |
Price |
Link |
Domain |
${rows}
`;
}
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} |
`;
}
}