Readme
This commit is contained in:
@@ -31,6 +31,16 @@ interface ContractAnalysisEmailInput {
|
||||
blockchain?: BlockchainEmailData | null;
|
||||
}
|
||||
|
||||
interface ContractDeadlineEmailInput {
|
||||
to: string;
|
||||
userDisplayName?: string | null;
|
||||
contractId: string;
|
||||
contractTitle: string | null;
|
||||
contractProvider: string | null;
|
||||
contractEndDate: Date;
|
||||
daysUntilExpiration: number;
|
||||
}
|
||||
|
||||
let transporter: nodemailer.Transporter | null = null;
|
||||
let transportMode: "smtp" | "ethereal" | null = null;
|
||||
let hasWarnedMissingEmailConfig = false;
|
||||
@@ -134,6 +144,17 @@ const formatContractLink = (contractId: string): string | null => {
|
||||
return `${baseUrl.replace(/\/$/, "")}/contacts?contract=${contractId}`;
|
||||
};
|
||||
|
||||
const getBaseUrl = (): string | null => {
|
||||
const baseUrl =
|
||||
process.env.NEXT_PUBLIC_APP_URL?.trim() || process.env.APP_URL?.trim();
|
||||
return baseUrl ? baseUrl.replace(/\/$/, "") : null;
|
||||
};
|
||||
|
||||
const getLogoUrl = (): string | null => {
|
||||
const baseUrl = getBaseUrl();
|
||||
return baseUrl ? `${baseUrl}/LexiChain.png` : null;
|
||||
};
|
||||
|
||||
export class EmailService {
|
||||
static async sendContractAnalysisCompletedEmail(
|
||||
input: ContractAnalysisEmailInput,
|
||||
@@ -178,6 +199,7 @@ export class EmailService {
|
||||
input.blueprint.premiumCurrency,
|
||||
);
|
||||
const contractUrl = formatContractLink(input.contractId);
|
||||
const logoUrl = getLogoUrl();
|
||||
const blockchainStatus = input.blockchain
|
||||
? "Registered"
|
||||
: "Not registered (blockchain unavailable or skipped)";
|
||||
@@ -185,7 +207,7 @@ export class EmailService {
|
||||
const textBody = [
|
||||
`Hello ${recipientName},`,
|
||||
"",
|
||||
"Your contract analysis is complete.",
|
||||
"Your LexiChain contract intelligence report is ready.",
|
||||
"",
|
||||
"Blueprint:",
|
||||
`- Contract title: ${input.contractTitle}`,
|
||||
@@ -197,7 +219,7 @@ export class EmailService {
|
||||
`- End date: ${formatDateValue(input.blueprint.endDate)}`,
|
||||
`- Premium: ${premiumLabel}`,
|
||||
"",
|
||||
"Summary:",
|
||||
"Executive summary:",
|
||||
input.blueprint.summary,
|
||||
"",
|
||||
"Blockchain proof:",
|
||||
@@ -212,46 +234,97 @@ export class EmailService {
|
||||
"",
|
||||
contractUrl ? `Open in app: ${contractUrl}` : "",
|
||||
"",
|
||||
"Keep this email for your records.",
|
||||
"Thank you for trusting LexiChain for secure contract governance.",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
const htmlBody = `
|
||||
<div style="font-family: Arial, sans-serif; line-height: 1.5; color: #0f172a;">
|
||||
<h2 style="margin-bottom: 12px;">Contract Analysis Completed</h2>
|
||||
<p>Hello ${recipientName},</p>
|
||||
<p>Your contract analysis has been completed successfully.</p>
|
||||
<div style="margin:0;padding:0;background:#0f172a;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#0f172a;padding:24px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="640" cellspacing="0" cellpadding="0" style="background:#ffffff;border-radius:20px;overflow:hidden;font-family:Arial, sans-serif;color:#0f172a;">
|
||||
<tr>
|
||||
<td style="padding:24px 28px;background:linear-gradient(135deg,#0f172a 0%,#1d4ed8 100%);color:#ffffff;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="left" style="vertical-align:middle;">
|
||||
${logoUrl ? `<img src="${logoUrl}" alt="LexiChain" width="140" style="display:block;border-radius:10px;" />` : '<strong style="font-size:20px;">LexiChain</strong>'}
|
||||
</td>
|
||||
<td align="right" style="vertical-align:middle;font-size:12px;letter-spacing:1.4px;text-transform:uppercase;opacity:0.85;">
|
||||
Contract Intelligence
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h1 style="margin:20px 0 6px;font-size:26px;">Your contract insight report is ready</h1>
|
||||
<p style="margin:0;font-size:14px;opacity:0.9;">Clear, verified, and ready for action.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<h3 style="margin-top: 24px; margin-bottom: 8px;">Blueprint</h3>
|
||||
<ul>
|
||||
<li><strong>Contract title:</strong> ${input.contractTitle}</li>
|
||||
<li><strong>Original file:</strong> ${input.contractFileName}</li>
|
||||
<li><strong>Type:</strong> ${input.blueprint.type}</li>
|
||||
<li><strong>Provider:</strong> ${input.blueprint.provider ?? "N/A"}</li>
|
||||
<li><strong>Policy number:</strong> ${input.blueprint.policyNumber ?? "N/A"}</li>
|
||||
<li><strong>Start date:</strong> ${formatDateValue(input.blueprint.startDate)}</li>
|
||||
<li><strong>End date:</strong> ${formatDateValue(input.blueprint.endDate)}</li>
|
||||
<li><strong>Premium:</strong> ${premiumLabel}</li>
|
||||
</ul>
|
||||
<tr>
|
||||
<td style="padding:24px 28px;">
|
||||
<p style="margin:0 0 16px;font-size:14px;">Hello ${recipientName},</p>
|
||||
<p style="margin:0 0 20px;font-size:14px;color:#334155;">
|
||||
Your LexiChain analysis is complete. Below is the executive blueprint and proof trace.
|
||||
</p>
|
||||
|
||||
<h3 style="margin-top: 24px; margin-bottom: 8px;">Summary</h3>
|
||||
<p>${input.blueprint.summary.replace(/\n/g, "<br />")}</p>
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:14px;padding:16px;">
|
||||
<tr>
|
||||
<td style="font-size:13px;font-weight:bold;padding-bottom:12px;">Blueprint Summary</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-size:13px;color:#475569;">
|
||||
<div><strong>Contract:</strong> ${input.contractTitle}</div>
|
||||
<div><strong>File:</strong> ${input.contractFileName}</div>
|
||||
<div><strong>Type:</strong> ${input.blueprint.type}</div>
|
||||
<div><strong>Provider:</strong> ${input.blueprint.provider ?? "N/A"}</div>
|
||||
<div><strong>Policy #:</strong> ${input.blueprint.policyNumber ?? "N/A"}</div>
|
||||
<div><strong>Start:</strong> ${formatDateValue(input.blueprint.startDate)}</div>
|
||||
<div><strong>End:</strong> ${formatDateValue(input.blueprint.endDate)}</div>
|
||||
<div><strong>Premium:</strong> ${premiumLabel}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3 style="margin-top: 24px; margin-bottom: 8px;">Blockchain Proof</h3>
|
||||
<ul>
|
||||
<li><strong>Status:</strong> ${blockchainStatus}</li>
|
||||
<li><strong>Document hash:</strong> ${input.blockchain?.documentHash ?? "N/A"}</li>
|
||||
<li><strong>Transaction hash:</strong> ${input.blockchain?.txHash ?? "N/A"}</li>
|
||||
<li><strong>Block number:</strong> ${input.blockchain?.blockNumber ?? "N/A"}</li>
|
||||
<li><strong>Block time:</strong> ${input.blockchain?.blockTimestamp?.toISOString() ?? "N/A"}</li>
|
||||
<li><strong>Network:</strong> ${input.blockchain?.network ?? "N/A"}</li>
|
||||
<li><strong>Contract address:</strong> ${input.blockchain?.contractAddress ?? "N/A"}</li>
|
||||
<li><strong>Explorer URL:</strong> ${input.blockchain?.explorerUrl ? `<a href="${input.blockchain.explorerUrl}" target="_blank" rel="noopener noreferrer">Open transaction</a>` : "N/A"}</li>
|
||||
</ul>
|
||||
<div style="margin:18px 0 8px;font-size:13px;font-weight:bold;">Executive Summary</div>
|
||||
<p style="margin:0 0 18px;font-size:13px;color:#475569;line-height:1.6;">${input.blueprint.summary.replace(/\n/g, "<br />")}</p>
|
||||
|
||||
${contractUrl ? `<p><a href="${contractUrl}">Open this contract in your dashboard</a></p>` : ""}
|
||||
<p style="margin-top: 24px; font-size: 12px; color: #475569;">Keep this email for your records.</p>
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#eef2ff;border:1px solid #c7d2fe;border-radius:14px;padding:16px;">
|
||||
<tr>
|
||||
<td style="font-size:13px;font-weight:bold;padding-bottom:12px;color:#1e3a8a;">Blockchain Proof</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-size:12px;color:#1e3a8a;line-height:1.6;">
|
||||
<div><strong>Status:</strong> ${blockchainStatus}</div>
|
||||
<div><strong>Document hash:</strong> ${input.blockchain?.documentHash ?? "N/A"}</div>
|
||||
<div><strong>Transaction hash:</strong> ${input.blockchain?.txHash ?? "N/A"}</div>
|
||||
<div><strong>Block number:</strong> ${input.blockchain?.blockNumber ?? "N/A"}</div>
|
||||
<div><strong>Block time:</strong> ${input.blockchain?.blockTimestamp?.toISOString() ?? "N/A"}</div>
|
||||
<div><strong>Network:</strong> ${input.blockchain?.network ?? "N/A"}</div>
|
||||
<div><strong>Contract address:</strong> ${input.blockchain?.contractAddress ?? "N/A"}</div>
|
||||
<div><strong>Explorer:</strong> ${input.blockchain?.explorerUrl ? `<a href="${input.blockchain.explorerUrl}" target="_blank" rel="noopener noreferrer" style="color:#1d4ed8;text-decoration:none;">Open transaction</a>` : "N/A"}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
${
|
||||
contractUrl
|
||||
? `
|
||||
<div style="margin-top:22px;">
|
||||
<a href="${contractUrl}" style="display:inline-block;background:#1d4ed8;color:#ffffff;text-decoration:none;padding:12px 18px;border-radius:10px;font-weight:bold;font-size:13px;">Open in LexiChain</a>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
<p style="margin:20px 0 0;font-size:12px;color:#94a3b8;">Precision you can audit. Trust you can prove.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -259,11 +332,14 @@ export class EmailService {
|
||||
from,
|
||||
to: input.to,
|
||||
subject: `Contract analyzed: ${input.contractTitle}`,
|
||||
text: textBody,
|
||||
html: htmlBody,
|
||||
headers: {
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
const previewUrl = nodemailer.getTestMessageUrl(info);
|
||||
const previewUrl =
|
||||
(nodemailer.getTestMessageUrl(info) as string | false) || null;
|
||||
if (previewUrl) {
|
||||
console.log(`📨 Ethereal preview URL: ${previewUrl}`);
|
||||
}
|
||||
@@ -277,4 +353,160 @@ export class EmailService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static async sendContractDeadlineReminderEmail(
|
||||
input: ContractDeadlineEmailInput,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
skipped?: boolean;
|
||||
previewUrl?: string | null;
|
||||
}> {
|
||||
try {
|
||||
const mailer = await getTransporter();
|
||||
if (!mailer) {
|
||||
return {
|
||||
success: false,
|
||||
skipped: true,
|
||||
error: "Email service not configured",
|
||||
};
|
||||
}
|
||||
|
||||
const from =
|
||||
process.env.MAIL_FROM?.trim() ||
|
||||
process.env.EMAIL_USER?.trim() ||
|
||||
(transportMode === "ethereal"
|
||||
? "LexiChain <no-reply@ethereal.email>"
|
||||
: "");
|
||||
if (!from) {
|
||||
warnMissingEmailConfigOnce();
|
||||
return { success: false, skipped: true, error: "MAIL_FROM is missing" };
|
||||
}
|
||||
|
||||
if (!input.to?.trim()) {
|
||||
return {
|
||||
success: false,
|
||||
skipped: true,
|
||||
error: "Recipient email is missing",
|
||||
};
|
||||
}
|
||||
|
||||
const recipientName = input.userDisplayName || "there";
|
||||
const contractUrl = formatContractLink(input.contractId);
|
||||
const logoUrl = getLogoUrl();
|
||||
const endDate = input.contractEndDate.toLocaleDateString();
|
||||
|
||||
const urgencyLabel =
|
||||
input.daysUntilExpiration <= 7
|
||||
? "Urgent"
|
||||
: input.daysUntilExpiration <= 14
|
||||
? "High"
|
||||
: "Planned";
|
||||
|
||||
const textBody = [
|
||||
`Hello ${recipientName},`,
|
||||
"",
|
||||
`Your contract deadline is approaching in ${input.daysUntilExpiration} days.`,
|
||||
"",
|
||||
`Contract: ${input.contractTitle ?? "Untitled contract"}`,
|
||||
`Provider: ${input.contractProvider ?? "N/A"}`,
|
||||
`End date: ${endDate}`,
|
||||
`Urgency: ${urgencyLabel}`,
|
||||
"",
|
||||
contractUrl ? `Open in app: ${contractUrl}` : "",
|
||||
"",
|
||||
"Please review the contract and schedule renewal if needed.",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
const htmlBody = `
|
||||
<div style="margin:0;padding:0;background:#0f172a;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#0f172a;padding:24px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="640" cellspacing="0" cellpadding="0" style="background:#ffffff;border-radius:20px;overflow:hidden;font-family:Arial, sans-serif;color:#0f172a;">
|
||||
<tr>
|
||||
<td style="padding:22px 28px;background:linear-gradient(135deg,#0f172a 0%,#2563eb 100%);color:#ffffff;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="left" style="vertical-align:middle;">
|
||||
${logoUrl ? `<img src="${logoUrl}" alt="LexiChain" width="130" style="display:block;border-radius:10px;" />` : '<strong style="font-size:18px;">LexiChain</strong>'}
|
||||
</td>
|
||||
<td align="right" style="vertical-align:middle;font-size:12px;letter-spacing:1.4px;text-transform:uppercase;opacity:0.85;">
|
||||
Deadline Alert
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h1 style="margin:18px 0 6px;font-size:24px;">Contract renewal reminder</h1>
|
||||
<p style="margin:0;font-size:14px;opacity:0.9;">Stay ahead of critical dates.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="padding:24px 28px;">
|
||||
<p style="margin:0 0 14px;font-size:14px;">Hello ${recipientName},</p>
|
||||
<p style="margin:0 0 18px;font-size:14px;color:#334155;">
|
||||
Your contract deadline is approaching in <strong>${input.daysUntilExpiration} days</strong>.
|
||||
</p>
|
||||
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:14px;padding:16px;">
|
||||
<tr>
|
||||
<td style="font-size:13px;font-weight:bold;padding-bottom:12px;">Deadline Summary</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-size:13px;color:#475569;line-height:1.6;">
|
||||
<div><strong>Contract:</strong> ${input.contractTitle ?? "Untitled contract"}</div>
|
||||
<div><strong>Provider:</strong> ${input.contractProvider ?? "N/A"}</div>
|
||||
<div><strong>End date:</strong> ${endDate}</div>
|
||||
<div><strong>Urgency:</strong> ${urgencyLabel}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
${
|
||||
contractUrl
|
||||
? `
|
||||
<div style="margin-top:20px;">
|
||||
<a href="${contractUrl}" style="display:inline-block;background:#1d4ed8;color:#ffffff;text-decoration:none;padding:12px 18px;border-radius:10px;font-weight:bold;font-size:13px;">Review in LexiChain</a>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
<p style="margin:18px 0 0;font-size:12px;color:#94a3b8;">Plan renewals early to avoid coverage gaps.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const info = await mailer.sendMail({
|
||||
from,
|
||||
to: input.to,
|
||||
subject: `Contract deadline in ${input.daysUntilExpiration} days`,
|
||||
html: htmlBody,
|
||||
headers: {
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
const previewUrl =
|
||||
(nodemailer.getTestMessageUrl(info) as string | false) || null;
|
||||
if (previewUrl) {
|
||||
console.log(`📨 Ethereal preview URL: ${previewUrl}`);
|
||||
}
|
||||
|
||||
return { success: true, previewUrl };
|
||||
} catch (error) {
|
||||
console.error("Failed to send deadline reminder email:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown email error",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user