diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/offers.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/offers.test.js.snap index eae28fa6f2..dab5945d1a 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/offers.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/offers.test.js.snap @@ -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 [ diff --git a/ghost/core/test/e2e-api/admin/offers.test.js b/ghost/core/test/e2e-api/admin/offers.test.js index d03ed68e90..8ba43552b7 100644 --- a/ghost/core/test/e2e-api/admin/offers.test.js +++ b/ghost/core/test/e2e-api/admin/offers.test.js @@ -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 diff --git a/ghost/members-api/lib/controllers/router.js b/ghost/members-api/lib/controllers/router.js index 121772ac91..613cec4ebf 100644 --- a/ghost/members-api/lib/controllers/router.js +++ b/ghost/members-api/lib/controllers/router.js @@ -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'); } diff --git a/ghost/payments/lib/payments.js b/ghost/payments/lib/payments.js index a87e1df093..1103a41631 100644 --- a/ghost/payments/lib/payments.js +++ b/ghost/payments/lib/payments.js @@ -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) {