🐛 Fixed free trial applied alongside an offer in checkout (#15975)

refs https://github.com/TryGhost/Team/issues/2375

If a discount offer is associated with a tier that has a free trial enabled on full price / standard portal in membership settings, then the stripe checkout applied both the discount and free trial to the member, which is incorrect as we shouldn't be combining both.

- removes trial days from stripe checkout if a coupon is being applied, so only one of them is applied at a time
This commit is contained in:
Rishabh Garg 2022-12-09 18:47:05 +05:30 committed by GitHub
parent 2e87ceae1f
commit 33458daae8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 138 additions and 0 deletions

View File

@ -97,6 +97,11 @@ class PaymentsService {
coupon: coupon?.id
};
// If we already have a coupon, we don't want to give trial days over it
if (data.coupon) {
delete data.trialDays;
}
if (!customer && email) {
data.customerEmail = email;
}

View File

@ -164,5 +164,138 @@ describe('PaymentsService', function () {
assert(url);
});
it('Can remove trial days in case of an existing coupon', async function () {
const BaseModel = Bookshelf.Model.extend({}, {
async add() {},
async edit() {}
});
const Offer = BaseModel.extend({
tableName: 'offers',
where: () => {
return {
query: () => {
return {
select: () => {
return {
first: sinon.stub().resolves({
stripe_coupon_id: 'stripe_coupon_1'
})
};
}
};
}
};
}
});
const StripeProduct = BaseModel.extend({
tableName: 'stripe_products'
});
const StripePrice = BaseModel.extend({
tableName: 'stripe_prices'
});
const StripeCustomer = BaseModel.extend({
tableName: 'stripe_customers'
});
const offersAPI = {};
const stripeAPIService = {
createCheckoutSession: sinon.fake.resolves({
url: 'https://checkout.session'
}),
getCustomer: sinon.fake(),
createCustomer: sinon.fake(),
getProduct: sinon.fake.resolves({
id: 'prod_1',
active: true
}),
editProduct: sinon.fake(),
createProduct: sinon.fake.resolves({
id: 'prod_1',
active: true
}),
getPrice: sinon.fake(function () {
return Promise.resolve({
id: 'price_1'
});
}),
createPrice: sinon.fake(function (data) {
return Promise.resolve({
id: 'price_1',
active: data.active,
unit_amount: data.amount,
currency: data.currency,
nickname: data.nickname,
recurring: {
interval: data.interval
}
});
}),
createCoupon: sinon.fake()
};
const service = new PaymentsService({
Offer,
StripeProduct,
StripePrice,
StripeCustomer,
offersAPI,
stripeAPIService
});
const tier = await Tier.create({
name: 'Test tier',
slug: 'test-tier',
currency: 'usd',
monthlyPrice: 1000,
yearlyPrice: 10000,
trialDays: 7
});
const price = StripePrice.forge({
id: 'id_1',
stripe_price_id: 'price_1',
stripe_product_id: 'prod_1',
active: true,
interval: 'month',
nickname: 'Monthly',
currency: 'usd',
amount: 1000,
type: 'recurring'
});
const product = StripeProduct.forge({
id: 'id_1',
stripe_product_id: 'prod_1',
product_id: tier.id.toHexString()
});
await price.save(null, {method: 'insert'});
await product.save(null, {method: 'insert'});
const cadence = 'month';
const offer = {
id: 'discount_offer_1',
tier: {
id: tier.id.toHexString()
}
};
const member = null;
const metadata = {};
const options = {};
await service.getPaymentLink({
tier,
cadence,
offer,
member,
metadata,
options
});
// assert trialDays should not be set when coupon is present for checkout session
assert.strictEqual(stripeAPIService.createCheckoutSession.getCall(0).args[2].coupon, 'stripe_coupon_1');
assert.strictEqual(stripeAPIService.createCheckoutSession.getCall(0).args[2].trialDays, undefined);
});
});
});