emails for rental cancelation, rental declined, rental request confirmation, payout received

This commit is contained in:
jackiettran
2025-10-27 13:07:02 -04:00
parent 407c69aa22
commit 502d84a741
17 changed files with 2690 additions and 45 deletions

View File

@@ -55,6 +55,7 @@ const Rental = sequelize.define("Rental", {
type: DataTypes.ENUM(
"pending",
"confirmed",
"declined",
"active",
"completed",
"cancelled",
@@ -98,6 +99,9 @@ const Rental = sequelize.define("Rental", {
cancelledAt: {
type: DataTypes.DATE,
},
declineReason: {
type: DataTypes.TEXT,
},
stripePaymentMethodId: {
type: DataTypes.STRING,
},

View File

@@ -296,12 +296,30 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => {
// Log error but don't fail the request
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Failed to send rental request email", {
error: emailError.message,
error: emailError,
rentalId: rental.id,
ownerId: rentalWithDetails.ownerId,
});
}
// Send rental request confirmation to renter
try {
await emailService.sendRentalRequestConfirmationEmail(rentalWithDetails);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Rental request confirmation sent to renter", {
rentalId: rental.id,
renterId: rentalWithDetails.renterId,
});
} catch (emailError) {
// Log error but don't fail the request
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Failed to send rental request confirmation email", {
error: emailError,
rentalId: rental.id,
renterId: rentalWithDetails.renterId,
});
}
res.status(201).json(rentalWithDetails);
} catch (error) {
res.status(500).json({ error: "Failed to create rental" });
@@ -477,6 +495,105 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
}
});
// Decline rental request (owner only)
router.put("/:id/decline", authenticateToken, async (req, res) => {
try {
const { reason } = req.body;
// Validate that reason is provided
if (!reason || reason.trim() === "") {
return res.status(400).json({
error: "A reason for declining is required",
});
}
const rental = await Rental.findByPk(req.params.id, {
include: [
{ model: Item, as: "item" },
{
model: User,
as: "owner",
attributes: ["id", "username", "firstName", "lastName"],
},
{
model: User,
as: "renter",
attributes: ["id", "username", "firstName", "lastName", "email"],
},
],
});
if (!rental) {
return res.status(404).json({ error: "Rental not found" });
}
// Only owner can decline
if (rental.ownerId !== req.user.id) {
return res
.status(403)
.json({ error: "Only the item owner can decline rental requests" });
}
// Can only decline pending rentals
if (rental.status !== "pending") {
return res.status(400).json({
error: "Can only decline pending rental requests",
});
}
// Update rental status to declined
await rental.update({
status: "declined",
declineReason: reason,
});
const updatedRental = await Rental.findByPk(rental.id, {
include: [
{ model: Item, as: "item" },
{
model: User,
as: "owner",
attributes: ["id", "username", "firstName", "lastName"],
},
{
model: User,
as: "renter",
attributes: ["id", "username", "firstName", "lastName"],
},
],
});
// Send decline notification email to renter
try {
await emailService.sendRentalDeclinedEmail(updatedRental, reason);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Rental decline notification sent to renter", {
rentalId: rental.id,
renterId: updatedRental.renterId,
});
} catch (emailError) {
// Log error but don't fail the request
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Failed to send rental decline email", {
error: emailError,
rentalId: rental.id,
renterId: updatedRental.renterId,
});
}
res.json(updatedRental);
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Error declining rental", {
error: error.message,
stack: error.stack,
rentalId: req.params.id,
userId: req.user.id,
});
res.status(500).json({ error: "Failed to decline rental" });
}
});
// Owner reviews renter
router.post("/:id/review-renter", authenticateToken, async (req, res) => {
try {
@@ -738,10 +855,15 @@ router.post("/:id/cancel", authenticateToken, async (req, res) => {
try {
const { reason } = req.body;
// Validate that reason is provided
if (!reason || !reason.trim()) {
return res.status(400).json({ error: "Cancellation reason is required" });
}
const result = await RefundService.processCancellation(
req.params.id,
req.user.id,
reason
reason.trim()
);
// Return the updated rental with refund information
@@ -761,6 +883,27 @@ router.post("/:id/cancel", authenticateToken, async (req, res) => {
],
});
// Send cancellation notification emails
try {
await emailService.sendRentalCancellationEmails(
updatedRental,
result.refund
);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Cancellation emails sent", {
rentalId: updatedRental.id,
cancelledBy: updatedRental.cancelledBy,
});
} catch (emailError) {
// Log error but don't fail the request
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Failed to send cancellation emails", {
error: emailError.message,
rentalId: updatedRental.id,
cancelledBy: updatedRental.cancelledBy,
});
}
res.json({
rental: updatedRental,
refund: result.refund,

View File

@@ -42,6 +42,11 @@ class EmailService {
"damageReportCS.html",
"lostItemCS.html",
"rentalRequest.html",
"rentalRequestConfirmation.html",
"rentalCancellationConfirmation.html",
"rentalCancellationNotification.html",
"rentalDeclined.html",
"payoutReceived.html",
];
for (const templateFile of templateFiles) {
@@ -50,12 +55,14 @@ class EmailService {
const templateContent = await fs.readFile(templatePath, "utf-8");
const templateName = path.basename(templateFile, ".html");
this.templates.set(templateName, templateContent);
console.log(`✓ Loaded template: ${templateName}`);
} catch (error) {
console.warn(`Template ${templateFile} not found, will use fallback`);
console.error(`✗ Failed to load template ${templateFile}:`, error.message);
console.error(` Template path: ${path.join(templatesDir, templateFile)}`);
}
}
console.log(`Loaded ${this.templates.size} email templates`);
console.log(`Loaded ${this.templates.size} of ${templateFiles.length} email templates`);
} catch (error) {
console.warn("Templates directory not found, using fallback templates");
}
@@ -290,6 +297,93 @@ class EmailService {
<p>Please respond to this request within 24 hours.</p>
`
),
rentalRequestConfirmation: baseTemplate.replace(
"{{content}}",
`
<p>Hi {{renterName}},</p>
<h2>Your Rental Request Has Been Submitted!</h2>
<p>Your request to rent <strong>{{itemName}}</strong> has been sent to the owner.</p>
<p><strong>Item:</strong> {{itemName}}</p>
<p><strong>Rental Period:</strong> {{startDate}} to {{endDate}}</p>
<p><strong>Delivery Method:</strong> {{deliveryMethod}}</p>
<p><strong>Total Amount:</strong> \${{totalAmount}}</p>
<p>{{paymentMessage}}</p>
<p>You'll receive an email notification once the owner responds to your request.</p>
<p><a href="{{viewRentalsUrl}}" class="button">View My Rentals</a></p>
`
),
rentalCancellationConfirmation: baseTemplate.replace(
"{{content}}",
`
<p>Hi {{recipientName}},</p>
<h2>Rental Cancelled Successfully</h2>
<p>This confirms that your rental for <strong>{{itemName}}</strong> has been cancelled.</p>
<p><strong>Item:</strong> {{itemName}}</p>
<p><strong>Start Date:</strong> {{startDate}}</p>
<p><strong>End Date:</strong> {{endDate}}</p>
<p><strong>Cancelled On:</strong> {{cancelledAt}}</p>
{{refundSection}}
`
),
rentalCancellationNotification: baseTemplate.replace(
"{{content}}",
`
<p>Hi {{recipientName}},</p>
<h2>Rental Cancellation Notice</h2>
<p>{{cancellationMessage}}</p>
<p><strong>Item:</strong> {{itemName}}</p>
<p><strong>Start Date:</strong> {{startDate}}</p>
<p><strong>End Date:</strong> {{endDate}}</p>
<p><strong>Cancelled On:</strong> {{cancelledAt}}</p>
{{additionalInfo}}
<p>If you have any questions or concerns, please reach out to our support team.</p>
`
),
payoutReceived: baseTemplate.replace(
"{{content}}",
`
<p>Hi {{ownerName}},</p>
<h2 style="color: #28a745;">Earnings Received: \${{payoutAmount}}</h2>
<p>Great news! Your earnings from the rental of <strong>{{itemName}}</strong> have been transferred to your account.</p>
<h3>Rental Details</h3>
<p><strong>Item:</strong> {{itemName}}</p>
<p><strong>Rental Period:</strong> {{startDate}} to {{endDate}}</p>
<p><strong>Transfer ID:</strong> {{stripeTransferId}}</p>
<h3>Earnings Breakdown</h3>
<p><strong>Rental Amount:</strong> \${{totalAmount}}</p>
<p><strong>Platform Fee (20%):</strong> -\${{platformFee}}</p>
<p style="font-size: 18px; color: #28a745;"><strong>Your Earnings:</strong> \${{payoutAmount}}</p>
<p>Funds are typically available in your bank account within 2-3 business days.</p>
<p><a href="{{earningsDashboardUrl}}" class="button">View Earnings Dashboard</a></p>
<p>Thank you for being a valued member of the RentAll community!</p>
`
),
rentalDeclined: baseTemplate.replace(
"{{content}}",
`
<p>Hi {{renterName}},</p>
<h2>Rental Request Declined</h2>
<p>Thank you for your interest in renting <strong>{{itemName}}</strong>. Unfortunately, the owner is unable to accept your rental request at this time.</p>
<h3>Request Details</h3>
<p><strong>Item:</strong> {{itemName}}</p>
<p><strong>Start Date:</strong> {{startDate}}</p>
<p><strong>End Date:</strong> {{endDate}}</p>
<p><strong>Delivery Method:</strong> {{deliveryMethod}}</p>
{{ownerMessage}}
<div class="info-box">
<p><strong>What happens next?</strong></p>
<p>{{paymentMessage}}</p>
<p>We encourage you to explore other similar items available for rent on RentAll. There are many great options waiting for you!</p>
</div>
<p style="text-align: center;"><a href="{{browseItemsUrl}}" class="button">Browse Available Items</a></p>
<p>If you have any questions or concerns, please don't hesitate to contact our support team.</p>
`
),
};
return (
@@ -456,6 +550,7 @@ class EmailService {
? parseFloat(rental.payoutAmount).toFixed(2)
: "0.00",
deliveryMethod: rental.deliveryMethod || "Not specified",
rentalNotes: rental.notes || "No additional notes provided",
approveUrl: approveUrl,
};
@@ -468,6 +563,382 @@ class EmailService {
);
}
async sendRentalRequestConfirmationEmail(rental) {
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const viewRentalsUrl = `${frontendUrl}/my-rentals`;
// Fetch renter details
const renter = await User.findByPk(rental.renterId, {
attributes: ["email", "firstName", "lastName"],
});
if (!renter) {
console.error(
"Renter not found for rental request confirmation notification"
);
return { success: false, error: "Renter not found" };
}
// Determine payment message based on rental amount
const totalAmount = parseFloat(rental.totalAmount) || 0;
const paymentMessage =
totalAmount > 0
? "The owner will review your request. You'll only be charged if they approve it."
: "The owner will review your request and respond soon.";
const variables = {
renterName: renter.firstName || "there",
itemName: rental.item?.name || "the item",
startDate: rental.startDateTime
? new Date(rental.startDateTime).toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
})
: "Not specified",
endDate: rental.endDateTime
? new Date(rental.endDateTime).toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
})
: "Not specified",
totalAmount: totalAmount.toFixed(2),
deliveryMethod: rental.deliveryMethod || "Not specified",
paymentMessage: paymentMessage,
viewRentalsUrl: viewRentalsUrl,
};
const htmlContent = this.renderTemplate(
"rentalRequestConfirmation",
variables
);
return await this.sendEmail(
renter.email,
`Rental Request Submitted - ${rental.item?.name || "Item"}`,
htmlContent
);
}
async sendRentalDeclinedEmail(rental, declineReason) {
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const browseItemsUrl = `${frontendUrl}/`;
// Fetch renter details
const renter = await User.findByPk(rental.renterId, {
attributes: ["email", "firstName", "lastName"],
});
if (!renter) {
console.error(
"Renter not found for rental decline notification"
);
return { success: false, error: "Renter not found" };
}
// Determine payment message based on rental amount
const totalAmount = parseFloat(rental.totalAmount) || 0;
const paymentMessage =
totalAmount > 0
? "Since your request was declined before payment was processed, you will not be charged."
: "No payment was required for this rental request.";
// Build owner message section if decline reason provided
const ownerMessage = declineReason
? `
<div class="notice-box">
<p><strong>Message from the owner:</strong></p>
<p>${declineReason}</p>
</div>
`
: "";
const variables = {
renterName: renter.firstName || "there",
itemName: rental.item?.name || "the item",
startDate: rental.startDateTime
? new Date(rental.startDateTime).toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
})
: "Not specified",
endDate: rental.endDateTime
? new Date(rental.endDateTime).toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
})
: "Not specified",
deliveryMethod: rental.deliveryMethod || "Not specified",
paymentMessage: paymentMessage,
ownerMessage: ownerMessage,
browseItemsUrl: browseItemsUrl,
payoutAmount: rental.payoutAmount
? parseFloat(rental.payoutAmount).toFixed(2)
: "0.00",
totalAmount: totalAmount.toFixed(2),
};
const htmlContent = this.renderTemplate(
"rentalDeclined",
variables
);
return await this.sendEmail(
renter.email,
`Rental Request Declined - ${rental.item?.name || "Item"}`,
htmlContent
);
}
async sendPayoutReceivedEmail(rental) {
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const earningsDashboardUrl = `${frontendUrl}/earnings`;
// Fetch owner details
const owner = await User.findByPk(rental.ownerId, {
attributes: ["email", "firstName", "lastName"],
});
if (!owner) {
console.error("Owner not found for payout notification");
return { success: false, error: "Owner not found" };
}
// Format currency values
const totalAmount = parseFloat(rental.totalAmount) || 0;
const platformFee = parseFloat(rental.platformFee) || 0;
const payoutAmount = parseFloat(rental.payoutAmount) || 0;
const variables = {
ownerName: owner.firstName || "there",
itemName: rental.item?.name || "your item",
startDate: rental.startDateTime
? new Date(rental.startDateTime).toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
})
: "Not specified",
endDate: rental.endDateTime
? new Date(rental.endDateTime).toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
})
: "Not specified",
totalAmount: totalAmount.toFixed(2),
platformFee: platformFee.toFixed(2),
payoutAmount: payoutAmount.toFixed(2),
stripeTransferId: rental.stripeTransferId || "N/A",
earningsDashboardUrl: earningsDashboardUrl,
};
const htmlContent = this.renderTemplate("payoutReceived", variables);
return await this.sendEmail(
owner.email,
`Earnings Received - $${payoutAmount.toFixed(2)} for ${rental.item?.name || "Your Item"}`,
htmlContent
);
}
async sendRentalCancellationEmails(rental, refundInfo) {
const results = {
confirmationEmailSent: false,
notificationEmailSent: false,
};
try {
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const browseUrl = `${frontendUrl}/`;
// Fetch both owner and renter details
const owner = await User.findByPk(rental.ownerId, {
attributes: ["email", "firstName", "lastName"],
});
const renter = await User.findByPk(rental.renterId, {
attributes: ["email", "firstName", "lastName"],
});
if (!owner || !renter) {
console.error(
"Owner or renter not found for rental cancellation emails"
);
return { success: false, error: "User not found" };
}
const cancelledBy = rental.cancelledBy; // 'owner' or 'renter'
const itemName = rental.item?.name || "the item";
const startDate = rental.startDateTime
? new Date(rental.startDateTime).toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
})
: "Not specified";
const endDate = rental.endDateTime
? new Date(rental.endDateTime).toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
})
: "Not specified";
const cancelledAt = rental.cancelledAt
? new Date(rental.cancelledAt).toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
})
: "Not specified";
// Determine who gets confirmation and who gets notification
let confirmationRecipient, notificationRecipient;
let confirmationRecipientName, notificationRecipientName;
let cancellationMessage, additionalInfo;
if (cancelledBy === "owner") {
// Owner cancelled: owner gets confirmation, renter gets notification
confirmationRecipient = owner.email;
confirmationRecipientName = owner.firstName || "there";
notificationRecipient = renter.email;
notificationRecipientName = renter.firstName || "there";
cancellationMessage = `The owner has cancelled the rental for <strong>${itemName}</strong>. We apologize for any inconvenience this may cause.`;
// Only show refund info if rental had a cost
if (rental.totalAmount > 0) {
additionalInfo = `
<div class="info-box">
<p><strong>Full Refund Processed</strong></p>
<p>You will receive a full refund of $${refundInfo.amount.toFixed(2)}. The refund will appear in your account within 5-10 business days.</p>
</div>
<div style="text-align: center">
<a href="${browseUrl}" class="button">Browse Other Items</a>
</div>
`;
} else {
additionalInfo = `
<div class="info-box">
<p>This rental has been cancelled by the owner. We apologize for any inconvenience.</p>
</div>
<div style="text-align: center">
<a href="${browseUrl}" class="button">Browse Other Items</a>
</div>
`;
}
} else {
// Renter cancelled: renter gets confirmation, owner gets notification
confirmationRecipient = renter.email;
confirmationRecipientName = renter.firstName || "there";
notificationRecipient = owner.email;
notificationRecipientName = owner.firstName || "there";
cancellationMessage = `The renter has cancelled their rental for <strong>${itemName}</strong>.`;
additionalInfo = `
<div class="info-box">
<p><strong>Your item is now available</strong></p>
<p>Your item is now available for other renters to book for these dates.</p>
</div>
`;
}
// Build refund section for confirmation email (only for paid rentals)
let refundSection = "";
if (rental.totalAmount > 0) {
if (refundInfo.amount > 0) {
const refundPercentage = (refundInfo.percentage * 100).toFixed(0);
refundSection = `
<h2>Refund Information</h2>
<div class="refund-amount">$${refundInfo.amount.toFixed(2)}</div>
<div class="info-box">
<p><strong>Refund Amount:</strong> $${refundInfo.amount.toFixed(2)} (${refundPercentage}% of total)</p>
<p><strong>Reason:</strong> ${refundInfo.reason}</p>
<p><strong>Processing Time:</strong> Refunds typically appear within 5-10 business days.</p>
</div>
`;
} else {
refundSection = `
<h2>Refund Information</h2>
<div class="warning-box">
<p><strong>No Refund Available</strong></p>
<p>${refundInfo.reason}</p>
</div>
`;
}
}
// For free rentals (totalAmount = 0), refundSection stays empty
// Send confirmation email to canceller
try {
const confirmationVariables = {
recipientName: confirmationRecipientName,
itemName: itemName,
startDate: startDate,
endDate: endDate,
cancelledAt: cancelledAt,
refundSection: refundSection,
};
const confirmationHtml = this.renderTemplate(
"rentalCancellationConfirmation",
confirmationVariables
);
const confirmationResult = await this.sendEmail(
confirmationRecipient,
`Cancellation Confirmed - ${itemName}`,
confirmationHtml
);
if (confirmationResult.success) {
console.log(
`Cancellation confirmation email sent to ${cancelledBy}: ${confirmationRecipient}`
);
results.confirmationEmailSent = true;
}
} catch (error) {
console.error(
`Failed to send cancellation confirmation email to ${cancelledBy}:`,
error.message
);
}
// Send notification email to other party
try {
const notificationVariables = {
recipientName: notificationRecipientName,
itemName: itemName,
startDate: startDate,
endDate: endDate,
cancelledAt: cancelledAt,
cancellationMessage: cancellationMessage,
additionalInfo: additionalInfo,
};
const notificationHtml = this.renderTemplate(
"rentalCancellationNotification",
notificationVariables
);
const notificationResult = await this.sendEmail(
notificationRecipient,
`Rental Cancelled - ${itemName}`,
notificationHtml
);
if (notificationResult.success) {
console.log(
`Cancellation notification email sent to ${cancelledBy === "owner" ? "renter" : "owner"}: ${notificationRecipient}`
);
results.notificationEmailSent = true;
}
} catch (error) {
console.error(
`Failed to send cancellation notification email:`,
error.message
);
}
} catch (error) {
console.error("Error sending cancellation emails:", error);
}
return results;
}
async sendTemplateEmail(toEmail, subject, templateName, variables = {}) {
const htmlContent = this.renderTemplate(templateName, variables);
return await this.sendEmail(toEmail, subject, htmlContent);

View File

@@ -1,5 +1,6 @@
const { Rental, User } = require("../models");
const { Rental, User, Item } = require("../models");
const StripeService = require("./stripeService");
const emailService = require("./emailService");
const { Op } = require("sequelize");
class PayoutService {
@@ -21,6 +22,10 @@ class PayoutService {
},
},
},
{
model: Item,
as: "item",
},
],
});
@@ -75,6 +80,20 @@ class PayoutService {
`Payout completed for rental ${rental.id}: $${rental.payoutAmount} to ${rental.owner.stripeConnectedAccountId}`
);
// Send payout notification email to owner
try {
await emailService.sendPayoutReceivedEmail(rental);
console.log(
`Payout notification email sent to owner for rental ${rental.id}`
);
} catch (emailError) {
// Log error but don't fail the payout
console.error(
`Failed to send payout notification email for rental ${rental.id}:`,
emailError.message
);
}
return {
success: true,
transferId: transfer.id,
@@ -151,6 +170,10 @@ class PayoutService {
},
},
},
{
model: Item,
as: "item",
},
],
});

View File

@@ -0,0 +1,419 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Earnings Received - RentAll</title>
<style>
/* Reset styles */
body,
table,
td,
p,
a,
li,
blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
/* Base styles */
body {
margin: 0;
padding: 0;
width: 100% !important;
min-width: 100%;
height: 100%;
background-color: #f8f9fa;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #212529;
}
/* Container */
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Header */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px 30px;
text-align: center;
}
.logo {
font-size: 32px;
font-weight: 700;
color: #ffffff;
text-decoration: none;
letter-spacing: -1px;
}
.tagline {
color: #e9ecef;
font-size: 14px;
margin-top: 8px;
}
/* Content */
.content {
padding: 40px 30px;
}
.content h1 {
font-size: 24px;
font-weight: 600;
margin: 0 0 20px 0;
color: #212529;
}
.content h2 {
font-size: 20px;
font-weight: 600;
margin: 30px 0 15px 0;
color: #495057;
}
.content p {
margin: 0 0 16px 0;
color: #6c757d;
line-height: 1.6;
}
.content strong {
color: #495057;
}
/* Earnings amount display */
.earnings-display {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
padding: 30px;
border-radius: 8px;
text-align: center;
margin: 30px 0;
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
}
.earnings-label {
color: #ffffff;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 10px;
opacity: 0.9;
}
.earnings-amount {
color: #ffffff;
font-size: 48px;
font-weight: 700;
margin: 0;
line-height: 1;
}
.earnings-subtitle {
color: #ffffff;
font-size: 14px;
margin-top: 10px;
opacity: 0.9;
}
/* Button */
.button {
display: inline-block;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff !important;
text-decoration: none;
padding: 16px 32px;
border-radius: 6px;
font-weight: 600;
margin: 20px 0;
text-align: center;
transition: all 0.3s ease;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
/* Success box */
.success-box {
background-color: #d4edda;
border-left: 4px solid #28a745;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.success-box p {
margin: 0 0 10px 0;
color: #155724;
}
.success-box p:last-child {
margin-bottom: 0;
}
/* Info box */
.info-box {
background-color: #e7f3ff;
border-left: 4px solid #0066cc;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.info-box p {
margin: 0 0 10px 0;
color: #004085;
}
.info-box p:last-child {
margin-bottom: 0;
}
/* Info table */
.info-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background-color: #f8f9fa;
border-radius: 6px;
overflow: hidden;
}
.info-table th,
.info-table td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #dee2e6;
}
.info-table th {
background-color: #e9ecef;
font-weight: 600;
color: #495057;
width: 40%;
}
.info-table td {
color: #6c757d;
}
.info-table tr:last-child td {
border-bottom: none;
}
/* Breakdown table with emphasis */
.breakdown-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background-color: #ffffff;
border: 1px solid #dee2e6;
border-radius: 6px;
overflow: hidden;
}
.breakdown-table td {
padding: 12px 15px;
border-bottom: 1px solid #e9ecef;
}
.breakdown-table tr:last-child td {
border-bottom: none;
}
.breakdown-label {
color: #6c757d;
font-size: 14px;
}
.breakdown-amount {
color: #495057;
font-weight: 600;
text-align: right;
}
.breakdown-earnings {
background-color: #d4edda;
font-weight: 700;
}
.breakdown-earnings .breakdown-label {
color: #155724;
font-size: 16px;
}
.breakdown-earnings .breakdown-amount {
color: #155724;
font-size: 18px;
}
/* Footer */
.footer {
background-color: #f8f9fa;
padding: 30px;
text-align: center;
border-top: 1px solid #e9ecef;
}
.footer p {
margin: 0 0 10px 0;
font-size: 14px;
color: #6c757d;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* Responsive */
@media only screen and (max-width: 600px) {
.email-container {
margin: 0;
border-radius: 0;
}
.header,
.content,
.footer {
padding: 20px;
}
.logo {
font-size: 28px;
}
.content h1 {
font-size: 22px;
}
.earnings-amount {
font-size: 36px;
}
.button {
display: block;
width: 100%;
box-sizing: border-box;
}
.info-table th,
.info-table td {
padding: 10px;
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<div class="logo">RentAll</div>
<div class="tagline">Earnings Received</div>
</div>
<div class="content">
<p>Hi {{ownerName}},</p>
<p>Great news! Your earnings have been transferred to your account.</p>
<div class="earnings-display">
<div class="earnings-label">You Earned</div>
<div class="earnings-amount">${{payoutAmount}}</div>
<div class="earnings-subtitle">
From rental of {{itemName}}
</div>
</div>
<h2>Rental Details</h2>
<table class="info-table">
<tr>
<th>Item Rented</th>
<td>{{itemName}}</td>
</tr>
<tr>
<th>Rental Period</th>
<td>{{startDate}} to {{endDate}}</td>
</tr>
<tr>
<th>Transfer ID</th>
<td>{{stripeTransferId}}</td>
</tr>
</table>
<h2>Earnings Breakdown</h2>
<table class="breakdown-table">
<tr>
<td class="breakdown-label">Rental Amount (charged to renter)</td>
<td class="breakdown-amount">${{totalAmount}}</td>
</tr>
<tr>
<td class="breakdown-label">Platform Fee (20%)</td>
<td class="breakdown-amount">-${{platformFee}}</td>
</tr>
<tr class="breakdown-earnings">
<td class="breakdown-label">Your Earnings</td>
<td class="breakdown-amount">${{payoutAmount}}</td>
</tr>
</table>
<div class="info-box">
<p><strong>When will I receive the funds?</strong></p>
<p>
Funds are typically available in your bank account within
<strong>2-3 business days</strong> from the transfer date.
</p>
<p>
You can track this transfer in your Stripe Dashboard using the
Transfer ID above.
</p>
</div>
<div style="text-align: center">
<a href="{{earningsDashboardUrl}}" class="button"
>View Earnings Dashboard</a
>
</div>
<p>
Thank you for being a valued member of the RentAll community! Keep
sharing your items to earn more.
</p>
</div>
<div class="footer">
<p><strong>RentAll</strong></p>
<p>
This is a notification about your earnings. You received this message
because a payout was successfully processed for your rental.
</p>
<p>If you have any questions, please contact our support team.</p>
<p>&copy; 2024 RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,311 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Cancellation Confirmed - RentAll</title>
<style>
/* Reset styles */
body,
table,
td,
p,
a,
li,
blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
/* Base styles */
body {
margin: 0;
padding: 0;
width: 100% !important;
min-width: 100%;
height: 100%;
background-color: #f8f9fa;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #212529;
}
/* Container */
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Header */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px 30px;
text-align: center;
}
.logo {
font-size: 32px;
font-weight: 700;
color: #ffffff;
text-decoration: none;
letter-spacing: -1px;
}
.tagline {
color: #e9ecef;
font-size: 14px;
margin-top: 8px;
}
/* Content */
.content {
padding: 40px 30px;
}
.content h1 {
font-size: 24px;
font-weight: 600;
margin: 0 0 20px 0;
color: #212529;
}
.content h2 {
font-size: 20px;
font-weight: 600;
margin: 30px 0 15px 0;
color: #495057;
}
.content p {
margin: 0 0 16px 0;
color: #6c757d;
line-height: 1.6;
}
.content strong {
color: #495057;
}
/* Success box */
.success-box {
background-color: #d4edda;
border-left: 4px solid #28a745;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.success-box p {
margin: 0;
color: #155724;
font-size: 16px;
font-weight: 600;
}
/* Info box */
.info-box {
background-color: #e7f3ff;
border-left: 4px solid #2196f3;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.info-box p {
margin: 0 0 10px 0;
color: #004085;
}
.info-box p:last-child {
margin-bottom: 0;
}
/* Warning box */
.warning-box {
background-color: #fff3cd;
border-left: 4px solid #ffc107;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.warning-box p {
margin: 0 0 10px 0;
color: #856404;
}
.warning-box p:last-child {
margin-bottom: 0;
}
/* Info table */
.info-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background-color: #f8f9fa;
border-radius: 6px;
overflow: hidden;
}
.info-table th,
.info-table td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #dee2e6;
}
.info-table th {
background-color: #e9ecef;
font-weight: 600;
color: #495057;
}
.info-table td {
color: #6c757d;
}
.info-table tr:last-child td {
border-bottom: none;
}
/* Refund highlight */
.refund-amount {
font-size: 28px;
font-weight: 700;
color: #28a745;
text-align: center;
margin: 20px 0;
}
/* Footer */
.footer {
background-color: #f8f9fa;
padding: 30px;
text-align: center;
border-top: 1px solid #e9ecef;
}
.footer p {
margin: 0 0 10px 0;
font-size: 14px;
color: #6c757d;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* Responsive */
@media only screen and (max-width: 600px) {
.email-container {
margin: 0;
border-radius: 0;
}
.header,
.content,
.footer {
padding: 20px;
}
.logo {
font-size: 28px;
}
.content h1 {
font-size: 22px;
}
.info-table th,
.info-table td {
padding: 10px;
font-size: 14px;
}
.refund-amount {
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<div class="logo">RentAll</div>
<div class="tagline">Cancellation Confirmation</div>
</div>
<div class="content">
<p>Hi {{recipientName}},</p>
<div class="success-box">
<p>Your cancellation has been confirmed</p>
</div>
<h1>Rental Cancelled Successfully</h1>
<p>
This confirms that your rental for <strong>{{itemName}}</strong> has
been cancelled.
</p>
<h2>Cancelled Rental Details</h2>
<table class="info-table">
<tr>
<th>Item</th>
<td>{{itemName}}</td>
</tr>
<tr>
<th>Start Date</th>
<td>{{startDate}}</td>
</tr>
<tr>
<th>End Date</th>
<td>{{endDate}}</td>
</tr>
<tr>
<th>Cancelled On</th>
<td>{{cancelledAt}}</td>
</tr>
</table>
{{refundSection}}
<p>
If you have any questions about this cancellation, please don't
hesitate to contact our support team.
</p>
</div>
<div class="footer">
<p><strong>RentAll</strong></p>
<p>
This is a confirmation of your rental cancellation. You received this
message because you cancelled a rental on RentAll.
</p>
<p>If you have any questions, please contact our support team.</p>
<p>&copy; 2024 RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,310 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Rental Cancelled - RentAll</title>
<style>
/* Reset styles */
body,
table,
td,
p,
a,
li,
blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
/* Base styles */
body {
margin: 0;
padding: 0;
width: 100% !important;
min-width: 100%;
height: 100%;
background-color: #f8f9fa;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #212529;
}
/* Container */
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Header */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px 30px;
text-align: center;
}
.logo {
font-size: 32px;
font-weight: 700;
color: #ffffff;
text-decoration: none;
letter-spacing: -1px;
}
.tagline {
color: #e9ecef;
font-size: 14px;
margin-top: 8px;
}
/* Content */
.content {
padding: 40px 30px;
}
.content h1 {
font-size: 24px;
font-weight: 600;
margin: 0 0 20px 0;
color: #212529;
}
.content h2 {
font-size: 20px;
font-weight: 600;
margin: 30px 0 15px 0;
color: #495057;
}
.content p {
margin: 0 0 16px 0;
color: #6c757d;
line-height: 1.6;
}
.content strong {
color: #495057;
}
.content ul {
margin: 16px 0;
padding-left: 24px;
color: #6c757d;
}
.content li {
margin-bottom: 8px;
}
/* Button */
.button {
display: inline-block;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff !important;
text-decoration: none;
padding: 16px 32px;
border-radius: 6px;
font-weight: 600;
margin: 20px 0;
text-align: center;
transition: all 0.3s ease;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
/* Info box */
.info-box {
background-color: #e7f3ff;
border-left: 4px solid #2196f3;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.info-box p {
margin: 0 0 10px 0;
color: #004085;
}
.info-box p:last-child {
margin-bottom: 0;
}
/* Warning box */
.warning-box {
background-color: #fff3cd;
border-left: 4px solid #ffc107;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.warning-box p {
margin: 0 0 10px 0;
color: #856404;
}
.warning-box p:last-child {
margin-bottom: 0;
}
/* Info table */
.info-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background-color: #f8f9fa;
border-radius: 6px;
overflow: hidden;
}
.info-table th,
.info-table td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #dee2e6;
}
.info-table th {
background-color: #e9ecef;
font-weight: 600;
color: #495057;
}
.info-table td {
color: #6c757d;
}
.info-table tr:last-child td {
border-bottom: none;
}
/* Footer */
.footer {
background-color: #f8f9fa;
padding: 30px;
text-align: center;
border-top: 1px solid #e9ecef;
}
.footer p {
margin: 0 0 10px 0;
font-size: 14px;
color: #6c757d;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* Responsive */
@media only screen and (max-width: 600px) {
.email-container {
margin: 0;
border-radius: 0;
}
.header,
.content,
.footer {
padding: 20px;
}
.logo {
font-size: 28px;
}
.content h1 {
font-size: 22px;
}
.button {
display: block;
width: 100%;
box-sizing: border-box;
}
.info-table th,
.info-table td {
padding: 10px;
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<div class="logo">RentAll</div>
<div class="tagline">Rental Update</div>
</div>
<div class="content">
<p>Hi {{recipientName}},</p>
<h1>Rental Cancellation Notice</h1>
<p>{{cancellationMessage}}</p>
<h2>Cancelled Rental Details</h2>
<table class="info-table">
<tr>
<th>Item</th>
<td>{{itemName}}</td>
</tr>
<tr>
<th>Start Date</th>
<td>{{startDate}}</td>
</tr>
<tr>
<th>End Date</th>
<td>{{endDate}}</td>
</tr>
<tr>
<th>Cancelled On</th>
<td>{{cancelledAt}}</td>
</tr>
</table>
{{additionalInfo}}
<p>
We understand this may be inconvenient. If you have any questions or
concerns, please don't hesitate to reach out to our support team.
</p>
</div>
<div class="footer">
<p><strong>RentAll</strong></p>
<p>
This is a notification about a rental cancellation. You received this
message because you were involved in a rental on RentAll.
</p>
<p>If you have any questions, please contact our support team.</p>
<p>&copy; 2024 RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,317 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Rental Request Declined - RentAll</title>
<style>
/* Reset styles */
body,
table,
td,
p,
a,
li,
blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
/* Base styles */
body {
margin: 0;
padding: 0;
width: 100% !important;
min-width: 100%;
height: 100%;
background-color: #f8f9fa;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #212529;
}
/* Container */
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Header */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px 30px;
text-align: center;
}
.logo {
font-size: 32px;
font-weight: 700;
color: #ffffff;
text-decoration: none;
letter-spacing: -1px;
}
.tagline {
color: #e9ecef;
font-size: 14px;
margin-top: 8px;
}
/* Content */
.content {
padding: 40px 30px;
}
.content h1 {
font-size: 24px;
font-weight: 600;
margin: 0 0 20px 0;
color: #212529;
}
.content h2 {
font-size: 20px;
font-weight: 600;
margin: 30px 0 15px 0;
color: #495057;
}
.content p {
margin: 0 0 16px 0;
color: #6c757d;
line-height: 1.6;
}
.content strong {
color: #495057;
}
/* Button */
.button {
display: inline-block;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff !important;
text-decoration: none;
padding: 16px 32px;
border-radius: 6px;
font-weight: 600;
margin: 20px 0;
text-align: center;
transition: all 0.3s ease;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
/* Info box */
.info-box {
background-color: #e7f3ff;
border-left: 4px solid #0066cc;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.info-box p {
margin: 0 0 10px 0;
color: #004085;
}
.info-box p:last-child {
margin-bottom: 0;
}
/* Notice box */
.notice-box {
background-color: #fff3cd;
border-left: 4px solid #ffc107;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.notice-box p {
margin: 0 0 10px 0;
color: #856404;
}
.notice-box p:last-child {
margin-bottom: 0;
}
/* Info table */
.info-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background-color: #f8f9fa;
border-radius: 6px;
overflow: hidden;
}
.info-table th,
.info-table td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #dee2e6;
}
.info-table th {
background-color: #e9ecef;
font-weight: 600;
color: #495057;
}
.info-table td {
color: #6c757d;
}
.info-table tr:last-child td {
border-bottom: none;
}
/* Footer */
.footer {
background-color: #f8f9fa;
padding: 30px;
text-align: center;
border-top: 1px solid #e9ecef;
}
.footer p {
margin: 0 0 10px 0;
font-size: 14px;
color: #6c757d;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* Responsive */
@media only screen and (max-width: 600px) {
.email-container {
margin: 0;
border-radius: 0;
}
.header,
.content,
.footer {
padding: 20px;
}
.logo {
font-size: 28px;
}
.content h1 {
font-size: 22px;
}
.button {
display: block;
width: 100%;
box-sizing: border-box;
}
.info-table th,
.info-table td {
padding: 10px;
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<div class="logo">RentAll</div>
<div class="tagline">Rental Request Update</div>
</div>
<div class="content">
<p>Hi {{renterName}},</p>
<p>
Thank you for your interest in renting <strong>{{itemName}}</strong>.
Unfortunately, the owner is unable to accept your rental request at
this time.
</p>
<h2>Request Details</h2>
<table class="info-table">
<tr>
<th>Item</th>
<td>{{itemName}}</td>
</tr>
<tr>
<th>Start Date</th>
<td>{{startDate}}</td>
</tr>
<tr>
<th>End Date</th>
<td>{{endDate}}</td>
</tr>
<tr>
<th>Delivery Method</th>
<td>{{deliveryMethod}}</td>
</tr>
</table>
{{ownerMessage}}
<div class="info-box">
<p><strong>What happens next?</strong></p>
<p>
{{paymentMessage}}
</p>
<p>
We encourage you to explore other similar items available for rent
on RentAll. There are many great options waiting for you!
</p>
</div>
<div style="text-align: center">
<a href="{{browseItemsUrl}}" class="button">Browse Available Items</a>
</div>
<p>
If you have any questions or concerns, please don't hesitate to
contact our support team.
</p>
</div>
<div class="footer">
<p><strong>RentAll</strong></p>
<p>
This is a notification about your rental request. You received this
message because you submitted a rental request on RentAll.
</p>
<p>If you have any questions, please contact our support team.</p>
<p>&copy; 2024 RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,320 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Rental Request Submitted - RentAll</title>
<style>
/* Reset styles */
body,
table,
td,
p,
a,
li,
blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
/* Base styles */
body {
margin: 0;
padding: 0;
width: 100% !important;
min-width: 100%;
height: 100%;
background-color: #f8f9fa;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #212529;
}
/* Container */
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Header */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px 30px;
text-align: center;
}
.logo {
font-size: 32px;
font-weight: 700;
color: #ffffff;
text-decoration: none;
letter-spacing: -1px;
}
.tagline {
color: #e9ecef;
font-size: 14px;
margin-top: 8px;
}
/* Content */
.content {
padding: 40px 30px;
}
.content h1 {
font-size: 24px;
font-weight: 600;
margin: 0 0 20px 0;
color: #212529;
}
.content h2 {
font-size: 20px;
font-weight: 600;
margin: 30px 0 15px 0;
color: #495057;
}
.content p {
margin: 0 0 16px 0;
color: #6c757d;
line-height: 1.6;
}
.content strong {
color: #495057;
}
/* Button */
.button {
display: inline-block;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff !important;
text-decoration: none;
padding: 16px 32px;
border-radius: 6px;
font-weight: 600;
margin: 20px 0;
text-align: center;
transition: all 0.3s ease;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
/* Info box */
.info-box {
background-color: #fff3cd;
border-left: 4px solid #ffc107;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.info-box p {
margin: 0 0 10px 0;
color: #856404;
}
.info-box p:last-child {
margin-bottom: 0;
}
/* Success box */
.success-box {
background-color: #d4edda;
border-left: 4px solid #28a745;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.success-box p {
margin: 0;
color: #155724;
font-size: 16px;
font-weight: 600;
}
/* Info table */
.info-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background-color: #f8f9fa;
border-radius: 6px;
overflow: hidden;
}
.info-table th,
.info-table td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #dee2e6;
}
.info-table th {
background-color: #e9ecef;
font-weight: 600;
color: #495057;
}
.info-table td {
color: #6c757d;
}
.info-table tr:last-child td {
border-bottom: none;
}
/* Footer */
.footer {
background-color: #f8f9fa;
padding: 30px;
text-align: center;
border-top: 1px solid #e9ecef;
}
.footer p {
margin: 0 0 10px 0;
font-size: 14px;
color: #6c757d;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* Responsive */
@media only screen and (max-width: 600px) {
.email-container {
margin: 0;
border-radius: 0;
}
.header,
.content,
.footer {
padding: 20px;
}
.logo {
font-size: 28px;
}
.content h1 {
font-size: 22px;
}
.button {
display: block;
width: 100%;
box-sizing: border-box;
}
.info-table th,
.info-table td {
padding: 10px;
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<div class="logo">RentAll</div>
<div class="tagline">Request Submitted</div>
</div>
<div class="content">
<p>Hi {{renterName}},</p>
<div class="success-box">
<p>Your rental request has been submitted!</p>
</div>
<p>
Your request to rent <strong>{{itemName}}</strong> has been sent to
the owner. They'll review your request and respond soon.
</p>
<h2>Request Details</h2>
<table class="info-table">
<tr>
<th>Item</th>
<td>{{itemName}}</td>
</tr>
<tr>
<th>Start Date</th>
<td>{{startDate}}</td>
</tr>
<tr>
<th>End Date</th>
<td>{{endDate}}</td>
</tr>
<tr>
<th>Delivery Method</th>
<td>{{deliveryMethod}}</td>
</tr>
<tr>
<th>Total Amount</th>
<td>${{totalAmount}}</td>
</tr>
</table>
<div class="info-box">
<p><strong>What happens next?</strong></p>
<p>
{{paymentMessage}}
</p>
<p>
You'll receive an email notification once the owner responds to your
request.
</p>
</div>
<div style="text-align: center">
<a href="{{viewRentalsUrl}}" class="button">View My Rentals</a>
</div>
<p>
<strong>Need to make changes?</strong> If you need to cancel or modify
your request, you can do so from the My Rentals page.
</p>
</div>
<div class="footer">
<p><strong>RentAll</strong></p>
<p>
This is a confirmation email for your rental request. You received
this message because you submitted a rental request on RentAll.
</p>
<p>If you have any questions, please contact our support team.</p>
<p>&copy; 2024 RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@@ -25,15 +25,43 @@ const level = () => {
return isDevelopment ? 'debug' : process.env.LOG_LEVEL || 'info';
};
// Custom format to extract stack traces from Error objects in metadata
const extractErrorStack = winston.format((info) => {
// Check if any metadata value is an Error object and extract its stack
Object.keys(info).forEach(key => {
if (info[key] instanceof Error) {
info[key] = {
message: info[key].message,
stack: info[key].stack,
name: info[key].name
};
}
});
return info;
});
// Define log format
const logFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
winston.format.colorize({ all: true }),
extractErrorStack(),
winston.format.printf((info) => {
if (info.stack) {
return `${info.timestamp} ${info.level}: ${info.message}\n${info.stack}`;
const { timestamp, level, message, stack, ...metadata } = info;
let output = `${timestamp} ${level}: ${message}`;
// Check for stack trace in the info object itself
if (stack) {
output += `\n${stack}`;
}
return `${info.timestamp} ${info.level}: ${info.message}`;
// Check for Error objects in metadata
Object.keys(metadata).forEach(key => {
if (metadata[key] && metadata[key].stack) {
output += `\n${key}: ${metadata[key].message}\n${metadata[key].stack}`;
}
});
return output;
}),
);
@@ -41,6 +69,7 @@ const logFormat = winston.format.combine(
const jsonFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
winston.format.errors({ stack: true }),
extractErrorStack(),
winston.format.json()
);