311 lines
9.4 KiB
TypeScript
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>
|
|
`;
|
|
}
|
|
}
|