🐛 Fixed Stripe Checkout for Members w/ existing subscriptions (#14953)

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

This adds a check for existing subscriptions for a member associated with the
email addressed used for Stripe Checkout, if any are found the Checkout Session
creation fails and responds with a 403.

We've also updated the error handling for the create-stripe-checkout-session
endpoint so that it follows the existing Ghost API patterns.
This commit is contained in:
Fabien 'egg' O'Carroll 2022-06-01 15:53:05 +01:00 committed by GitHub
parent c32b1baa9b
commit 6c455dc1f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 129 additions and 5 deletions

View File

@ -85,7 +85,7 @@
"@tryghost/logging": "2.1.8",
"@tryghost/magic-link": "1.0.26",
"@tryghost/member-events": "0.4.6",
"@tryghost/members-api": "8.1.2",
"@tryghost/members-api": "8.1.3",
"@tryghost/members-events-service": "0.4.3",
"@tryghost/members-importer": "0.5.16",
"@tryghost/members-offers": "0.11.6",

View File

@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Create Stripe Checkout Session Does allow to create a checkout session if the customerEmail is not associated with a paid member 1: [body] 1`] = `
Object {
"publicKey": "pk_test_for_stripe",
"sessionId": "cs_123",
}
`;
exports[`Create Stripe Checkout Session Does allow to create a checkout session if the customerEmail is not associated with a paid member 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "*",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-type": "application/json",
"vary": "Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Create Stripe Checkout Session Does not allow to create a checkout session if the customerEmail is associated with a paid member 1: [body] 1`] = `
Object {
"errors": Array [
Object {
"code": "CANNOT_CHECKOUT_WITH_EXISTING_SUBSCRIPTION",
"context": null,
"details": null,
"ghostErrorCode": null,
"help": null,
"id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"message": "A subscription exists for this Member.",
"property": null,
"type": "NoPermissionError",
},
],
}
`;
exports[`Create Stripe Checkout Session Does not allow to create a checkout session if the customerEmail is associated with a paid member 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "*",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "268",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Encoding",
"x-powered-by": "Express",
}
`;

View File

@ -0,0 +1,76 @@
const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework');
const nock = require('nock');
let membersAgent, adminAgent, membersService;
describe('Create Stripe Checkout Session', function () {
before(async function () {
const agents = await agentProvider.getAgentsForMembers();
membersAgent = agents.membersAgent;
adminAgent = agents.adminAgent;
membersService = require('../../../core/server/services/members');
await fixtureManager.init('members');
await adminAgent.loginAsOwner();
});
beforeEach(function () {
mockManager.mockMail();
mockManager.mockStripe();
});
afterEach(function () {
mockManager.restore();
});
it('Does not allow to create a checkout session if the customerEmail is associated with a paid member', async function () {
const {body: {tiers}} = await adminAgent.get('/tiers/?include=monthly_price&yearly_price');
const paidTier = tiers.find(tier => tier.type === 'paid');
await membersAgent.post('/api/create-stripe-checkout-session/')
.body({
customerEmail: 'paid@test.com',
tierId: paidTier.id,
cadence: 'month'
})
.expectStatus(403)
.matchBodySnapshot({
errors: [{
id: matchers.anyUuid,
code: 'CANNOT_CHECKOUT_WITH_EXISTING_SUBSCRIPTION'
}]
})
.matchHeaderSnapshot({
etag: matchers.anyEtag
});
});
it('Does allow to create a checkout session if the customerEmail is not associated with a paid member', async function () {
const {body: {tiers}} = await adminAgent.get('/tiers/?include=monthly_price&yearly_price');
const paidTier = tiers.find(tier => tier.type === 'paid');
nock('https://api.stripe.com')
.persist()
.post(/v1\/.*/)
.reply((uri, body) => {
if (uri === '/v1/checkout/sessions') {
return [200, {id: 'cs_123'}];
}
return [500];
});
await membersAgent.post('/api/create-stripe-checkout-session/')
.body({
customerEmail: 'free@test.com',
tierId: paidTier.id,
cadence: 'month'
})
.expectStatus(200)
.matchBodySnapshot()
.matchHeaderSnapshot();
});
});

View File

@ -1871,10 +1871,10 @@
"@tryghost/domain-events" "^0.1.14"
"@tryghost/member-events" "^0.4.6"
"@tryghost/members-api@8.1.2":
version "8.1.2"
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-8.1.2.tgz#dd4191ad7cbf0e6687c69153b06b8b5f9ef4709e"
integrity sha512-cD1NrGgPJQfaZBkW0GfTJRq5pcOABu13Tf/BFa4koFi03JUbK3QM92mZPmboks2cpDzkku6EzwFS4D7nbgcS5Q==
"@tryghost/members-api@8.1.3":
version "8.1.3"
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-8.1.3.tgz#8ddcf3d12e31639969a9678b3eb87fb6e49a7c5c"
integrity sha512-XJLXfwSd4EGbk5D9XJ0qOIF/9QBroT51/MN89Vtufw8MLHMx5OxlbIWfCXezrZHADg9GHWyao0xej3MQjGJqAA==
dependencies:
"@nexes/nql" "^0.6.0"
"@tryghost/debug" "^0.1.2"