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} 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, };