🐛 Fixed plan upgrade not cancelling trial (#18699)

closes https://github.com/TryGhost/Product/issues/4036

Fixed a bug where a member on a trial plan would not have their trial
cancelled when they upgraded to a paid plan
This commit is contained in:
Michael Barrett 2023-10-20 08:52:08 +01:00 committed by GitHub
parent 93382df314
commit 094ea1d2b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 66 additions and 10 deletions

View File

@ -556,15 +556,7 @@ export function subscriptionHasFreeTrial({sub} = {}) {
}
export function isInThePast(date) {
const today = new Date();
// 👇️ OPTIONAL!
// This line sets the hour of the current date to midnight
// so the comparison only returns `true` if the passed in date
// is at least yesterday
today.setHours(0, 0, 0, 0);
return date < today;
return date < new Date();
}
export function getProductFromPrice({site, priceId}) {

View File

@ -1,4 +1,4 @@
import {getAllProductsForSite, getAvailableProducts, getCurrencySymbol, getFreeProduct, getMemberName, getMemberSubscription, getPriceFromSubscription, getPriceIdFromPageQuery, getSupportAddress, getUrlHistory, hasMultipleProducts, isActiveOffer, isInviteOnlySite, isPaidMember, isSameCurrency, transformApiTiersData, isSigninAllowed, isSignupAllowed, getCompExpiry} from './helpers';
import {getAllProductsForSite, getAvailableProducts, getCurrencySymbol, getFreeProduct, getMemberName, getMemberSubscription, getPriceFromSubscription, getPriceIdFromPageQuery, getSupportAddress, getUrlHistory, hasMultipleProducts, isActiveOffer, isInviteOnlySite, isPaidMember, isSameCurrency, transformApiTiersData, isSigninAllowed, isSignupAllowed, getCompExpiry, isInThePast} from './helpers';
import * as Fixtures from './fixtures-generator';
import {site as FixturesSite, member as FixtureMember, offer as FixtureOffer, transformTierFixture as TransformFixtureTiers} from '../utils/test-fixtures';
import {isComplimentaryMember} from '../utils/helpers';
@ -448,4 +448,17 @@ describe('Helpers - ', () => {
expect(getCompExpiry({member})).toEqual('');
});
});
describe('isInThePast', () => {
it('returns a boolean indicating if the provided date is in the past', () => {
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - 1);
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 1);
expect(isInThePast(pastDate)).toEqual(true);
expect(isInThePast(futureDate)).toEqual(false);
});
});
});

View File

@ -21,6 +21,8 @@ const messages = {
invalidEmail: 'Invalid Email'
};
const SUBSCRIPTION_STATUS_TRIALING = 'trialing';
/**
* @typedef {object} ITokenService
* @prop {(token: string) => Promise<import('jsonwebtoken').JwtPayload>} decodeToken
@ -1367,6 +1369,10 @@ module.exports = class MemberRepository {
data.subscription.price
);
updatedSubscription = await this._stripeAPIService.removeCouponFromSubscription(subscription.id);
if (subscriptionModel.get('status') === SUBSCRIPTION_STATUS_TRIALING) {
updatedSubscription = await this._stripeAPIService.cancelSubscriptionTrial(subscription.id);
}
}
}

View File

@ -759,4 +759,16 @@ module.exports = class StripeAPI {
default_payment_method: paymentMethod
});
}
/**
* @param {string} id - The ID of the subscription to cancel the trial for
*
* @returns {Promise<import('stripe').Stripe.Subscription>}
*/
async cancelSubscriptionTrial(id) {
await this._rateLimitBucket.throttle();
return this._stripe.subscriptions.update(id, {
trial_end: 'now'
});
}
};

View File

@ -250,4 +250,37 @@ describe('StripeAPI', function () {
});
});
});
describe('cancelSubscriptionTrial', function () {
const mockSubscription = {
id: 'sub_123'
};
beforeEach(function () {
mockStripe = {
subscriptions: {
update: sinon.stub().resolves(mockSubscription)
}
};
const mockStripeConstructor = sinon.stub().returns(mockStripe);
StripeAPI.__set__('Stripe', mockStripeConstructor);
api.configure({
secretKey: ''
});
});
afterEach(function () {
sinon.restore();
});
it('cancels a subscription trial', async function () {
const result = await api.cancelSubscriptionTrial(mockSubscription.id);
should.equal(mockStripe.subscriptions.update.callCount, 1);
should.equal(mockStripe.subscriptions.update.args[0][0], mockSubscription.id);
should.deepEqual(mockStripe.subscriptions.update.args[0][1], {trial_end: 'now'});
should.deepEqual(result, mockSubscription);
});
});
});