mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Replaced Products with Tiers API endpoints
ref: https://github.com/TryGhost/Team/issues/1145
ref: 8f8b7e7364
- The /products/ endpoint was replaced with /tiers/ some time ago but we didn't finish the switch
- The work is complete now, so can remove the endpoint entirely and cleanup remaining usages
This commit is contained in:
parent
a58ac016be
commit
5cc7a54edc
14 changed files with 14 additions and 560 deletions
|
@ -85,10 +85,6 @@ module.exports = {
|
|||
return shared.pipeline(require('./offers'), localUtils);
|
||||
},
|
||||
|
||||
get products() {
|
||||
return shared.pipeline(require('./products'), localUtils);
|
||||
},
|
||||
|
||||
get tiers() {
|
||||
return shared.pipeline(require('./tiers'), localUtils);
|
||||
},
|
||||
|
@ -209,10 +205,6 @@ module.exports = {
|
|||
return shared.pipeline(require('./authors-public'), localUtils, 'content');
|
||||
},
|
||||
|
||||
get productsPublic() {
|
||||
return shared.pipeline(require('./products-public'), localUtils, 'content');
|
||||
},
|
||||
|
||||
get tiersPublic() {
|
||||
return shared.pipeline(require('./tiers-public'), localUtils, 'content');
|
||||
},
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
// NOTE: We must not cache references to membersService.api
|
||||
// as it is a getter and may change during runtime.
|
||||
const membersService = require('../../services/members');
|
||||
|
||||
const allowedIncludes = ['stripe_prices', 'monthly_price', 'yearly_price', 'benefits'];
|
||||
|
||||
module.exports = {
|
||||
docName: 'products',
|
||||
|
||||
browse: {
|
||||
options: [
|
||||
'limit',
|
||||
'fields',
|
||||
'include',
|
||||
'filter',
|
||||
'order',
|
||||
'debug',
|
||||
'page'
|
||||
],
|
||||
permissions: true,
|
||||
validation: {
|
||||
options: {
|
||||
include: {
|
||||
values: allowedIncludes
|
||||
}
|
||||
}
|
||||
},
|
||||
async query(frame) {
|
||||
const page = await membersService.api.productRepository.list(frame.options);
|
||||
|
||||
return page;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,116 +0,0 @@
|
|||
// NOTE: We must not cache references to membersService.api
|
||||
// as it is a getter and may change during runtime.
|
||||
const errors = require('@tryghost/errors');
|
||||
const membersService = require('../../services/members');
|
||||
|
||||
const tpl = require('@tryghost/tpl');
|
||||
|
||||
const allowedIncludes = ['stripe_prices', 'monthly_price', 'yearly_price', 'benefits'];
|
||||
|
||||
const messages = {
|
||||
productNotFound: 'Product not found.'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
docName: 'products',
|
||||
|
||||
browse: {
|
||||
options: [
|
||||
'limit',
|
||||
'fields',
|
||||
'include',
|
||||
'filter',
|
||||
'order',
|
||||
'debug',
|
||||
'page'
|
||||
],
|
||||
permissions: true,
|
||||
validation: {
|
||||
options: {
|
||||
include: {
|
||||
values: allowedIncludes
|
||||
}
|
||||
}
|
||||
},
|
||||
async query(frame) {
|
||||
const page = await membersService.api.productRepository.list(frame.options);
|
||||
|
||||
return page;
|
||||
}
|
||||
},
|
||||
|
||||
read: {
|
||||
options: [
|
||||
'include'
|
||||
],
|
||||
headers: {},
|
||||
data: [
|
||||
'id'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
include: {
|
||||
values: allowedIncludes
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
async query(frame) {
|
||||
const model = await membersService.api.productRepository.get(frame.data, frame.options);
|
||||
|
||||
if (!model) {
|
||||
throw new errors.NotFoundError({
|
||||
message: tpl(messages.productNotFound)
|
||||
});
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
},
|
||||
|
||||
add: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
cacheInvalidate: true
|
||||
},
|
||||
validation: {
|
||||
data: {
|
||||
name: {required: true}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
async query(frame) {
|
||||
const model = await membersService.api.productRepository.create(
|
||||
frame.data,
|
||||
frame.options
|
||||
);
|
||||
return model;
|
||||
}
|
||||
},
|
||||
|
||||
edit: {
|
||||
statusCode: 200,
|
||||
options: [
|
||||
'id'
|
||||
],
|
||||
headers: {
|
||||
cacheInvalidate: true
|
||||
},
|
||||
validation: {
|
||||
options: {
|
||||
id: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
async query(frame) {
|
||||
const model = await membersService.api.productRepository.update(
|
||||
frame.data,
|
||||
frame.options
|
||||
);
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -39,10 +39,6 @@ module.exports = {
|
|||
return require('./media');
|
||||
},
|
||||
|
||||
get products() {
|
||||
return require('./products');
|
||||
},
|
||||
|
||||
get tiers() {
|
||||
return require('./tiers');
|
||||
},
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
module.exports = {
|
||||
all(_apiConfig, frame) {
|
||||
if (!frame.options.withRelated) {
|
||||
return;
|
||||
}
|
||||
|
||||
frame.options.withRelated = frame.options.withRelated.map((relation) => {
|
||||
if (relation === 'stripe_prices') {
|
||||
return 'stripePrices';
|
||||
}
|
||||
if (relation === 'monthly_price') {
|
||||
return 'monthlyPrice';
|
||||
}
|
||||
if (relation === 'yearly_price') {
|
||||
return 'yearlyPrice';
|
||||
}
|
||||
return relation;
|
||||
});
|
||||
},
|
||||
|
||||
add(_apiConfig, frame) {
|
||||
frame.data = frame.data.products[0];
|
||||
},
|
||||
|
||||
edit(_apiConfig, frame) {
|
||||
frame.data = frame.data.products[0];
|
||||
}
|
||||
};
|
|
@ -61,10 +61,6 @@ module.exports = {
|
|||
return require('./members');
|
||||
},
|
||||
|
||||
get products() {
|
||||
return require('./products');
|
||||
},
|
||||
|
||||
get tiers() {
|
||||
return require('./tiers');
|
||||
},
|
||||
|
|
|
@ -1,213 +0,0 @@
|
|||
//@ts-check
|
||||
const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:products');
|
||||
const _ = require('lodash');
|
||||
const utils = require('../../../../shared/utils');
|
||||
|
||||
const allowedIncludes = ['stripe_prices', 'monthly_price', 'yearly_price'];
|
||||
|
||||
module.exports = {
|
||||
browse: createSerializer('browse', paginatedProducts),
|
||||
read: createSerializer('read', singleProduct),
|
||||
edit: createSerializer('edit', singleProduct),
|
||||
add: createSerializer('add', singleProduct)
|
||||
};
|
||||
|
||||
/**
|
||||
* @template PageMeta
|
||||
*
|
||||
* @param {{data: import('bookshelf').Model[], meta: PageMeta}} page
|
||||
* @param {APIConfig} _apiConfig
|
||||
* @param {Frame} frame
|
||||
*
|
||||
* @returns {{products: SerializedProduct[], meta: PageMeta}}
|
||||
*/
|
||||
function paginatedProducts(page, _apiConfig, frame) {
|
||||
const requestedQueryIncludes = frame.original && frame.original.query && frame.original.query.include && frame.original.query.include.split(',') || [];
|
||||
const requestedOptionsIncludes = utils.options.trimAndLowerCase(frame.original && frame.original.options && frame.original.options.include || []);
|
||||
return {
|
||||
products: page.data.map((model) => {
|
||||
return cleanIncludes(
|
||||
allowedIncludes,
|
||||
requestedQueryIncludes.concat(requestedOptionsIncludes),
|
||||
serializeProduct(model, frame.options, frame.apiType)
|
||||
);
|
||||
}),
|
||||
meta: page.meta
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('bookshelf').Model} model
|
||||
* @param {APIConfig} _apiConfig
|
||||
* @param {Frame} frame
|
||||
*
|
||||
* @returns {{products: SerializedProduct[]}}
|
||||
*/
|
||||
function singleProduct(model, _apiConfig, frame) {
|
||||
const requestedQueryIncludes = frame.original && frame.original.query && frame.original.query.include && frame.original.query.include.split(',') || [];
|
||||
const requestedOptionsIncludes = frame.original && frame.original.options && frame.original.options.include || [];
|
||||
return {
|
||||
products: [
|
||||
cleanIncludes(
|
||||
allowedIncludes,
|
||||
requestedQueryIncludes.concat(requestedOptionsIncludes),
|
||||
serializeProduct(model, frame.options, frame.apiType)
|
||||
)
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('bookshelf').Model} product
|
||||
* @param {object} options
|
||||
* @param {'content'|'admin'} apiType
|
||||
*
|
||||
* @returns {SerializedProduct}
|
||||
*/
|
||||
function serializeProduct(product, options, apiType) {
|
||||
const json = product.toJSON(options);
|
||||
|
||||
const hideStripeData = apiType === 'content';
|
||||
|
||||
const serialized = {
|
||||
id: json.id,
|
||||
name: json.name,
|
||||
description: json.description,
|
||||
slug: json.slug,
|
||||
active: json.active,
|
||||
visibility: json.visibility,
|
||||
type: json.type,
|
||||
welcome_page_url: json.welcome_page_url,
|
||||
created_at: json.created_at,
|
||||
updated_at: json.updated_at,
|
||||
stripe_prices: json.stripePrices ? json.stripePrices.map(price => serializeStripePrice(price, hideStripeData)) : null,
|
||||
monthly_price: serializeStripePrice(json.monthlyPrice, hideStripeData),
|
||||
yearly_price: serializeStripePrice(json.yearlyPrice, hideStripeData),
|
||||
benefits: json.benefits || null
|
||||
};
|
||||
|
||||
return serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} data
|
||||
* @param {boolean} hideStripeData
|
||||
*
|
||||
* @returns {StripePrice}
|
||||
*/
|
||||
function serializeStripePrice(data, hideStripeData) {
|
||||
if (_.isEmpty(data)) {
|
||||
return null;
|
||||
}
|
||||
const price = {
|
||||
id: data.id,
|
||||
stripe_product_id: data.stripe_product_id,
|
||||
stripe_price_id: data.stripe_price_id,
|
||||
active: data.active,
|
||||
nickname: data.nickname,
|
||||
description: data.description,
|
||||
currency: data.currency,
|
||||
amount: data.amount,
|
||||
type: data.type,
|
||||
interval: data.interval,
|
||||
created_at: data.created_at,
|
||||
updated_at: data.updated_at
|
||||
};
|
||||
|
||||
if (hideStripeData) {
|
||||
delete price.stripe_price_id;
|
||||
delete price.stripe_product_id;
|
||||
}
|
||||
|
||||
return price;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template Data
|
||||
*
|
||||
* @param {string[]} allowed
|
||||
* @param {string[]} requested
|
||||
* @param {Data & Object<string, any>} data
|
||||
*
|
||||
* @returns {Data}
|
||||
*/
|
||||
function cleanIncludes(allowed, requested, data) {
|
||||
const cleaned = {
|
||||
...data
|
||||
};
|
||||
for (const include of allowed) {
|
||||
if (!requested.includes(include)) {
|
||||
delete cleaned[include];
|
||||
}
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template Data
|
||||
* @template Response
|
||||
* @param {string} debugString
|
||||
* @param {(data: Data, apiConfig: APIConfig, frame: Frame) => Response} serialize - A function to serialize the data into an object suitable for API response
|
||||
*
|
||||
* @returns {(data: Data, apiConfig: APIConfig, frame: Frame) => void}
|
||||
*/
|
||||
function createSerializer(debugString, serialize) {
|
||||
return function serializer(data, apiConfig, frame) {
|
||||
debug(debugString);
|
||||
const response = serialize(data, apiConfig, frame);
|
||||
frame.response = response;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} SerializedProduct
|
||||
* @prop {string} id
|
||||
* @prop {string} name
|
||||
* @prop {string} slug
|
||||
* @prop {string} description
|
||||
* @prop {boolean} active
|
||||
* @prop {string} type
|
||||
* @prop {string} welcome_page_url
|
||||
* @prop {Date} created_at
|
||||
* @prop {Date} updated_at
|
||||
* @prop {StripePrice[]} [stripe_prices]
|
||||
* @prop {StripePrice} [monthly_price]
|
||||
* @prop {StripePrice} [yearly_price]
|
||||
* @prop {Benefit[]} [benefits]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Benefit
|
||||
* @prop {string} id
|
||||
* @prop {string} name
|
||||
* @prop {string} slug
|
||||
* @prop {Date} created_at
|
||||
* @prop {Date} updated_at
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} StripePrice
|
||||
* @prop {string} id
|
||||
* @prop {string|null} stripe_product_id
|
||||
* @prop {string|null} stripe_price_id
|
||||
* @prop {boolean} active
|
||||
* @prop {string} nickname
|
||||
* @prop {string} description
|
||||
* @prop {string} currency
|
||||
* @prop {number} amount
|
||||
* @prop {'recurring'|'one-time'} type
|
||||
* @prop {'day'|'week'|'month'|'year'} interval
|
||||
* @prop {Date} created_at
|
||||
* @prop {Date} updated_at
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} APIConfig
|
||||
* @prop {string} docName
|
||||
* @prop {string} method
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object<string, any>} Frame
|
||||
* @prop {Object} options
|
||||
*/
|
|
@ -86,13 +86,6 @@ module.exports = function apiRoutes() {
|
|||
router.put('/tags/:id', mw.authAdminApi, http(api.tags.edit));
|
||||
router.del('/tags/:id', mw.authAdminApi, http(api.tags.destroy));
|
||||
|
||||
// Products
|
||||
// TODO Remove
|
||||
router.get('/products', mw.authAdminApi, http(api.products.browse));
|
||||
router.post('/products', mw.authAdminApi, http(api.products.add));
|
||||
router.get('/products/:id', mw.authAdminApi, http(api.products.read));
|
||||
router.put('/products/:id', mw.authAdminApi, http(api.products.edit));
|
||||
|
||||
// Tiers
|
||||
router.get('/tiers', mw.authAdminApi, http(api.tiers.browse));
|
||||
router.post('/tiers', mw.authAdminApi, http(api.tiers.add));
|
||||
|
|
|
@ -32,10 +32,9 @@ module.exports = function apiRoutes() {
|
|||
// ## Settings
|
||||
router.get('/settings', mw.authenticatePublic, http(api.publicSettings.browse));
|
||||
|
||||
router.get('/products', mw.authenticatePublic, http(api.productsPublic.browse));
|
||||
router.get('/tiers', mw.authenticatePublic, http(api.tiersPublic.browse));
|
||||
// ## Members
|
||||
router.get('/newsletters', mw.authenticatePublic, http(api.newslettersPublic.browse));
|
||||
|
||||
router.get('/tiers', mw.authenticatePublic, http(api.tiersPublic.browse));
|
||||
router.get('/offers/:id', mw.authenticatePublic, http(api.offersPublic.read));
|
||||
|
||||
return router;
|
||||
|
|
|
@ -128,7 +128,7 @@ describe('Pages API', function () {
|
|||
});
|
||||
|
||||
it('Can include specific tier for page with tiers visibility', async function () {
|
||||
const res = await request.get(localUtils.API.getApiQuery('products/'))
|
||||
const res = await request.get(localUtils.API.getApiQuery('tiers/'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
|
@ -136,7 +136,7 @@ describe('Pages API', function () {
|
|||
|
||||
const jsonResponse = res.body;
|
||||
|
||||
const paidTier = jsonResponse.products.find(p => p.type === 'paid');
|
||||
const paidTier = jsonResponse.tiers.find(p => p.type === 'paid');
|
||||
|
||||
const tiersPage = testUtils.DataGenerator.forKnex.createPost({
|
||||
type: 'page',
|
||||
|
@ -199,15 +199,15 @@ describe('Pages API', function () {
|
|||
.set('Origin', config.get('url'))
|
||||
.expect(200);
|
||||
|
||||
const resProducts = await request
|
||||
.get(localUtils.API.getApiQuery(`products/`))
|
||||
const resTiers = await request
|
||||
.get(localUtils.API.getApiQuery(`tiers/`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect(200);
|
||||
|
||||
const products = resProducts.body.products;
|
||||
const tiers = resTiers.body.tiers;
|
||||
page.updated_at = res.body.pages[0].updated_at;
|
||||
page.visibility = 'tiers';
|
||||
const paidTiers = products.filter((p) => {
|
||||
const paidTiers = tiers.filter((p) => {
|
||||
return p.type === 'paid';
|
||||
}).map((product) => {
|
||||
return product;
|
||||
|
|
|
@ -354,7 +354,7 @@ describe('Posts API', function () {
|
|||
});
|
||||
|
||||
it('Can include specific tier for post with tiers visibility', async function () {
|
||||
const res = await request.get(localUtils.API.getApiQuery('products/'))
|
||||
const res = await request.get(localUtils.API.getApiQuery('tiers/'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
|
@ -362,7 +362,7 @@ describe('Posts API', function () {
|
|||
|
||||
const jsonResponse = res.body;
|
||||
|
||||
const paidTier = jsonResponse.products.find(p => p.type === 'paid');
|
||||
const paidTier = jsonResponse.tiers.find(p => p.type === 'paid');
|
||||
|
||||
const tiersPost = testUtils.DataGenerator.forKnex.createPost({
|
||||
slug: 'thou-shalt-be-for-specific-tiers',
|
||||
|
|
|
@ -1,131 +0,0 @@
|
|||
const should = require('should');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const {agentProvider, fixtureManager, mockManager} = require('../../utils/e2e-framework');
|
||||
|
||||
describe('Tiers API', function () {
|
||||
let agent;
|
||||
|
||||
before(async function () {
|
||||
agent = await agentProvider.getAdminAPIAgent();
|
||||
await fixtureManager.init('members');
|
||||
await agent.loginAsOwner();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
mockManager.mockLabsDisabled('multipleProducts');
|
||||
const stripeService = require('../../../core/server/services/stripe');
|
||||
stripeService.api._configured = true;
|
||||
mockManager.mockStripe();
|
||||
|
||||
sinon
|
||||
.stub(stripeService.api, 'createProduct')
|
||||
.resolves({
|
||||
id: 'prod_LFPlH9BDDwXcZ1'
|
||||
});
|
||||
|
||||
sinon
|
||||
.stub(stripeService.api, 'createPrice')
|
||||
.resolves({
|
||||
id: 'price_1KYpK92eZvKYlo2C86IrYSPM',
|
||||
currency: 'usd',
|
||||
nickname: null,
|
||||
unit_amount: 299
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
mockManager.restore();
|
||||
const stripeService = require('../../../core/server/services/stripe');
|
||||
stripeService.api._configured = false;
|
||||
});
|
||||
|
||||
it('Errors when price is non-integer', async function () {
|
||||
const tier = {
|
||||
name: 'Blah',
|
||||
monthly_price: {
|
||||
amount: 99.99
|
||||
}
|
||||
};
|
||||
|
||||
await agent
|
||||
.post('/products/')
|
||||
.body({products: [tier]})
|
||||
.expectStatus(422);
|
||||
});
|
||||
|
||||
it('Errors when price is negative', async function () {
|
||||
const tier = {
|
||||
name: 'Blah',
|
||||
monthly_price: {
|
||||
amount: -100
|
||||
}
|
||||
};
|
||||
|
||||
await agent
|
||||
.post('/products/')
|
||||
.body({products: [tier]})
|
||||
.expectStatus(422);
|
||||
});
|
||||
|
||||
it('Errors when price is too large', async function () {
|
||||
const tier = {
|
||||
name: 'Blah',
|
||||
monthly_price: {
|
||||
amount: Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
};
|
||||
|
||||
await agent
|
||||
.post('/products/')
|
||||
.body({products: [tier]})
|
||||
.expectStatus(422);
|
||||
});
|
||||
|
||||
it('Create a new tier with benefits', async function () {
|
||||
const tier = {
|
||||
name: 'Blah',
|
||||
monthly_price: {
|
||||
amount: 10
|
||||
},
|
||||
benefits: [{
|
||||
name: 'This is a benefit'
|
||||
}]
|
||||
};
|
||||
|
||||
await agent
|
||||
.post('/products/')
|
||||
.body({products: [tier]})
|
||||
.expectStatus(201);
|
||||
});
|
||||
|
||||
it('Errors when a benefit has an empty name', async function () {
|
||||
const tier = {
|
||||
name: 'Blah',
|
||||
monthly_price: {
|
||||
amount: 10
|
||||
},
|
||||
benefits: [{
|
||||
name: ''
|
||||
}]
|
||||
};
|
||||
|
||||
await agent
|
||||
.post('/products/')
|
||||
.body({products: [tier]})
|
||||
.expectStatus(422);
|
||||
});
|
||||
|
||||
it('Errors when a product is edited with a benefit that has an empty name', async function () {
|
||||
const tier = {
|
||||
benefits: [{
|
||||
name: ''
|
||||
}]
|
||||
};
|
||||
|
||||
await agent
|
||||
.put('/products/' + fixtureManager.get('products', 0).id)
|
||||
.body({products: [tier]})
|
||||
.expectStatus(422);
|
||||
});
|
||||
});
|
|
@ -108,11 +108,11 @@ describe('Pages Content API', function () {
|
|||
|
||||
it('Can include specific tier for page with tiers visibility', async function () {
|
||||
const res = await agent
|
||||
.get(`products/`)
|
||||
.get(`tiers/`)
|
||||
.expectStatus(200);
|
||||
|
||||
const jsonResponse = res.body;
|
||||
const paidTier = jsonResponse.products.find(p => p.type === 'paid');
|
||||
const paidTier = jsonResponse.tiers.find(p => p.type === 'paid');
|
||||
|
||||
const tiersPage = testUtils.DataGenerator.forKnex.createPost({
|
||||
slug: 'thou-shalt-be-for-specific-tiers',
|
||||
|
|
|
@ -280,11 +280,11 @@ describe('Posts Content API', function () {
|
|||
|
||||
it('Can include specific tier for post with tiers visibility', async function () {
|
||||
const res = await agent
|
||||
.get(`products/`)
|
||||
.get(`tiers/`)
|
||||
.expectStatus(200);
|
||||
|
||||
const jsonResponse = res.body;
|
||||
const paidTier = jsonResponse.products.find(p => p.type === 'paid');
|
||||
const paidTier = jsonResponse.tiers.find(p => p.type === 'paid');
|
||||
|
||||
const tiersPost = testUtils.DataGenerator.forKnex.createPost({
|
||||
slug: 'thou-shalt-be-for-specific-tiers',
|
||||
|
|
Loading…
Add table
Reference in a new issue