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