33458daae8
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
302 lines
9.9 KiB
JavaScript
302 lines
9.9 KiB
JavaScript
const assert = require('assert');
|
|
const sinon = require('sinon');
|
|
const knex = require('knex');
|
|
const {Tier} = require('@tryghost/tiers');
|
|
const PaymentsService = require('../../lib/payments');
|
|
|
|
describe('PaymentsService', function () {
|
|
let Bookshelf;
|
|
let db;
|
|
|
|
before(async function () {
|
|
db = knex({
|
|
client: 'sqlite3',
|
|
useNullAsDefault: true,
|
|
connection: {
|
|
filename: ':memory:'
|
|
}
|
|
});
|
|
await db.schema.createTable('offers', function (table) {
|
|
table.string('id', 24);
|
|
table.string('stripe_coupon_id', 255);
|
|
table.string('discount_type', 191);
|
|
});
|
|
await db.schema.createTable('stripe_products', function (table) {
|
|
table.string('id', 24);
|
|
table.string('product_id', 24);
|
|
table.string('stripe_product_id', 255);
|
|
});
|
|
await db.schema.createTable('stripe_prices', function (table) {
|
|
table.string('id', 24);
|
|
table.string('stripe_price_id', 255);
|
|
table.string('stripe_product_id', 255);
|
|
table.boolean('active');
|
|
table.string('nickname', 191);
|
|
table.string('currency', 50);
|
|
table.integer('amount');
|
|
table.string('type', 50);
|
|
table.string('interval', 50);
|
|
});
|
|
await db.schema.createTable('stripe_customers', function (table) {
|
|
table.string('id', 24);
|
|
table.string('member_id', 24);
|
|
table.string('stripe_customer_id', 255);
|
|
table.string('name', 191);
|
|
table.string('email', 191);
|
|
});
|
|
|
|
Bookshelf = require('bookshelf')(db);
|
|
});
|
|
|
|
beforeEach(async function () {
|
|
await db('offers').truncate();
|
|
await db('stripe_products').truncate();
|
|
await db('stripe_prices').truncate();
|
|
await db('stripe_customers').truncate();
|
|
});
|
|
|
|
after(async function () {
|
|
await db.destroy();
|
|
});
|
|
|
|
describe('getPaymentLink', function () {
|
|
it('Can handle 404 from Stripe', async function () {
|
|
const BaseModel = Bookshelf.Model.extend({}, {
|
|
async add() {},
|
|
async edit() {}
|
|
});
|
|
const Offer = BaseModel.extend({
|
|
tableName: 'offers'
|
|
});
|
|
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.rejects(new Error('Price does not exist')),
|
|
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
|
|
});
|
|
|
|
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 = null;
|
|
const member = null;
|
|
const metadata = {};
|
|
const options = {};
|
|
|
|
const url = await service.getPaymentLink({
|
|
tier,
|
|
cadence,
|
|
offer,
|
|
member,
|
|
metadata,
|
|
options
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|