diff --git a/services/web/scripts/recurly/resync_recurly_state_single_subscription.mjs b/services/web/scripts/recurly/resync_recurly_state_single_subscription.mjs new file mode 100644 index 0000000000..85d3e3eb77 --- /dev/null +++ b/services/web/scripts/recurly/resync_recurly_state_single_subscription.mjs @@ -0,0 +1,137 @@ +import { Subscription } from '../../app/src/models/Subscription.js' +import RecurlyWrapper from '../../app/src/Features/Subscription/RecurlyWrapper.js' +import SubscriptionUpdater from '../../app/src/Features/Subscription/SubscriptionUpdater.js' +import minimist from 'minimist' +import { setTimeout } from 'node:timers/promises' +import util from 'node:util' + +util.inspect.defaultOptions.maxArrayLength = null + +const handleSyncSubscriptionError = async (subscription, error) => { + console.warn(`Errors with subscription id=${subscription._id}:`, error) + + if (typeof error === 'string' && error.match(/429$/)) { + console.warn('Recurly rate limit hit (429). Waiting for 5 minutes...') + await setTimeout(1000 * 60 * 5) + return + } + if (typeof error === 'string' && error.match(/5\d\d$/)) { + console.warn('Recurly server error (5xx). Retrying in 1 minute...') + await setTimeout(1000 * 60) + await syncRecurlyStateInSubscription(subscription) + return + } + await setTimeout(80) +} + +const syncRecurlyStateInSubscription = async subscription => { + let recurlySubscription + + try { + recurlySubscription = await RecurlyWrapper.promises.getSubscription( + subscription.recurlySubscription_id + ) + } catch (error) { + await handleSyncSubscriptionError(subscription, error) + return + } + + if (!subscription.recurlyStatus) { + subscription.recurlyStatus = {} + } + + if (subscription.recurlyStatus.state !== recurlySubscription.state) { + console.log( + `Mismatched recurlyStatus.state for subscription ID ${subscription._id}. ` + + `Our database: '${subscription.recurlyStatus.state || 'undefined/null'}', recurly: '${recurlySubscription.state}'.` + ) + + subscription.recurlyStatus.state = recurlySubscription.state + + if (COMMIT) { + try { + console.log( + `Committing update for subscription ID: ${subscription._id}` + ) + await SubscriptionUpdater.promises.updateSubscriptionFromRecurly( + recurlySubscription, + subscription, + {} + ) + } catch (error) { + await handleSyncSubscriptionError(subscription, error) + } + + console.log( + `Successfully updated subscription ID ${subscription._id} with new recurlyStatus.state: ${subscription.recurlyStatus.state}` + ) + } + } else { + console.log( + `Subscription ID ${subscription._id}: recurlyStatus.state is already in sync.` + ) + } + + await setTimeout(80) +} + +let COMMIT, SUBSCRIPTION_ID + +const setup = () => { + const argv = minimist(process.argv.slice(2)) + + SUBSCRIPTION_ID = argv.subscriptionId + if (!SUBSCRIPTION_ID) { + console.error( + 'Error: Please provide a subscription ID using --subscriptionId=' + ) + process.exit(1) + } + console.log( + `Attempting to sync subscription.recurlyStatus with ID: ${SUBSCRIPTION_ID}` + ) + + COMMIT = argv.commit !== undefined + if (!COMMIT) { + console.warn( + 'Doing dry run without --commit. No database changes will be made.' + ) + } +} + +const run = async () => { + try { + const subscription = await Subscription.findById(SUBSCRIPTION_ID).exec() + + if (!subscription) { + console.error( + `Error: Subscription with ID ${SUBSCRIPTION_ID} not found in the database.` + ) + process.exit(1) + } + + if (!subscription.recurlySubscription_id) { + console.error( + `Error: Subscription ID ${SUBSCRIPTION_ID} does not have a Recurly subscription ID.` + ) + process.exit(1) + } + + console.log( + `Found subscription: ${subscription._id}, Recurly ID: ${subscription.recurlySubscription_id}` + ) + + await syncRecurlyStateInSubscription(subscription) + + console.log('DONE') + } catch (error) { + console.error('An unhandled error occurred during script execution:', error) + process.exit(1) + } +} + +setup() + +await run() + +process.exit(0)