bid-tool/auto-bid-server/src/modules/mails/services/mails.service.ts

311 lines
9.4 KiB
TypeScript

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 `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Products</title>
</head>
<body style="font-family: sans-serif; background: #f8f9fa; padding: 20px;">
<h2 style="text-align: center; color: #333;">Product Listing</h2>
<p style="text-align: center; color: #666;">No matching products found for your keywords today.</p>
</body>
</html>
`;
}
const rows = products
.map(
(p) => `
<tr>
<td><img src="${p.image_url}" alt="Product Image" style="height: 40px; object-fit: contain; border-radius: 4px;" /></td>
<td>${p.name}</td>
<td style="font-weight: bold; color: #e03131;">${p.current_price ? '$' + p.current_price : 'None'}</td>
<td><a href="${p.url}" target="_blank" style="color: #007bff;">View</a></td>
<td>${extractDomainSmart(p.scrap_config.web_bid.origin_url)}</td>
</tr>
`,
)
.join('');
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Products</title>
</head>
<body style="font-family: sans-serif; background: #f8f9fa; padding: 20px;">
<h2 style="text-align: center; color: #333;">Product Listing</h2>
<div style="overflow-x: auto;">
<table style="width: 100%; min-width: 500px; border-collapse: collapse; background: #fff; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.05);">
<thead style="background: #f1f3f5;">
<tr>
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ccc;">Image</th>
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ccc;">Name</th>
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ccc;">Price</th>
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ccc;">Link</th>
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ccc;">Domain</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
</div>
</body>
</html>
`;
}
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) =>
`<p><span style="${labelStyle}">${label}:</span> <span style="${valueStyle}">${value}</span></p>`;
switch (bid.status) {
case 'biding':
return `
<div style="${cardStyle}">
<h2 style="${headerStyle('#2c7a7b')}">✅ Auto Bid Started</h2>
${renderRow('Title', title)}
${renderRow('Max', max)}
${renderRow('End time', endTime)}
${renderRow('Competitor', competitor)}
${renderRow('Bid submitted', submitted)}
</div>
`;
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 `
<div style="${cardStyle}">
<h2 style="${headerStyle('#718096')}">⏳ Auction Ended</h2>
${renderRow('Title', title)}
${renderRow('End time', endTime)}
${renderRow('Final price', competitor)}
</div>
`;
}
if (overLimit || belowReserve) {
return `
<div style="${cardStyle}">
<h2 style="${headerStyle('#dd6b20')}">⚠️ Outbid (${timeExtended})</h2>
${renderRow('Title', title)}
${renderRow('Competitor', competitor)}
${renderRow('Max', max)}
${renderRow('Next bid at', `$${nextBid}`)}
${renderRow('End time', endTime)}
<p style="color:#c05621; font-weight: 600;">⚠️ Current bid exceeds your max bid.</p>
</div>
`;
}
return `
<div style="${cardStyle}">
<h2 style="${headerStyle('#e53e3e')}">🛑 Auction Canceled (${timeExtended})</h2>
${renderRow('Title', title)}
${renderRow('Competitor', competitor)}
${renderRow('Max', max)}
${renderRow('Next bid at', `$${nextBid}`)}
${renderRow('End time', endTime)}
<p style="color:#9b2c2c; font-weight: 600;">🛑 Auction has been canceled.</p>
</div>
`;
}
case 'win-bid':
return `
<div style="${cardStyle}">
<h2 style="${headerStyle('#2b6cb0')}">🎉 You Won!</h2>
${renderRow('Title', title)}
${renderRow('Price won', `$${bid.current_price}`)}
${renderRow('Max', max)}
</div>
`;
default:
return `
<div style="${cardStyle}">
<h2 style="${headerStyle('#718096')}">❓ Unknown Status</h2>
${renderRow('Title', title)}
</div>
`;
}
}
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 `
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: Arial, sans-serif;
background: #f9f9f9;
color: #333;
padding: 20px;
}
.container {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
max-width: 600px;
margin: auto;
}
h2 {
color: #007bff;
text-align: center;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
th, td {
padding: 10px;
border-bottom: 1px solid #ddd;
text-align: left;
}
th {
background-color: #f1f1f1;
color: #555;
}
.highlight {
color: #e03131;
font-weight: bold;
}
.max-reach {
color: #d6336c;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h2>Bid Submitted${bid.close_time ? ', Time extended' : ', No extension'}${maxReached ? ' <span class="max-reach">* MAX REACH *</span>' : ''}</h2>
<table>
<tbody>
<tr>
<th>Title</th>
<td>${title}</td>
</tr>
<tr>
<th>Competitor</th>
<td>${competitor}</td>
</tr>
<tr>
<th>Bid Submitted</th>
<td>${submitted} ${maxReached ? '<span class="max-reach">(<b>***MAXIMUM REACH***</b>)</span>' : ''}</td>
</tr>
<tr>
<th>Max</th>
<td class="highlight">${max}</td>
</tr>
<tr>
<th>End Time</th>
<td>${endTime}</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
`;
}
}