Merged v5.22.3 into main

v5.22.3
This commit is contained in:
Hannah Wolfe 2022-11-01 17:29:16 +00:00
commit 85c5a19f33
No known key found for this signature in database
GPG Key ID: AB586C3B5AE5C037
8 changed files with 189 additions and 8 deletions

View File

@ -1,6 +1,6 @@
{
"name": "ghost-admin",
"version": "5.22.2",
"version": "5.22.3",
"description": "Ember.js admin client for Ghost",
"author": "Ghost Foundation",
"homepage": "http://ghost.org",

View File

@ -1,6 +1,6 @@
{
"name": "ghost",
"version": "5.22.2",
"version": "5.22.3",
"description": "The professional publishing platform",
"author": "Ghost Foundation",
"homepage": "https://ghost.org",

View File

@ -1,5 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Create Stripe Checkout Session Can create a checkout session when using offers 1: [body] 1`] = `
Object {
"url": "https://site.com",
}
`;
exports[`Create Stripe Checkout Session Can create a checkout session when using offers 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 Can create a checkout session without passing a customerEmail 1: [body] 1`] = `
Object {
"url": "https://site.com",
}
`;
exports[`Create Stripe Checkout Session Can create a checkout session without passing a customerEmail 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 allow to create a checkout session if the customerEmail is not associated with a paid member 1: [body] 1`] = `
Object {
"url": "https://site.com",

View File

@ -1,3 +1,4 @@
const querystring = require('querystring');
const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework');
const nock = require('nock');
const should = require('should');
@ -55,6 +56,133 @@ describe('Create Stripe Checkout Session', function () {
});
});
it('Can create a checkout session when using offers', async function () {
const {body: {tiers}} = await adminAgent.get('/tiers/?include=monthly_price&yearly_price');
const paidTier = tiers.find(tier => tier.type === 'paid');
const {body: {offers: [offer]}} = await adminAgent.post('/offers/').body({
offers: [{
name: 'Test Offer',
code: 'test-offer',
cadence: 'month',
status: 'active',
currency: 'usd',
type: 'percent',
amount: 20,
duration: 'once',
duration_in_months: null,
display_title: 'Test Offer',
display_description: null,
tier: {
id: paidTier.id
}
}]
});
nock('https://api.stripe.com')
.persist()
.get(/v1\/.*/)
.reply((uri, body) => {
const [match, resource, id] = uri.match(/\/v1\/(\w+)\/(.+)\/?/) || [null];
if (match) {
if (resource === 'products') {
return [200, {
id: id,
active: true
}];
}
if (resource === 'prices') {
return [200, {
id: id,
active: true,
currency: 'usd',
unit_amount: 500
}];
}
}
return [500];
});
nock('https://api.stripe.com')
.persist()
.post(/v1\/.*/)
.reply((uri, body) => {
if (uri === '/v1/checkout/sessions') {
return [200, {id: 'cs_123', url: 'https://site.com'}];
}
if (uri === '/v1/coupons') {
return [200, {id: 'coupon_123', url: 'https://site.com'}];
}
return [500];
});
await membersAgent.post('/api/create-stripe-checkout-session/')
.body({
customerEmail: 'free@test.com',
offerId: offer.id
})
.expectStatus(200)
.matchBodySnapshot()
.matchHeaderSnapshot();
});
it('Can create a checkout session without passing a customerEmail', 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()
.get(/v1\/.*/)
.reply((uri, body) => {
const [match, resource, id] = uri.match(/\/v1\/(\w+)\/(.+)\/?/) || [null];
if (match) {
if (resource === 'products') {
return [200, {
id: id,
active: true
}];
}
if (resource === 'prices') {
return [200, {
id: id,
active: true,
currency: 'usd',
unit_amount: 500
}];
}
}
return [500];
});
nock('https://api.stripe.com')
.persist()
.post(/v1\/.*/)
.reply((uri, body) => {
if (uri === '/v1/checkout/sessions') {
const bodyJSON = querystring.parse(body);
// TODO: Actually work out what Stripe checks and when/how it errors
if (bodyJSON.customerEmail) {
return [400, {error: 'Invalid Email'}];
}
return [200, {id: 'cs_123', url: 'https://site.com'}];
}
return [500];
});
await membersAgent.post('/api/create-stripe-checkout-session/')
.body({
tierId: paidTier.id,
cadence: 'month'
})
.expectStatus(200)
.matchBodySnapshot()
.matchHeaderSnapshot();
});
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');

