emails for rental cancelation, rental declined, rental request confirmation, payout received
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
419
backend/templates/emails/payoutReceived.html
Normal file
419
backend/templates/emails/payoutReceived.html
Normal 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>© 2024 RentAll. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
311
backend/templates/emails/rentalCancellationConfirmation.html
Normal file
311
backend/templates/emails/rentalCancellationConfirmation.html
Normal 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>© 2024 RentAll. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
310
backend/templates/emails/rentalCancellationNotification.html
Normal file
310
backend/templates/emails/rentalCancellationNotification.html
Normal 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>© 2024 RentAll. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
317
backend/templates/emails/rentalDeclined.html
Normal file
317
backend/templates/emails/rentalDeclined.html
Normal 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>© 2024 RentAll. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
320
backend/templates/emails/rentalRequestConfirmation.html
Normal file
320
backend/templates/emails/rentalRequestConfirmation.html
Normal 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>© 2024 RentAll. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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()
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user