mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-13 21:53:43 +01:00
feat: removing recurly revert logic since we are sunsetting recurly (#31470)
GitOrigin-RevId: c165c7d14ec5d57fdbe3b0962bd65222182a9f48
This commit is contained in:
committed by
Copybot
parent
06668fd6a8
commit
793506c10f
@@ -47,8 +47,6 @@ class DuplicateNameError extends OError {}
|
||||
|
||||
class InvalidNameError extends BackwardCompatibleError {}
|
||||
|
||||
class IndeterminateInvoiceError extends OError {}
|
||||
|
||||
class UnsupportedFileTypeError extends BackwardCompatibleError {}
|
||||
|
||||
class FileTooLargeError extends BackwardCompatibleError {}
|
||||
@@ -378,7 +376,6 @@ module.exports = {
|
||||
UnconfirmedEmailError,
|
||||
EmailExistsError,
|
||||
InvalidError,
|
||||
IndeterminateInvoiceError,
|
||||
NotInV2Error,
|
||||
OutputFileFetchFailedError,
|
||||
SAMLAssertionAudienceMismatch,
|
||||
|
||||
@@ -394,43 +394,6 @@ export class PaymentProviderSubscription {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Form a request to revert the plan to it's last saved backup state
|
||||
*
|
||||
* @param {string} previousPlanCode
|
||||
* @param {Array<AddOn> | null} previousAddOns
|
||||
* @return {PaymentProviderSubscriptionChangeRequest}
|
||||
*
|
||||
* @throws {OError} if the restore point plan doesnt exist
|
||||
*/
|
||||
getRequestForPlanRevert(previousPlanCode, previousAddOns) {
|
||||
const lastSuccessfulPlan =
|
||||
PlansLocator.findLocalPlanInSettings(previousPlanCode)
|
||||
if (lastSuccessfulPlan == null) {
|
||||
throw new OError('Unable to find plan in settings', { previousPlanCode })
|
||||
}
|
||||
const changeRequest = new PaymentProviderSubscriptionChangeRequest({
|
||||
subscription: this,
|
||||
timeframe: 'now',
|
||||
planCode: previousPlanCode,
|
||||
})
|
||||
|
||||
// defaulting to empty array is important, as that will wipe away any add-ons that were added in the failed payment
|
||||
// but were not part of the last successful subscription
|
||||
const addOns = []
|
||||
for (const previousAddon of previousAddOns || []) {
|
||||
const addOnUpdate = new PaymentProviderSubscriptionAddOnUpdate({
|
||||
code: previousAddon.addOnCode,
|
||||
quantity: previousAddon.quantity,
|
||||
unitPrice: previousAddon.unitAmountInCents / 100,
|
||||
})
|
||||
addOns.push(addOnUpdate)
|
||||
}
|
||||
changeRequest.addOnUpdates = addOns
|
||||
|
||||
return changeRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade group plan with the plan code provided
|
||||
*
|
||||
|
||||
@@ -884,38 +884,6 @@ function subscriptionUpdateRequestToApi(updateRequest) {
|
||||
return requestBody
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of failed invoices for a given Recurly subscription ID.
|
||||
*
|
||||
* @async
|
||||
* @function
|
||||
* @param {string} subscriptionId - The ID of the Recurly subscription to fetch failed invoices for.
|
||||
* @returns {Promise<Array<recurly.Invoice>>} A promise that resolves to an array of failed invoice objects.
|
||||
*/
|
||||
async function getPastDueInvoices(subscriptionId) {
|
||||
const failed = []
|
||||
const invoices = client.listSubscriptionInvoices(`uuid-${subscriptionId}`, {
|
||||
params: { state: 'past_due' },
|
||||
})
|
||||
|
||||
for await (const invoice of invoices.each()) {
|
||||
failed.push(invoice)
|
||||
}
|
||||
return failed
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks an invoice as failed using the Recurly client.
|
||||
*
|
||||
* @async
|
||||
* @function failInvoice
|
||||
* @param {string} invoiceId - The ID of the invoice to be marked as failed.
|
||||
* @returns {Promise<void>} Resolves when the invoice has been successfully marked as failed.
|
||||
*/
|
||||
async function failInvoice(invoiceId) {
|
||||
await client.markInvoiceFailed(invoiceId)
|
||||
}
|
||||
|
||||
async function terminateSubscriptionByUuid(subscriptionUuid) {
|
||||
const subscription = await client.terminateSubscription(
|
||||
'uuid-' + subscriptionUuid,
|
||||
@@ -952,8 +920,6 @@ export default {
|
||||
subscriptionIsCanceledOrExpired,
|
||||
pauseSubscriptionByUuid: callbackify(pauseSubscriptionByUuid),
|
||||
resumeSubscriptionByUuid: callbackify(resumeSubscriptionByUuid),
|
||||
getPastDueInvoices: callbackify(getPastDueInvoices),
|
||||
failInvoice: callbackify(failInvoice),
|
||||
terminateSubscriptionByUuid: callbackify(terminateSubscriptionByUuid),
|
||||
|
||||
promises: {
|
||||
@@ -975,8 +941,6 @@ export default {
|
||||
getPaymentMethod,
|
||||
getAddOn,
|
||||
getPlan,
|
||||
getPastDueInvoices,
|
||||
failInvoice,
|
||||
terminateSubscriptionByUuid,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import UserGetter from '../User/UserGetter.mjs'
|
||||
import PermissionsManager from '../Authorization/PermissionsManager.mjs'
|
||||
import { sanitizeSessionUserForFrontEnd } from '../../infrastructure/FrontEndUser.mjs'
|
||||
import { z, parseReq } from '../../infrastructure/Validation.mjs'
|
||||
import { IndeterminateInvoiceError } from '../Errors/Errors.js'
|
||||
import SubscriptionLocator from './SubscriptionLocator.mjs'
|
||||
import { PaymentProviderSubscriptionChange } from './PaymentProviderEntities.mjs'
|
||||
|
||||
@@ -887,54 +886,7 @@ function recurlyCallback(req, res, next) {
|
||||
)
|
||||
)
|
||||
|
||||
// this is a recurly only case which is required since Recurly does not have a reliable way to check credit info pre-upgrade purchase
|
||||
if (event === 'failed_payment_notification') {
|
||||
if (!Settings.planReverts?.enabled) {
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
|
||||
// A manual charge may have no subscription, in which case we get a
|
||||
// <subscription_id nil="true"/> element, which produces an object instead
|
||||
// of a string subscription_id.
|
||||
const subscriptionId = eventData.transaction?.subscription_id
|
||||
if (!subscriptionId || typeof subscriptionId !== 'string') {
|
||||
logger.info(
|
||||
{ transactionId: eventData.transaction?.id },
|
||||
'ignoring failed_payment_notification without subscription_id'
|
||||
)
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
|
||||
SubscriptionHandler.getSubscriptionRestorePoint(
|
||||
subscriptionId,
|
||||
function (err, lastSubscription) {
|
||||
if (err) {
|
||||
return next(err)
|
||||
}
|
||||
// if theres no restore point it could be a failed renewal, or no restore set. Either way it will be handled through dunning automatically
|
||||
if (!lastSubscription || !lastSubscription?.planCode) {
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
SubscriptionHandler.revertPlanChange(
|
||||
eventData.transaction.subscription_id,
|
||||
lastSubscription,
|
||||
function (err) {
|
||||
if (err instanceof IndeterminateInvoiceError) {
|
||||
logger.warn(
|
||||
{ recurlySubscriptionId: err.info.recurlySubscriptionId },
|
||||
'could not determine invoice to fail for subscription'
|
||||
)
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
if (err) {
|
||||
return next(err)
|
||||
}
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
} else if (
|
||||
if (
|
||||
[
|
||||
'new_subscription_notification',
|
||||
'updated_subscription_notification',
|
||||
|
||||
@@ -2,17 +2,14 @@
|
||||
|
||||
import RecurlyWrapper from './RecurlyWrapper.mjs'
|
||||
|
||||
import RecurlyClient from './RecurlyClient.mjs'
|
||||
import { User } from '../../models/User.mjs'
|
||||
import logger from '@overleaf/logger'
|
||||
import SubscriptionHelper from './SubscriptionHelper.mjs'
|
||||
import SubscriptionUpdater from './SubscriptionUpdater.mjs'
|
||||
import SubscriptionLocator from './SubscriptionLocator.mjs'
|
||||
import LimitationsManager from './LimitationsManager.mjs'
|
||||
import EmailHandler from '../Email/EmailHandler.mjs'
|
||||
import { callbackify } from '@overleaf/promise-utils'
|
||||
import UserUpdater from '../User/UserUpdater.mjs'
|
||||
import { IndeterminateInvoiceError } from '../Errors/Errors.js'
|
||||
import Modules from '../../infrastructure/Modules.mjs'
|
||||
import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
|
||||
import { AI_ADD_ON_CODE } from './AiHelper.mjs'
|
||||
@@ -399,80 +396,6 @@ async function resumeSubscription(user) {
|
||||
await Modules.promises.hooks.fire('resumePaidSubscription', subscription)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param recurlySubscriptionId
|
||||
*/
|
||||
async function getSubscriptionRestorePoint(recurlySubscriptionId) {
|
||||
const lastSubscription =
|
||||
await SubscriptionLocator.promises.getLastSuccessfulSubscription(
|
||||
recurlySubscriptionId
|
||||
)
|
||||
return lastSubscription
|
||||
}
|
||||
|
||||
/**
|
||||
* @param recurlySubscriptionId
|
||||
* @param subscriptionRestorePoint
|
||||
*/
|
||||
async function revertPlanChange(
|
||||
recurlySubscriptionId,
|
||||
subscriptionRestorePoint
|
||||
) {
|
||||
const subscription = await RecurlyClient.promises.getSubscription(
|
||||
recurlySubscriptionId
|
||||
)
|
||||
|
||||
const changeRequest = subscription.getRequestForPlanRevert(
|
||||
subscriptionRestorePoint.planCode,
|
||||
subscriptionRestorePoint.addOns
|
||||
)
|
||||
|
||||
const pastDue = await RecurlyClient.promises.getPastDueInvoices(
|
||||
recurlySubscriptionId
|
||||
)
|
||||
|
||||
// only process revert requests within the past 24 hours, as we dont want to restore plans at the end of their dunning cycle
|
||||
const yesterday = new Date()
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
if (
|
||||
pastDue.length !== 1 ||
|
||||
!pastDue[0].id ||
|
||||
!pastDue[0].dueAt ||
|
||||
pastDue[0].dueAt < yesterday ||
|
||||
pastDue[0].collectionMethod !== 'automatic'
|
||||
) {
|
||||
throw new IndeterminateInvoiceError(
|
||||
'cant determine invoice to fail for plan revert',
|
||||
{
|
||||
recurlySubscriptionId,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
await RecurlyClient.promises.failInvoice(pastDue[0].id)
|
||||
await SubscriptionUpdater.promises.setSubscriptionWasReverted(
|
||||
subscriptionRestorePoint._id
|
||||
)
|
||||
await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
|
||||
await syncSubscription({ uuid: recurlySubscriptionId }, {})
|
||||
}
|
||||
|
||||
async function setSubscriptionRestorePoint(userId) {
|
||||
const subscription =
|
||||
await SubscriptionLocator.promises.getUsersSubscription(userId)
|
||||
// if the subscription is not a recurly one, we can return early as we dont allow for failed payments on other payment providers
|
||||
// we need to deal with it for recurly, because we cant verify payment in advance
|
||||
if (!subscription?.recurlySubscription_id || !subscription.planCode) {
|
||||
return
|
||||
}
|
||||
await SubscriptionUpdater.promises.setRestorePoint(
|
||||
subscription.id,
|
||||
subscription.planCode,
|
||||
subscription.addOns,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
validateNoSubscriptionInRecurly: callbackify(validateNoSubscriptionInRecurly),
|
||||
createSubscription: callbackify(createSubscription),
|
||||
@@ -490,9 +413,6 @@ export default {
|
||||
reactivateAddon: callbackify(reactivateAddon),
|
||||
pauseSubscription: callbackify(pauseSubscription),
|
||||
resumeSubscription: callbackify(resumeSubscription),
|
||||
revertPlanChange: callbackify(revertPlanChange),
|
||||
setSubscriptionRestorePoint: callbackify(setSubscriptionRestorePoint),
|
||||
getSubscriptionRestorePoint: callbackify(getSubscriptionRestorePoint),
|
||||
promises: {
|
||||
validateNoSubscriptionInRecurly,
|
||||
createSubscription,
|
||||
@@ -510,8 +430,5 @@ export default {
|
||||
reactivateAddon,
|
||||
pauseSubscription,
|
||||
resumeSubscription,
|
||||
revertPlanChange,
|
||||
setSubscriptionRestorePoint,
|
||||
getSubscriptionRestorePoint,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
/**
|
||||
* @import { AddOn } from '../../../../types/subscription/plan'
|
||||
*/
|
||||
|
||||
import { callbackifyAll } from '@overleaf/promise-utils'
|
||||
|
||||
import { Subscription } from '../../models/Subscription.mjs'
|
||||
@@ -183,27 +179,6 @@ const SubscriptionLocator = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the last successful subscription for a given user.
|
||||
*
|
||||
* @async
|
||||
* @function
|
||||
* @param {string} recurlyId - The ID of the recurly subscription tied to the mongo subscription to check for a previous successful state.
|
||||
* @returns {Promise<{_id: ObjectId, planCode: string, addOns: [AddOn]}|null>} A promise that resolves to the last successful planCode and addon state,
|
||||
* or null if we havent stored a previous
|
||||
*/
|
||||
async getLastSuccessfulSubscription(recurlyId) {
|
||||
const subscription = await Subscription.findOne({
|
||||
recurlySubscription_id: recurlyId,
|
||||
}).exec()
|
||||
return subscription && subscription.lastSuccesfulSubscription
|
||||
? {
|
||||
...subscription.lastSuccesfulSubscription,
|
||||
_id: subscription._id,
|
||||
}
|
||||
: null
|
||||
},
|
||||
|
||||
async getUserSubscriptionStatus(userId) {
|
||||
let usersSubscription = { personal: false, group: false }
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import Modules from '../../infrastructure/Modules.mjs'
|
||||
* @typedef {import('../../../../types/subscription/dashboard/subscription').Subscription} Subscription
|
||||
* @typedef {import('../../../../types/subscription/dashboard/subscription').PaymentProvider} PaymentProvider
|
||||
* @typedef {import('../../../../types/group-management/group-audit-log').GroupAuditLog} GroupAuditLog
|
||||
* @import { AddOn } from '../../../../types/subscription/plan'
|
||||
* @typedef {InstanceType<Subscription>} MongoSubscription
|
||||
*/
|
||||
|
||||
@@ -518,28 +517,6 @@ async function _sendSubscriptionEventForAllMembers(subscriptionId, event) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the plan code and addon state to revert the plan to in case of failed upgrades, or clears the last restore point if it was used/ voided
|
||||
* @param {ObjectId} subscriptionId the mongo ID of the subscription to set the restore point for
|
||||
* @param {string} planCode the plan code to revert to
|
||||
* @param {Array<AddOn>} addOns the addOns to revert to
|
||||
* @param {Boolean} consumed whether the restore point was used to revert a subscription
|
||||
*/
|
||||
async function setRestorePoint(subscriptionId, planCode, addOns, consumed) {
|
||||
const update = {
|
||||
$set: {
|
||||
'lastSuccesfulSubscription.planCode': planCode,
|
||||
'lastSuccesfulSubscription.addOns': addOns,
|
||||
},
|
||||
}
|
||||
|
||||
if (consumed) {
|
||||
update.$inc = { timesRevertedDueToFailedPayment: 1 }
|
||||
}
|
||||
|
||||
await Subscription.updateOne({ _id: subscriptionId }, update).exec()
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the ownershiop of the given subscription.
|
||||
* @param {MongoSubscription} subscription
|
||||
@@ -572,31 +549,6 @@ async function transferSubscriptionOwnership(
|
||||
await Subscription.updateOne(query, update).exec()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the restore point for a given subscription, and signals that the subscription was sucessfully reverted.
|
||||
*
|
||||
* @async
|
||||
* @function setSubscriptionWasReverted
|
||||
* @param {ObjectId} subscriptionId the mongo ID of the subscription to set the restore point for
|
||||
* @returns {Promise<void>} Resolves when the restore point has been cleared.
|
||||
*/
|
||||
async function setSubscriptionWasReverted(subscriptionId) {
|
||||
// consume the backup and flag that the subscription was reverted due to failed payment
|
||||
await setRestorePoint(subscriptionId, null, null, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the restore point for a given subscription, and signals that the subscription was not reverted.
|
||||
*
|
||||
* @async
|
||||
* @function voidRestorePoint
|
||||
* @param {string} subscriptionId - The unique identifier of the subscription.
|
||||
* @returns {Promise<void>} Resolves when the restore point has been cleared.
|
||||
*/
|
||||
async function voidRestorePoint(subscriptionId) {
|
||||
await setRestorePoint(subscriptionId, null, null, false)
|
||||
}
|
||||
|
||||
export default {
|
||||
updateAdmin: callbackify(updateAdmin),
|
||||
syncSubscription: callbackify(syncSubscription),
|
||||
@@ -611,9 +563,6 @@ export default {
|
||||
restoreSubscription: callbackify(restoreSubscription),
|
||||
updateSubscriptionFromRecurly: callbackify(updateSubscriptionFromRecurly),
|
||||
scheduleRefreshFeatures: callbackify(scheduleRefreshFeatures),
|
||||
setSubscriptionRestorePoint: callbackify(setRestorePoint),
|
||||
setSubscriptionWasReverted: callbackify(setSubscriptionWasReverted),
|
||||
voidRestorePoint: callbackify(voidRestorePoint),
|
||||
promises: {
|
||||
updateAdmin,
|
||||
syncSubscription,
|
||||
@@ -628,9 +577,6 @@ export default {
|
||||
restoreSubscription,
|
||||
updateSubscriptionFromRecurly,
|
||||
scheduleRefreshFeatures,
|
||||
setRestorePoint,
|
||||
setSubscriptionWasReverted,
|
||||
voidRestorePoint,
|
||||
handleExpiredSubscription,
|
||||
transferSubscriptionOwnership,
|
||||
},
|
||||
|
||||
@@ -50,13 +50,6 @@ export const SubscriptionSchema = new Schema(
|
||||
invited_emails: [String],
|
||||
teamInvites: [TeamInviteSchema],
|
||||
recurlySubscription_id: String,
|
||||
lastSuccesfulSubscription: {
|
||||
planCode: {
|
||||
type: String,
|
||||
},
|
||||
addOns: Schema.Types.Mixed,
|
||||
},
|
||||
timesRevertedDueToFailedPayment: { type: Number, default: 0 },
|
||||
teamName: { type: String },
|
||||
teamNotice: { type: String },
|
||||
planCode: { type: String },
|
||||
|
||||
@@ -859,75 +859,6 @@ describe('PaymentProviderEntities', function () {
|
||||
).to.throw(Errors.AddOnNotPresentError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRequestForPlanRevert()', function () {
|
||||
beforeEach(function (ctx) {
|
||||
const { PaymentProviderSubscription } = ctx.PaymentProviderEntities
|
||||
ctx.subscription = new PaymentProviderSubscription({
|
||||
id: 'subscription-id',
|
||||
userId: 'user-id',
|
||||
planCode: 'regular-plan',
|
||||
planName: 'My Plan',
|
||||
planPrice: 10,
|
||||
addOns: [
|
||||
{
|
||||
addOnCode: 'addon-1',
|
||||
quantity: 2,
|
||||
unitAmountInCents: 500,
|
||||
},
|
||||
{
|
||||
addOnCode: 'addon-2',
|
||||
quantity: 1,
|
||||
unitAmountInCents: 600,
|
||||
},
|
||||
],
|
||||
subtotal: 10.99,
|
||||
taxRate: 0.2,
|
||||
taxAmount: 2.4,
|
||||
total: 14.4,
|
||||
currency: 'USD',
|
||||
})
|
||||
})
|
||||
|
||||
it('throws if the plan to revert to doesnt exist', function (ctx) {
|
||||
const invalidPlanCode = 'non-existent-plan'
|
||||
expect(() =>
|
||||
ctx.subscription.getRequestForPlanRevert(invalidPlanCode, null)
|
||||
).to.throw('Unable to find plan in settings')
|
||||
})
|
||||
|
||||
it('creates a change request with the restore point', function (ctx) {
|
||||
const previousPlanCode = 'cheap-plan'
|
||||
const previousAddOns = [
|
||||
{ addOnCode: 'addon-1', quantity: 1, unitAmountInCents: 500 },
|
||||
]
|
||||
const changeRequest = ctx.subscription.getRequestForPlanRevert(
|
||||
previousPlanCode,
|
||||
previousAddOns
|
||||
)
|
||||
expect(changeRequest).to.be.an.instanceOf(
|
||||
ctx.PaymentProviderEntities
|
||||
.PaymentProviderSubscriptionChangeRequest
|
||||
)
|
||||
expect(changeRequest.planCode).to.equal(previousPlanCode)
|
||||
expect(changeRequest.addOnUpdates).to.deep.equal([
|
||||
{
|
||||
code: 'addon-1',
|
||||
quantity: 1,
|
||||
unitPrice: 5,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('defaults to addons to an empty array to clear the addon state', function (ctx) {
|
||||
const previousPlanCode = 'cheap-plan'
|
||||
const changeRequest = ctx.subscription.getRequestForPlanRevert(
|
||||
previousPlanCode,
|
||||
null
|
||||
)
|
||||
expect(changeRequest.addOnUpdates).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -979,51 +979,4 @@ describe('RecurlyClient', function () {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPastDueInvoices', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.client.listSubscriptionInvoices = sinon.stub()
|
||||
})
|
||||
|
||||
it('should return empty if no past due are found', async function (ctx) {
|
||||
ctx.client.listSubscriptionInvoices.returns({
|
||||
each: async function* () {},
|
||||
})
|
||||
const invoices = await ctx.RecurlyClient.promises.getPastDueInvoices(
|
||||
ctx.subscription.id
|
||||
)
|
||||
expect(invoices).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('should return past due invoice', async function (ctx) {
|
||||
const pastDueInvoice = { id: 'invoice-1', state: 'past_due' }
|
||||
ctx.client.listSubscriptionInvoices.returns({
|
||||
each: async function* () {
|
||||
yield pastDueInvoice
|
||||
},
|
||||
})
|
||||
const invoices = await ctx.RecurlyClient.promises.getPastDueInvoices(
|
||||
ctx.subscription.id
|
||||
)
|
||||
expect(invoices).to.deep.equal([pastDueInvoice])
|
||||
})
|
||||
|
||||
it('should return multiple invoices if multiple past due exist', async function (ctx) {
|
||||
const pastDueInvoices = [
|
||||
{ id: 'invoice-1', state: 'past_due' },
|
||||
{ id: 'invoice-2', state: 'past_due' },
|
||||
]
|
||||
ctx.client.listSubscriptionInvoices.returns({
|
||||
each: async function* () {
|
||||
for (const invoice of pastDueInvoices) {
|
||||
yield invoice
|
||||
}
|
||||
},
|
||||
})
|
||||
const invoices = await ctx.RecurlyClient.promises.getPastDueInvoices(
|
||||
ctx.subscription.id
|
||||
)
|
||||
expect(invoices).to.deep.equal(pastDueInvoices)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -57,7 +57,6 @@ describe('SubscriptionController', function () {
|
||||
syncSubscription: sinon.stub().yields(),
|
||||
attemptPaypalInvoiceCollection: sinon.stub().yields(),
|
||||
startFreeTrial: sinon.stub(),
|
||||
revertPlanChange: sinon.stub(),
|
||||
promises: {
|
||||
createSubscription: sinon.stub().resolves(),
|
||||
updateSubscription: sinon.stub().resolves(),
|
||||
@@ -85,7 +84,6 @@ describe('SubscriptionController', function () {
|
||||
tax: 0,
|
||||
total: 2000,
|
||||
}),
|
||||
revertPlanChange: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -133,9 +131,6 @@ describe('SubscriptionController', function () {
|
||||
subdomain: 'sl',
|
||||
},
|
||||
},
|
||||
planReverts: {
|
||||
enabled: false,
|
||||
},
|
||||
siteUrl: 'http://de.overleaf.dev:3000',
|
||||
}
|
||||
ctx.AuthorizationManager = {
|
||||
@@ -869,144 +864,6 @@ describe('SubscriptionController', function () {
|
||||
ctx.res.sendStatus.calledWith(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a failed payment notification', function () {
|
||||
describe('with planReverts disabled in settings', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.settings.planReverts = { enabled: false }
|
||||
ctx.SubscriptionHandler.revertPlanChange = sinon.stub()
|
||||
|
||||
ctx.req.body = {
|
||||
failed_payment_notification: {
|
||||
transaction: {
|
||||
subscription_id: 'subscription-123',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx.res = {
|
||||
sendStatus() {
|
||||
resolve()
|
||||
},
|
||||
}
|
||||
sinon.spy(ctx.res, 'sendStatus')
|
||||
ctx.SubscriptionController.recurlyCallback(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
it('should not call revertPlanChange', function (ctx) {
|
||||
expect(ctx.SubscriptionHandler.revertPlanChange.called).to.be.false
|
||||
})
|
||||
|
||||
it('should respond with 200', async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.res.sendStatus.calledWith(200)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with planReverts enabled in settings', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.settings.planReverts = { enabled: true }
|
||||
})
|
||||
|
||||
describe('with no valid restore point', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.SubscriptionHandler.getSubscriptionRestorePoint = sinon
|
||||
.stub()
|
||||
.yields(null, null)
|
||||
ctx.SubscriptionHandler.revertPlanChange = sinon.stub()
|
||||
|
||||
ctx.req.body = {
|
||||
failed_payment_notification: {
|
||||
transaction: {
|
||||
subscription_id: 'subscription-123',
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx.res = {
|
||||
sendStatus() {
|
||||
resolve()
|
||||
},
|
||||
}
|
||||
sinon.spy(ctx.res, 'sendStatus')
|
||||
ctx.SubscriptionController.recurlyCallback(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
it('should not call revertPlanChange()', function (ctx) {
|
||||
expect(ctx.SubscriptionHandler.revertPlanChange.called).to.be.false
|
||||
})
|
||||
|
||||
it('should respond with 200', function (ctx) {
|
||||
ctx.res.sendStatus.calledWith(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a valid restore point', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.addOns = [
|
||||
{
|
||||
addOnCode: 'addon-1',
|
||||
quantity: 2,
|
||||
unitAmountInCents: 500,
|
||||
},
|
||||
{
|
||||
addOnCode: 'addon-2',
|
||||
quantity: 1,
|
||||
unitAmountInCents: 600,
|
||||
},
|
||||
]
|
||||
ctx.lastSubscription = {
|
||||
planCode: 'gold',
|
||||
addOns: ctx.addOns,
|
||||
}
|
||||
ctx.SubscriptionHandler.getSubscriptionRestorePoint = sinon
|
||||
.stub()
|
||||
.yields(null, ctx.lastSubscription)
|
||||
ctx.SubscriptionHandler.revertPlanChange = sinon.stub().yields()
|
||||
ctx.req.body = {
|
||||
failed_payment_notification: {
|
||||
transaction: {
|
||||
subscription_id: 'subscription-123',
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx.res = {
|
||||
sendStatus() {
|
||||
resolve()
|
||||
},
|
||||
}
|
||||
sinon.spy(ctx.res, 'sendStatus')
|
||||
ctx.SubscriptionController.recurlyCallback(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should get the subscription restore point', function (ctx) {
|
||||
expect(
|
||||
ctx.SubscriptionHandler.getSubscriptionRestorePoint.calledWith(
|
||||
'subscription-123'
|
||||
)
|
||||
).to.be.true
|
||||
})
|
||||
|
||||
it('should call revertPlanChange()', function (ctx) {
|
||||
expect(
|
||||
ctx.SubscriptionHandler.revertPlanChange.calledWith(
|
||||
'subscription-123',
|
||||
ctx.lastSubscription
|
||||
)
|
||||
).to.be.true
|
||||
})
|
||||
|
||||
it('should respond with 200', function (ctx) {
|
||||
ctx.res.sendStatus.calledWith(200)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('purchaseAddon', function () {
|
||||
|
||||
@@ -105,8 +105,6 @@ describe('SubscriptionHandler', function () {
|
||||
.resolves(ctx.activeRecurlyClientSubscription),
|
||||
pauseSubscriptionByUuid: sinon.stub().resolves(),
|
||||
resumeSubscriptionByUuid: sinon.stub().resolves(),
|
||||
failInvoice: sinon.stub(),
|
||||
getPastDueInvoices: sinon.stub(),
|
||||
},
|
||||
}
|
||||
ctx.SubscriptionUpdater = {
|
||||
@@ -115,7 +113,6 @@ describe('SubscriptionHandler', function () {
|
||||
syncSubscription: sinon.stub().resolves(),
|
||||
syncStripeSubscription: sinon.stub().resolves(),
|
||||
startFreeTrial: sinon.stub().resolves(),
|
||||
setSubscriptionWasReverted: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1004,150 +1001,4 @@ describe('SubscriptionHandler', function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('revertPlanChange', function () {
|
||||
describe('with correct invoices', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.subscriptionRestorePoint = {
|
||||
planCode: 'collaborator',
|
||||
addOns: [
|
||||
{ addOnCode: 'addon-1', quantity: 1, unitAmountInCents: 500 },
|
||||
],
|
||||
_id: 'restore-point-id',
|
||||
}
|
||||
ctx.pastDueInvoice = {
|
||||
id: 'invoice-123',
|
||||
dueAt: new Date(),
|
||||
collectionMethod: 'automatic',
|
||||
}
|
||||
ctx.user.id = ctx.activeRecurlySubscription.account.account_code
|
||||
ctx.User.findById = (userId, projection) => ({
|
||||
exec: () => {
|
||||
userId.should.equal(ctx.user.id)
|
||||
return Promise.resolve(ctx.user)
|
||||
},
|
||||
})
|
||||
ctx.RecurlyClient.promises.getSubscription.resolves(
|
||||
ctx.activeRecurlyClientSubscription
|
||||
)
|
||||
ctx.RecurlyClient.promises.getPastDueInvoices.resolves([
|
||||
ctx.pastDueInvoice,
|
||||
])
|
||||
ctx.RecurlyClient.promises.failInvoice.resolves()
|
||||
ctx.SubscriptionUpdater.promises.setSubscriptionWasReverted.resolves()
|
||||
ctx.RecurlyClient.promises.applySubscriptionChangeRequest.resolves()
|
||||
|
||||
await ctx.SubscriptionHandler.promises.revertPlanChange(
|
||||
ctx.activeRecurlyClientSubscription.id,
|
||||
ctx.subscriptionRestorePoint
|
||||
)
|
||||
})
|
||||
|
||||
it('should fetch the subscription from recurly', async function (ctx) {
|
||||
expect(
|
||||
ctx.RecurlyClient.promises.getSubscription.calledWith(
|
||||
ctx.activeRecurlyClientSubscription.id
|
||||
)
|
||||
).to.be.true
|
||||
})
|
||||
|
||||
it('should fail the invoice', async function (ctx) {
|
||||
expect(
|
||||
ctx.RecurlyClient.promises.failInvoice.calledWith(
|
||||
ctx.pastDueInvoice.id
|
||||
)
|
||||
).to.be.true
|
||||
})
|
||||
|
||||
it('should call setSubscriptionWasReverted', async function (ctx) {
|
||||
expect(
|
||||
ctx.SubscriptionUpdater.promises.setSubscriptionWasReverted.calledWith(
|
||||
ctx.subscriptionRestorePoint._id
|
||||
)
|
||||
).to.be.true
|
||||
})
|
||||
|
||||
it('should sync the subscription', async function (ctx) {
|
||||
ctx.SubscriptionUpdater.promises.syncSubscription.calledOnce.should.equal(
|
||||
true
|
||||
)
|
||||
ctx.SubscriptionUpdater.promises.syncSubscription.args[0][0].should.deep.equal(
|
||||
ctx.activeRecurlySubscription
|
||||
)
|
||||
ctx.SubscriptionUpdater.promises.syncSubscription.args[0][1].should.deep.equal(
|
||||
ctx.user._id
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('should throw an IndeterminateInvoiceError when', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.subscriptionRestorePoint = {
|
||||
planCode: 'collaborator',
|
||||
addOns: [
|
||||
{ addOnCode: 'addon-1', quantity: 1, unitAmountInCents: 500 },
|
||||
],
|
||||
_id: 'restore-point-id',
|
||||
}
|
||||
ctx.RecurlyClient.promises.getSubscription.resolves(
|
||||
ctx.activeRecurlyClientSubscription
|
||||
)
|
||||
})
|
||||
|
||||
it('finds a past due invoice older than 24 hours', async function (ctx) {
|
||||
const oldInvoice = {
|
||||
id: 'invoice-123',
|
||||
dueAt: new Date(Date.now() - 25 * 60 * 60 * 1000), // 25 hours ago
|
||||
collectionMethod: 'automatic',
|
||||
}
|
||||
ctx.RecurlyClient.promises.getPastDueInvoices.resolves([oldInvoice])
|
||||
|
||||
await expect(
|
||||
ctx.SubscriptionHandler.promises.revertPlanChange(
|
||||
ctx.activeRecurlyClientSubscription.id,
|
||||
ctx.subscriptionRestorePoint
|
||||
)
|
||||
).to.be.rejectedWith('cant determine invoice to fail for plan revert')
|
||||
})
|
||||
|
||||
it('finds more than one past due invoice', async function (ctx) {
|
||||
const invoices = [
|
||||
{
|
||||
id: 'invoice-123',
|
||||
dueAt: new Date(),
|
||||
collectionMethod: 'automatic',
|
||||
},
|
||||
{
|
||||
id: 'invoice-456',
|
||||
dueAt: new Date(),
|
||||
collectionMethod: 'automatic',
|
||||
},
|
||||
]
|
||||
ctx.RecurlyClient.promises.getPastDueInvoices.resolves(invoices)
|
||||
|
||||
await expect(
|
||||
ctx.SubscriptionHandler.promises.revertPlanChange(
|
||||
ctx.activeRecurlyClientSubscription.id,
|
||||
ctx.subscriptionRestorePoint
|
||||
)
|
||||
).to.be.rejectedWith('cant determine invoice to fail for plan revert')
|
||||
})
|
||||
|
||||
it('finds an invoice with a collectionMethod other than automatic', async function (ctx) {
|
||||
const manualInvoice = {
|
||||
id: 'invoice-123',
|
||||
dueAt: new Date(),
|
||||
collectionMethod: 'manual',
|
||||
}
|
||||
ctx.RecurlyClient.promises.getPastDueInvoices.resolves([manualInvoice])
|
||||
|
||||
await expect(
|
||||
ctx.SubscriptionHandler.promises.revertPlanChange(
|
||||
ctx.activeRecurlyClientSubscription.id,
|
||||
ctx.subscriptionRestorePoint
|
||||
)
|
||||
).to.be.rejectedWith('cant determine invoice to fail for plan revert')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1038,97 +1038,4 @@ describe('SubscriptionUpdater', function () {
|
||||
).to.equal(4)
|
||||
})
|
||||
})
|
||||
describe('setRestorePoint', function () {
|
||||
it('should set the restore point with the given plan code and add-ons', async function (ctx) {
|
||||
const subscriptionId = new ObjectId()
|
||||
const planCode = 'gold-plan'
|
||||
const addOns = [
|
||||
{ addOnCode: 'addon-1', quantity: 2, unitAmountInCents: 500 },
|
||||
{ addOnCode: 'addon-2', quantity: 1, unitAmountInCents: 1000 },
|
||||
]
|
||||
const consumed = false
|
||||
|
||||
await ctx.SubscriptionUpdater.promises.setRestorePoint(
|
||||
subscriptionId,
|
||||
planCode,
|
||||
addOns,
|
||||
consumed
|
||||
)
|
||||
|
||||
sinon.assert.calledWith(
|
||||
ctx.SubscriptionModel.updateOne,
|
||||
{ _id: subscriptionId },
|
||||
{
|
||||
$set: {
|
||||
'lastSuccesfulSubscription.planCode': planCode,
|
||||
'lastSuccesfulSubscription.addOns': addOns,
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should increment revertedDueToFailedPayment if consumed is true', async function (ctx) {
|
||||
const consumed = true
|
||||
const subscriptionId = new ObjectId()
|
||||
|
||||
await ctx.SubscriptionUpdater.promises.setRestorePoint(
|
||||
subscriptionId,
|
||||
null,
|
||||
null,
|
||||
consumed
|
||||
)
|
||||
|
||||
sinon.assert.calledWith(
|
||||
ctx.SubscriptionModel.updateOne,
|
||||
{ _id: subscriptionId },
|
||||
{
|
||||
$set: {
|
||||
'lastSuccesfulSubscription.planCode': null,
|
||||
'lastSuccesfulSubscription.addOns': null,
|
||||
},
|
||||
$inc: { timesRevertedDueToFailedPayment: 1 },
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setSubscriptionWasReverted', function () {
|
||||
it('should clear the restore point and mark the subscription as reverted', async function (ctx) {
|
||||
const subscriptionId = new ObjectId().toString()
|
||||
|
||||
await ctx.SubscriptionUpdater.promises.setSubscriptionWasReverted(
|
||||
subscriptionId
|
||||
)
|
||||
|
||||
ctx.SubscriptionModel.updateOne.should.have.been.calledWith(
|
||||
{ _id: subscriptionId },
|
||||
{
|
||||
$set: {
|
||||
'lastSuccesfulSubscription.planCode': null,
|
||||
'lastSuccesfulSubscription.addOns': null,
|
||||
},
|
||||
$inc: { timesRevertedDueToFailedPayment: 1 },
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('voidRestorePoint', function () {
|
||||
it('should clear the restore point without marking the subscription as reverted', async function (ctx) {
|
||||
const subscriptionId = new ObjectId().toString()
|
||||
|
||||
await ctx.SubscriptionUpdater.promises.voidRestorePoint(subscriptionId)
|
||||
|
||||
sinon.assert.calledWith(
|
||||
ctx.SubscriptionModel.updateOne,
|
||||
{ _id: subscriptionId },
|
||||
{
|
||||
$set: {
|
||||
'lastSuccesfulSubscription.planCode': null,
|
||||
'lastSuccesfulSubscription.addOns': null,
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user