paid out amount updates on page load if webhook doesn't work
This commit is contained in:
@@ -293,6 +293,127 @@ class StripeWebhookService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconcile payout statuses for an owner by checking Stripe for actual status.
|
||||||
|
* This handles cases where the payout.paid webhook was missed or failed.
|
||||||
|
*
|
||||||
|
* Simplified approach: Since Stripe automatic payouts sweep the entire available
|
||||||
|
* balance, if there's been a paid payout after our transfer was created, our
|
||||||
|
* funds were included.
|
||||||
|
*
|
||||||
|
* @param {string} ownerId - The owner's user ID
|
||||||
|
* @returns {Object} - { reconciled, updated, errors }
|
||||||
|
*/
|
||||||
|
static async reconcilePayoutStatuses(ownerId) {
|
||||||
|
const results = {
|
||||||
|
reconciled: 0,
|
||||||
|
updated: 0,
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find rentals that need reconciliation
|
||||||
|
const rentalsToReconcile = await Rental.findAll({
|
||||||
|
where: {
|
||||||
|
ownerId,
|
||||||
|
payoutStatus: "completed",
|
||||||
|
stripeTransferId: { [Op.not]: null },
|
||||||
|
bankDepositStatus: { [Op.is]: null },
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "owner",
|
||||||
|
attributes: ["stripeConnectedAccountId"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rentalsToReconcile.length === 0) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Reconciling payout statuses", {
|
||||||
|
ownerId,
|
||||||
|
rentalsCount: rentalsToReconcile.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the connected account ID (same for all rentals of this owner)
|
||||||
|
const connectedAccountId = rentalsToReconcile[0].owner?.stripeConnectedAccountId;
|
||||||
|
if (!connectedAccountId) {
|
||||||
|
logger.warn("Owner has no connected account ID", { ownerId });
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch recent paid payouts once for all rentals
|
||||||
|
const paidPayouts = await stripe.payouts.list(
|
||||||
|
{ status: "paid", limit: 20 },
|
||||||
|
{ stripeAccount: connectedAccountId }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (paidPayouts.data.length === 0) {
|
||||||
|
logger.info("No paid payouts found for connected account", { connectedAccountId });
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rental of rentalsToReconcile) {
|
||||||
|
results.reconciled++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the transfer to find when it was created
|
||||||
|
const transfer = await stripe.transfers.retrieve(rental.stripeTransferId);
|
||||||
|
|
||||||
|
// Find a payout that arrived after the transfer was created
|
||||||
|
const matchingPayout = paidPayouts.data.find(
|
||||||
|
(payout) => payout.arrival_date >= transfer.created
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingPayout) {
|
||||||
|
await rental.update({
|
||||||
|
bankDepositStatus: "paid",
|
||||||
|
bankDepositAt: new Date(matchingPayout.arrival_date * 1000),
|
||||||
|
stripePayoutId: matchingPayout.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
results.updated++;
|
||||||
|
|
||||||
|
logger.info("Reconciled rental payout status", {
|
||||||
|
rentalId: rental.id,
|
||||||
|
payoutId: matchingPayout.id,
|
||||||
|
arrivalDate: matchingPayout.arrival_date,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (rentalError) {
|
||||||
|
results.errors.push({
|
||||||
|
rentalId: rental.id,
|
||||||
|
error: rentalError.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.error("Error reconciling rental payout status", {
|
||||||
|
rentalId: rental.id,
|
||||||
|
error: rentalError.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Payout reconciliation complete", {
|
||||||
|
ownerId,
|
||||||
|
reconciled: results.reconciled,
|
||||||
|
updated: results.updated,
|
||||||
|
errors: results.errors.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error in reconcilePayoutStatuses", {
|
||||||
|
ownerId,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = StripeWebhookService;
|
module.exports = StripeWebhookService;
|
||||||
|
|||||||
Reference in New Issue
Block a user