Files
starr-philproperties/apps/api/cron/jobs/fail_stale_payments.cron.js

88 lines
3.6 KiB
JavaScript

/***********************************************************************************************************************************************************************
* File Name : fail_stale_payments.cron.js
* Type : Cron Job
* Description : Marks abandoned checkout attempts as 'failed' instead of
* leaving them stuck on 'pending' forever.
*
* A payment/purchase row is created as 'pending' the moment
* PayPal's create-order call succeeds (createOrder / createCourseOrder),
* before the buyer ever reaches PayPal's approval page. If PayPal's
* own hosted checkout then fails to load ("Things don't appear to
* be working at the moment") or the buyer just abandons the tab,
* the browser never gets redirected back to our return_url/cancel_url —
* so captureOrder/cancelOrder is never called, and the row sits as
* 'pending' indefinitely even though no money ever moved.
*
* This job sweeps 'pending' rows older than STALE_MINUTES and
* marks them 'failed', so payment history correctly reflects
* that nothing was charged, instead of silently doing nothing.
*
* Schedule : Every 10 minutes ("*\/10 * * * *"). Registered by cron/client.cron.js.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Aug. 3, 2026
***********************************************************************************************************************************************************************/
'use strict';
const { Op } = require('sequelize');
const mdl_Payments = require('../../models/tiers/payments.mdl');
const mdl_CoursePurchase = require('../../models/courses/course_purchases.mdl');
const STALE_MINUTES = 30;
async function run() {
const cutoff = new Date(Date.now() - STALE_MINUTES * 60_000);
try {
const stalePayments = await mdl_Payments.findAll({
where: { status: 'pending', createdAt: { [Op.lt]: cutoff } },
});
for (const payment of stalePayments) {
await payment.update({
status: 'failed',
provider_payload: {
...payment.provider_payload,
failed_reason: 'abandoned_checkout',
marked_failed_at: new Date().toISOString(),
},
});
}
if (stalePayments.length) {
console.log(`[CRON][FAIL STALE PAYMENTS] Marked ${stalePayments.length} abandoned tier payment(s) as failed.`);
}
} catch (err) {
console.error('[CRON][FAIL STALE PAYMENTS] Failed sweeping payments:', err);
}
try {
const stalePurchases = await mdl_CoursePurchase.findAll({
where: { status: 'pending', createdAt: { [Op.lt]: cutoff } },
});
for (const purchase of stalePurchases) {
await purchase.update({
status: 'failed',
provider_payload: {
...purchase.provider_payload,
failed_reason: 'abandoned_checkout',
marked_failed_at: new Date().toISOString(),
},
});
}
if (stalePurchases.length) {
console.log(`[CRON][FAIL STALE PAYMENTS] Marked ${stalePurchases.length} abandoned course purchase(s) as failed.`);
}
} catch (err) {
console.error('[CRON][FAIL STALE PAYMENTS] Failed sweeping course purchases:', err);
}
}
module.exports = {
name: 'failStalePayments',
schedule: '*/10 * * * *',
run,
};