Files
rentall-app/lambdas/payoutRetryProcessor/handler.js
2026-01-14 18:05:41 -05:00

194 lines
5.4 KiB
JavaScript

const { logger, stripe, email } = require("../shared");
const queries = require("./queries");
const path = require("path");
/**
* Process all failed payouts by retrying them.
* This is the main handler logic invoked by the Lambda.
* @returns {Promise<Object>} Results summary with successful and failed counts
*/
async function processPayoutRetries() {
logger.info("Starting payout retry process");
// Get all failed payouts with eligible owners
const failedPayouts = await queries.getFailedPayoutsWithOwners();
logger.info("Found failed payouts to retry", {
count: failedPayouts.length,
});
if (failedPayouts.length === 0) {
return {
success: true,
totalProcessed: 0,
successful: [],
failed: [],
message: "No failed payouts to retry",
};
}
const results = {
successful: [],
failed: [],
};
// Process each failed payout
for (const rental of failedPayouts) {
try {
logger.info("Processing payout retry", {
rentalId: rental.id,
ownerId: rental.ownerId,
amount: rental.payoutAmount,
});
// Reset to pending before retry attempt
await queries.resetPayoutToPending(rental.id);
// Attempt to create Stripe transfer
const transfer = await stripe.createTransfer({
amount: rental.payoutAmount,
destination: rental.owner.stripeConnectedAccountId,
metadata: {
rentalId: rental.id,
ownerId: rental.ownerId,
totalAmount: rental.totalAmount.toString(),
platformFee: rental.platformFee.toString(),
startDateTime: rental.startDateTime.toISOString(),
endDateTime: rental.endDateTime.toISOString(),
retryAttempt: "true",
},
});
// Update rental with successful payout
await queries.updatePayoutSuccess(rental.id, transfer.id);
logger.info("Payout retry successful", {
rentalId: rental.id,
transferId: transfer.id,
amount: rental.payoutAmount,
});
// Send success email notification
try {
await sendPayoutSuccessEmail(rental, transfer.id);
} catch (emailError) {
// Log error but don't fail the payout
logger.error("Failed to send payout success email", {
error: emailError.message,
rentalId: rental.id,
});
}
results.successful.push({
rentalId: rental.id,
amount: rental.payoutAmount,
transferId: transfer.id,
});
} catch (error) {
logger.error("Payout retry failed", {
error: error.message,
rentalId: rental.id,
ownerId: rental.ownerId,
});
// Update payout status back to failed
await queries.updatePayoutFailed(rental.id);
// Check if account is disconnected
if (stripe.isAccountDisconnectedError(error)) {
logger.warn("Account appears disconnected, cleaning up", {
accountId: rental.owner.stripeConnectedAccountId,
});
await handleDisconnectedAccount(rental.owner.stripeConnectedAccountId);
}
results.failed.push({
rentalId: rental.id,
error: error.message,
});
}
}
const summary = {
success: true,
totalProcessed: failedPayouts.length,
successfulCount: results.successful.length,
failedCount: results.failed.length,
successful: results.successful,
failed: results.failed,
};
logger.info("Payout retry process complete", {
totalProcessed: summary.totalProcessed,
successful: summary.successfulCount,
failed: summary.failedCount,
});
return summary;
}
/**
* Send payout success email to owner.
* @param {Object} rental - Rental object with owner and item
* @param {string} stripeTransferId - The Stripe transfer ID
*/
async function sendPayoutSuccessEmail(rental, stripeTransferId) {
const templatePath = path.join(
__dirname,
"templates",
"payoutReceivedToOwner.html"
);
const template = await email.loadTemplate(templatePath);
const ownerName = rental.owner.firstName || rental.owner.lastName || "there";
const frontendUrl = process.env.FRONTEND_URL;
const variables = {
ownerName,
payoutAmount: rental.payoutAmount.toFixed(2),
itemName: rental.item.name,
startDate: email.formatEmailDate(rental.startDateTime),
endDate: email.formatEmailDate(rental.endDateTime),
stripeTransferId,
totalAmount: rental.totalAmount.toFixed(2),
platformFee: rental.platformFee.toFixed(2),
earningsDashboardUrl: `${frontendUrl}/dashboard/earnings`,
};
const htmlBody = email.renderTemplate(template, variables);
await email.sendEmail(
rental.owner.email,
"Your earnings have been deposited - Village Share",
htmlBody
);
}
/**
* Handle cleanup when a Stripe account is detected as disconnected.
* @param {string} stripeConnectedAccountId - The disconnected account ID
*/
async function handleDisconnectedAccount(stripeConnectedAccountId) {
try {
const user = await queries.clearDisconnectedAccount(
stripeConnectedAccountId
);
if (user) {
logger.info("Cleaned up disconnected Stripe account", {
userId: user.id,
accountId: stripeConnectedAccountId,
});
}
} catch (error) {
logger.error("Failed to clean up disconnected account", {
accountId: stripeConnectedAccountId,
error: error.message,
});
}
}
module.exports = {
processPayoutRetries,
};