View File

@ -142,7 +142,7 @@ module.exports = class RouterController {
async createCheckoutSession(req, res) {
let ghostPriceId = req.body.priceId;
const tierId = req.body.tierId;
const cadence = req.body.cadence;
let cadence = req.body.cadence;
const identity = req.body.identity;
const offerId = req.body.offerId;
const metadata = req.body.metadata ?? {};
@ -185,6 +185,7 @@ module.exports = class RouterController {
if (offerId) {
offer = await this._offersAPI.getOffer({id: offerId});
tier = await this._tiersService.api.read(offer.tier.id);
cadence = offer.cadence;
} else {
offer = null;
tier = await this._tiersService.api.read(tierId);

View File

@ -67,7 +67,7 @@ class PaymentsService {
let coupon = null;
let trialDays = null;
if (offer) {
if (offer.tier.id !== tier.id) {
if (!tier.id.equals(offer.tier.id)) {
throw new BadRequestError({
message: 'This Offer is not valid for the Tier'
});
@ -86,11 +86,13 @@ class PaymentsService {
const price = await this.getPriceForTierCadence(tier, cadence);
const email = options.email || null;
const session = await this.stripeAPIService.createCheckoutSession(price.id, customer, {
metadata,
successUrl: options.successUrl,
cancelUrl: options.cancelUrl,
customerEmail: options.email,
customerEmail: customer ? email : null,
trialDays: trialDays ?? tier.trialDays,
coupon: coupon?.id
});
@ -119,8 +121,8 @@ class PaymentsService {
async createCustomerForMember(member) {
const customer = await this.stripeAPIService.createCustomer({
email: member.email,
name: member.name
email: member.get('email'),
name: member.get('name')
});
await this.StripeCustomerModel.add({

View File

@ -300,6 +300,11 @@ export function getAvailableProducts({site}) {
}
return products.filter(product => !!product).filter((product) => {
if (site.is_stripe_configured) {
return true;
}
return product.type !== 'paid';
}).filter((product) => {
return !!(product.monthlyPrice && product.yearlyPrice);
}).filter((product) => {
return !!(Object.keys(product.monthlyPrice).length > 0 && Object.keys(product.yearlyPrice).length > 0);

View File

@ -1,4 +1,4 @@
import {getCurrencySymbol, getFreeProduct, getMemberName, getMemberSubscription, getPriceFromSubscription, getPriceIdFromPageQuery, getSupportAddress, getUrlHistory, hasMultipleProducts, isActiveOffer, isInviteOnlySite, isPaidMember, isSameCurrency, transformApiTiersData} from './helpers';
import {getAvailableProducts, getCurrencySymbol, getFreeProduct, getMemberName, getMemberSubscription, getPriceFromSubscription, getPriceIdFromPageQuery, getSupportAddress, getUrlHistory, hasMultipleProducts, isActiveOffer, isInviteOnlySite, isPaidMember, isSameCurrency, transformApiTiersData} 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';
@ -294,4 +294,17 @@ describe('Helpers - ', () => {
expect(urlHistory).toBeUndefined();
});
});
describe('getAvailableProducts', () => {
it('Does not include paid Tiers when stripe is not configured', () => {
const actual = getAvailableProducts({
site: {
...FixturesSite.multipleTiers.basic,
is_stripe_configured: false
}
});
expect(actual.length).toBe(0);
});
});
});