payout retry lambda
This commit is contained in:
193
lambdas/payoutRetryProcessor/handler.js
Normal file
193
lambdas/payoutRetryProcessor/handler.js
Normal file
@@ -0,0 +1,193 @@
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user