Handled stripe setup for free trial offers
refs https://github.com/TryGhost/Team/issues/1726 - free trial offers don't need a stripe coupon created for them - checkout sessions for free trial offers ignore stripe coupon and directly pass the trial days value - trial days of an offer take precedence over trial days added as default to a tier
This commit is contained in:
parent
66970e5002
commit
843bbfa55d
@ -120,6 +120,46 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Offers API Can add a trial offer 1: [body] 1`] = `
|
||||
Object {
|
||||
"offers": Array [
|
||||
Object {
|
||||
"amount": 20,
|
||||
"cadence": "year",
|
||||
"code": "4th-trial",
|
||||
"currency": null,
|
||||
"currency_restriction": false,
|
||||
"display_description": "",
|
||||
"display_title": "",
|
||||
"duration": "trial",
|
||||
"duration_in_months": null,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Fourth of July Sales trial",
|
||||
"redemption_count": 0,
|
||||
"status": "active",
|
||||
"tier": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
"type": "trial",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Offers API Can add a trial offer 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "359",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/offers\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-cache-invalidate": "/*",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Offers API Can archive an offer 1: [body] 1`] = `
|
||||
Object {
|
||||
"offers": Array [
|
||||
@ -243,6 +283,26 @@ Object {
|
||||
},
|
||||
"type": "fixed",
|
||||
},
|
||||
Object {
|
||||
"amount": 20,
|
||||
"cadence": "year",
|
||||
"code": "4th-trial",
|
||||
"currency": null,
|
||||
"currency_restriction": false,
|
||||
"display_description": "",
|
||||
"display_title": "",
|
||||
"duration": "trial",
|
||||
"duration_in_months": null,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Fourth of July Sales trial",
|
||||
"redemption_count": 0,
|
||||
"status": "active",
|
||||
"tier": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Default Product",
|
||||
},
|
||||
"type": "trial",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
@ -251,7 +311,7 @@ exports[`Offers API Can browse 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "1491",
|
||||
"content-length": "1863",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
@ -322,6 +382,26 @@ Object {
|
||||
},
|
||||
"type": "fixed",
|
||||
},
|
||||
Object {
|
||||
"amount": 20,
|
||||
"cadence": "year",
|
||||
"code": "4th-trial",
|
||||
"currency": null,
|
||||
"currency_restriction": false,
|
||||
"display_description": "",
|
||||
"display_title": "",
|
||||
"duration": "trial",
|
||||
"duration_in_months": null,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Fourth of July Sales trial",
|
||||
"redemption_count": 0,
|
||||
"status": "active",
|
||||
"tier": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Default Product",
|
||||
},
|
||||
"type": "trial",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
@ -330,7 +410,7 @@ exports[`Offers API Can browse active 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "1089",
|
||||
"content-length": "1461",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
@ -456,6 +536,45 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Offers API Can get a trial offer 1: [body] 1`] = `
|
||||
Object {
|
||||
"offers": Array [
|
||||
Object {
|
||||
"amount": 20,
|
||||
"cadence": "year",
|
||||
"code": "4th-trial",
|
||||
"currency": null,
|
||||
"currency_restriction": false,
|
||||
"display_description": "",
|
||||
"display_title": "",
|
||||
"duration": "trial",
|
||||
"duration_in_months": null,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Fourth of July Sales trial",
|
||||
"redemption_count": 0,
|
||||
"status": "active",
|
||||
"tier": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Default Product",
|
||||
},
|
||||
"type": "trial",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Offers API Can get a trial offer 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "384",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Offers API Cannot create offer with same code 1: [body] 1`] = `
|
||||
Object {
|
||||
"errors": Array [
|
||||
|
@ -16,6 +16,7 @@ async function getFreeProduct() {
|
||||
describe('Offers API', function () {
|
||||
let defaultTier;
|
||||
let savedOffer;
|
||||
let trialOffer;
|
||||
|
||||
before(async function () {
|
||||
agent = await agentProvider.getAdminAPIAgent();
|
||||
@ -53,7 +54,7 @@ describe('Offers API', function () {
|
||||
id: defaultTier.id
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const {body} = await agent
|
||||
.post(`offers/`)
|
||||
.body({offers: [newOffer]})
|
||||
@ -85,7 +86,7 @@ describe('Offers API', function () {
|
||||
id: defaultTier.id
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
await agent
|
||||
.post(`offers/`)
|
||||
.body({offers: [newOffer]})
|
||||
@ -116,7 +117,7 @@ describe('Offers API', function () {
|
||||
id: defaultTier.id
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
await agent
|
||||
.post(`offers/`)
|
||||
.body({offers: [newOffer]})
|
||||
@ -151,7 +152,7 @@ describe('Offers API', function () {
|
||||
id: defaultTier.id
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
await agent
|
||||
.post(`offers/`)
|
||||
.body({offers: [newOffer]})
|
||||
@ -170,6 +171,39 @@ describe('Offers API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('Can add a trial offer', async function () {
|
||||
const newOffer = {
|
||||
name: 'Fourth of July Sales trial',
|
||||
code: '4th-trial',
|
||||
cadence: 'year',
|
||||
amount: 20,
|
||||
duration: 'trial',
|
||||
type: 'trial',
|
||||
currency: 'USD',
|
||||
tier: {
|
||||
id: defaultTier.id
|
||||
}
|
||||
};
|
||||
|
||||
const {body} = await agent
|
||||
.post(`offers/`)
|
||||
.body({offers: [newOffer]})
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag,
|
||||
location: anyLocationFor('offers')
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
offers: [{
|
||||
id: anyObjectId,
|
||||
tier: {
|
||||
id: anyObjectId
|
||||
}
|
||||
}]
|
||||
});
|
||||
trialOffer = body.offers[0];
|
||||
});
|
||||
|
||||
it('Cannot create offer with same code', async function () {
|
||||
const newOffer = {
|
||||
name: 'Fourth of July',
|
||||
@ -183,7 +217,7 @@ describe('Offers API', function () {
|
||||
id: defaultTier.id
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
await agent
|
||||
.post(`offers/`)
|
||||
.body({offers: [newOffer]})
|
||||
@ -211,7 +245,7 @@ describe('Offers API', function () {
|
||||
id: defaultTier.id
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
await agent
|
||||
.post(`offers/`)
|
||||
.body({offers: [newOffer]})
|
||||
@ -239,7 +273,7 @@ describe('Offers API', function () {
|
||||
id: defaultTier.id
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
await agent
|
||||
.post(`offers/`)
|
||||
.body({offers: [newOffer]})
|
||||
@ -262,7 +296,7 @@ describe('Offers API', function () {
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
offers: new Array(4).fill({
|
||||
offers: new Array(5).fill({
|
||||
id: anyObjectId,
|
||||
tier: {
|
||||
id: anyObjectId
|
||||
@ -288,6 +322,25 @@ describe('Offers API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('Can get a trial offer', async function () {
|
||||
await agent
|
||||
.get(`offers/${trialOffer.id}/`)
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
offers: new Array(1).fill({
|
||||
id: anyObjectId,
|
||||
type: 'trial',
|
||||
duration: 'trial',
|
||||
tier: {
|
||||
id: anyObjectId
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('Can edit an offer', async function () {
|
||||
// We can change all fields except discount related fields
|
||||
let updatedOffer = {
|
||||
@ -431,7 +484,7 @@ describe('Offers API', function () {
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
offers: new Array(3).fill({
|
||||
offers: new Array(4).fill({
|
||||
id: anyObjectId,
|
||||
tier: {
|
||||
id: anyObjectId
|
||||
|
@ -168,6 +168,7 @@ module.exports = class RouterController {
|
||||
}
|
||||
|
||||
let couponId = null;
|
||||
let trialDays;
|
||||
if (offerId) {
|
||||
const offer = await this._offersAPI.getOffer({id: offerId});
|
||||
const tier = (await this._productRepository.get(offer.tier)).toJSON();
|
||||
@ -183,9 +184,13 @@ module.exports = class RouterController {
|
||||
} else {
|
||||
ghostPriceId = tier.yearly_price_id;
|
||||
}
|
||||
|
||||
const coupon = await this._paymentsService.getCouponForOffer(offerId);
|
||||
couponId = coupon.id;
|
||||
// Free trial offers don't have a stripe coupon
|
||||
if (offer.type === 'trial') {
|
||||
trialDays = offer.amount;
|
||||
} else {
|
||||
const coupon = await this._paymentsService.getCouponForOffer(offerId);
|
||||
couponId = coupon.id;
|
||||
}
|
||||
|
||||
metadata.offer = offer.id;
|
||||
}
|
||||
@ -214,9 +219,8 @@ module.exports = class RouterController {
|
||||
const priceId = price.get('stripe_price_id');
|
||||
|
||||
const product = await this._productRepository.get({stripe_price_id: priceId});
|
||||
let trialDays;
|
||||
|
||||
if (this.labsService.isSet('freeTrial')) {
|
||||
if (this.labsService.isSet('freeTrial') && !trialDays) {
|
||||
trialDays = product.get('trial_days');
|
||||
}
|
||||
|
||||
|
@ -27,8 +27,8 @@ class PaymentsService {
|
||||
* @returns {Promise<{id: string}>}
|
||||
*/
|
||||
async getCouponForOffer(offerId) {
|
||||
const row = await this.OfferModel.where({id: offerId}).query().select('stripe_coupon_id').first();
|
||||
if (!row) {
|
||||
const row = await this.OfferModel.where({id: offerId}).query().select('stripe_coupon_id', 'discount_type').first();
|
||||
if (!row || row.discount_type === 'trial') {
|
||||
return null;
|
||||
}
|
||||
if (!row.stripe_coupon_id) {
|
||||
|
Loading…
Reference in New Issue
Block a user