mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-06 22:40:14 -05:00
Stored offer_id in subscriptions (#14488)
refs https://github.com/TryGhost/Team/issues/1519 **Tests:** - Tests whether the metadata from an offer is read correctly and stored in the database - Test that invalid offer ids are ignored **Changes in members repository:** - Compare changes here: https://github.com/TryGhost/Members/compare/%40tryghost/members-api%405.9.2...%40tryghost/members-api%405.11.1 - The `offer_id` column of subscriptions is set based on the coupon id from Stripe - `getByStripeCouponId` method added in the offers repository (required to look up an offer from a stripe_coupon_id) - the `members-payments` package was bumped twice (once for changes, once for undoing those changes, my bad). Nothing else has changed in that package.
This commit is contained in:
parent
3171df4102
commit
0c72e78e6a
3 changed files with 231 additions and 39 deletions
|
@ -83,10 +83,10 @@
|
|||
"@tryghost/logging": "2.1.5",
|
||||
"@tryghost/magic-link": "1.0.21",
|
||||
"@tryghost/member-events": "0.4.1",
|
||||
"@tryghost/members-api": "5.9.2",
|
||||
"@tryghost/members-api": "5.11.1",
|
||||
"@tryghost/members-events-service": "0.3.3",
|
||||
"@tryghost/members-importer": "0.5.8",
|
||||
"@tryghost/members-offers": "0.10.9",
|
||||
"@tryghost/members-offers": "0.11.1",
|
||||
"@tryghost/members-ssr": "1.0.23",
|
||||
"@tryghost/members-stripe-service": "0.10.0",
|
||||
"@tryghost/metrics": "1.0.8",
|
||||
|
|
|
@ -6,6 +6,7 @@ const stripe = require('stripe');
|
|||
const {Product} = require('../../../core/server/models/product');
|
||||
const {agentProvider, mockManager, fixtureManager} = require('../../utils/e2e-framework');
|
||||
const models = require('../../../core/server/models');
|
||||
const offers = require('../../../core/server/services/offers');
|
||||
|
||||
let membersAgent;
|
||||
let adminAgent;
|
||||
|
@ -67,6 +68,7 @@ describe('Members API', function () {
|
|||
const customer = {};
|
||||
const paymentMethod = {};
|
||||
const setupIntent = {};
|
||||
const coupon = {};
|
||||
|
||||
beforeEach(function () {
|
||||
nock('https://api.stripe.com')
|
||||
|
@ -96,6 +98,13 @@ describe('Members API', function () {
|
|||
}
|
||||
return [200, subscription];
|
||||
}
|
||||
|
||||
if (resource === 'coupons') {
|
||||
if (coupon.id !== id) {
|
||||
return [404];
|
||||
}
|
||||
return [200, coupon];
|
||||
}
|
||||
});
|
||||
|
||||
nock('https://api.stripe.com')
|
||||
|
@ -120,6 +129,10 @@ describe('Members API', function () {
|
|||
return [200, customer];
|
||||
}
|
||||
|
||||
if (resource === 'coupons') {
|
||||
return [200, coupon];
|
||||
}
|
||||
|
||||
return [500];
|
||||
});
|
||||
});
|
||||
|
@ -1062,11 +1075,48 @@ describe('Members API', function () {
|
|||
|
||||
describe('Discounts', function () {
|
||||
const beforeNow = Math.floor((Date.now() - 2000) / 1000) * 1000;
|
||||
let offer;
|
||||
let couponId = 'testCoupon123';
|
||||
|
||||
before(async function () {
|
||||
// Create a random offer_id that we'll use
|
||||
// The actual amounts don't matter as we'll only take the ones from Stripe ATM
|
||||
const newOffer = {
|
||||
name: 'Black Friday',
|
||||
code: 'black-friday',
|
||||
display_title: 'Black Friday Sale!',
|
||||
display_description: '10% off on yearly plan',
|
||||
type: 'percent',
|
||||
cadence: 'year',
|
||||
amount: 12,
|
||||
duration: 'once',
|
||||
duration_in_months: null,
|
||||
currency_restriction: false,
|
||||
currency: null,
|
||||
status: 'active',
|
||||
redemption_count: 0,
|
||||
tier: {
|
||||
id: (await getPaidProduct()).id
|
||||
}
|
||||
};
|
||||
|
||||
// Make sure we link this to the right coupon in Stripe
|
||||
// This will store the offer with stripe_coupon_id = couponId
|
||||
set(coupon, {
|
||||
id: couponId
|
||||
});
|
||||
|
||||
const {body} = await adminAgent
|
||||
.post(`offers/`)
|
||||
.body({offers: [newOffer]})
|
||||
.expectStatus(200);
|
||||
offer = body.offers[0];
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper for repetitive tests. It tests the MRR and MRR delta given a discount + a price
|
||||
*/
|
||||
async function testDiscount({discount, interval, unit_amount, assert_mrr}) {
|
||||
async function testDiscount({discount, interval, unit_amount, assert_mrr, offer_id}) {
|
||||
const customer_id = createStripeID('cust');
|
||||
const subscription_id = createStripeID('sub');
|
||||
|
||||
|
@ -1097,7 +1147,8 @@ describe('Members API', function () {
|
|||
},
|
||||
start_date: beforeNow / 1000,
|
||||
current_period_end: Math.floor(beforeNow / 1000) + (60 * 60 * 24 * 31),
|
||||
cancel_at_period_end: false
|
||||
cancel_at_period_end: false,
|
||||
metadata: {}
|
||||
});
|
||||
|
||||
set(customer, {
|
||||
|
@ -1129,7 +1180,8 @@ describe('Members API', function () {
|
|||
|
||||
await membersAgent.post('/webhooks/stripe/')
|
||||
.body(webhookPayload)
|
||||
.header('stripe-signature', webhookSignature);
|
||||
.header('stripe-signature', webhookSignature)
|
||||
.expectStatus(200);
|
||||
|
||||
const {body} = await adminAgent.get(`/members/?search=${customer_id}@email.com`);
|
||||
assert.equal(body.members.length, 1, 'The member was not created');
|
||||
|
@ -1147,7 +1199,8 @@ describe('Members API', function () {
|
|||
plan_interval: interval,
|
||||
plan_currency: 'usd',
|
||||
current_period_end: new Date(Math.floor(beforeNow / 1000) * 1000 + (60 * 60 * 24 * 31 * 1000)),
|
||||
mrr: assert_mrr
|
||||
mrr: assert_mrr,
|
||||
offer_id: offer_id
|
||||
});
|
||||
|
||||
await assertMemberEvents({
|
||||
|
@ -1204,7 +1257,8 @@ describe('Members API', function () {
|
|||
plan_amount: unit_amount,
|
||||
plan_interval: interval,
|
||||
plan_currency: 'usd',
|
||||
mrr: 0
|
||||
mrr: 0,
|
||||
offer_id: offer_id
|
||||
});
|
||||
|
||||
await assertMemberEvents({
|
||||
|
@ -1234,7 +1288,7 @@ describe('Members API', function () {
|
|||
object: 'discount',
|
||||
checkout_session: null,
|
||||
coupon: {
|
||||
id: 'Z4OV52SU',
|
||||
id: couponId, // This coupon id maps to the created offer above
|
||||
object: 'coupon',
|
||||
amount_off: null,
|
||||
created: 1649774041,
|
||||
|
@ -1261,7 +1315,8 @@ describe('Members API', function () {
|
|||
discount,
|
||||
unit_amount: 500,
|
||||
interval: 'month',
|
||||
assert_mrr: 250
|
||||
assert_mrr: 250,
|
||||
offer_id: offer.id
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1271,7 +1326,7 @@ describe('Members API', function () {
|
|||
object: 'discount',
|
||||
checkout_session: null,
|
||||
coupon: {
|
||||
id: 'Z4OV52SU',
|
||||
id: couponId,
|
||||
object: 'coupon',
|
||||
amount_off: null,
|
||||
created: 1649774041,
|
||||
|
@ -1298,7 +1353,8 @@ describe('Members API', function () {
|
|||
discount,
|
||||
unit_amount: 1200,
|
||||
interval: 'year',
|
||||
assert_mrr: 50
|
||||
assert_mrr: 50,
|
||||
offer_id: offer.id
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1308,7 +1364,7 @@ describe('Members API', function () {
|
|||
object: 'discount',
|
||||
checkout_session: null,
|
||||
coupon: {
|
||||
id: 'Z4OV52SU',
|
||||
id: couponId,
|
||||
object: 'coupon',
|
||||
amount_off: 1,
|
||||
created: 1649774041,
|
||||
|
@ -1335,7 +1391,8 @@ describe('Members API', function () {
|
|||
discount,
|
||||
unit_amount: 500,
|
||||
interval: 'month',
|
||||
assert_mrr: 499
|
||||
assert_mrr: 499,
|
||||
offer_id: offer.id
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1345,7 +1402,7 @@ describe('Members API', function () {
|
|||
object: 'discount',
|
||||
checkout_session: null,
|
||||
coupon: {
|
||||
id: 'Z4OV52SU',
|
||||
id: couponId,
|
||||
object: 'coupon',
|
||||
amount_off: 60,
|
||||
created: 1649774041,
|
||||
|
@ -1372,7 +1429,8 @@ describe('Members API', function () {
|
|||
discount,
|
||||
unit_amount: 1200,
|
||||
interval: 'year',
|
||||
assert_mrr: 95
|
||||
assert_mrr: 95,
|
||||
offer_id: offer.id
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1382,7 +1440,7 @@ describe('Members API', function () {
|
|||
object: 'discount',
|
||||
checkout_session: null,
|
||||
coupon: {
|
||||
id: 'Z4OV52SU',
|
||||
id: couponId,
|
||||
object: 'coupon',
|
||||
amount_off: null,
|
||||
created: 1649774041,
|
||||
|
@ -1409,7 +1467,8 @@ describe('Members API', function () {
|
|||
discount,
|
||||
unit_amount: 500,
|
||||
interval: 'month',
|
||||
assert_mrr: 500
|
||||
assert_mrr: 500,
|
||||
offer_id: offer.id
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1425,7 +1484,7 @@ describe('Members API', function () {
|
|||
object: 'discount',
|
||||
checkout_session: null,
|
||||
coupon: {
|
||||
id: 'Z4OV52SU',
|
||||
id: couponId,
|
||||
object: 'coupon',
|
||||
amount_off: null,
|
||||
created: 1649774041,
|
||||
|
@ -1529,7 +1588,8 @@ describe('Members API', function () {
|
|||
plan_interval: interval,
|
||||
plan_currency: 'usd',
|
||||
current_period_end: new Date(Math.floor(beforeNow / 1000) * 1000 + (60 * 60 * 24 * 31 * 1000)),
|
||||
mrr: mrr_without
|
||||
mrr: mrr_without,
|
||||
offer_id: null
|
||||
});
|
||||
|
||||
await assertMemberEvents({
|
||||
|
@ -1578,7 +1638,8 @@ describe('Members API', function () {
|
|||
plan_amount: unit_amount,
|
||||
plan_interval: interval,
|
||||
plan_currency: 'usd',
|
||||
mrr: mrr_with
|
||||
mrr: mrr_with,
|
||||
offer_id: offer.id
|
||||
});
|
||||
|
||||
await assertMemberEvents({
|
||||
|
@ -1596,6 +1657,135 @@ describe('Members API', function () {
|
|||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('Silently ignores an invalid offer id in metadata', async function () {
|
||||
const interval = 'month';
|
||||
const unit_amount = 500;
|
||||
const mrr_with = 400;
|
||||
|
||||
const discount = {
|
||||
id: 'di_1Knkn7HUEDadPGIBPOQgmzIX',
|
||||
object: 'discount',
|
||||
checkout_session: null,
|
||||
coupon: {
|
||||
id: 'unknownCoupon', // this one is unknown in Ghost
|
||||
object: 'coupon',
|
||||
amount_off: null,
|
||||
created: 1649774041,
|
||||
currency: 'eur',
|
||||
duration: 'forever',
|
||||
duration_in_months: null,
|
||||
livemode: false,
|
||||
max_redemptions: null,
|
||||
metadata: {},
|
||||
name: '20% off',
|
||||
percent_off: 20,
|
||||
redeem_by: null,
|
||||
times_redeemed: 0,
|
||||
valid: true
|
||||
},
|
||||
end: null,
|
||||
invoice: null,
|
||||
invoice_item: null,
|
||||
promotion_code: null,
|
||||
start: beforeNow / 1000,
|
||||
subscription: null
|
||||
};
|
||||
|
||||
const customer_id = createStripeID('cust');
|
||||
const subscription_id = createStripeID('sub');
|
||||
|
||||
discount.customer = customer_id;
|
||||
|
||||
set(subscription, {
|
||||
id: subscription_id,
|
||||
customer: customer_id,
|
||||
status: 'active',
|
||||
discount,
|
||||
items: {
|
||||
type: 'list',
|
||||
data: [{
|
||||
id: 'item_123',
|
||||
price: {
|
||||
id: 'price_123',
|
||||
product: 'product_123',
|
||||
active: true,
|
||||
nickname: interval,
|
||||
currency: 'usd',
|
||||
recurring: {
|
||||
interval
|
||||
},
|
||||
unit_amount,
|
||||
type: 'recurring'
|
||||
}
|
||||
}]
|
||||
},
|
||||
start_date: beforeNow / 1000,
|
||||
current_period_end: Math.floor(beforeNow / 1000) + (60 * 60 * 24 * 31),
|
||||
cancel_at_period_end: false
|
||||
});
|
||||
|
||||
set(customer, {
|
||||
id: customer_id,
|
||||
name: 'Test Member',
|
||||
email: `${customer_id}@email.com`,
|
||||
subscriptions: {
|
||||
type: 'list',
|
||||
data: [subscription]
|
||||
}
|
||||
});
|
||||
|
||||
let webhookPayload = JSON.stringify({
|
||||
type: 'checkout.session.completed',
|
||||
data: {
|
||||
object: {
|
||||
mode: 'subscription',
|
||||
customer: customer.id,
|
||||
subscription: subscription.id,
|
||||
metadata: {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let webhookSignature = stripe.webhooks.generateTestHeaderString({
|
||||
payload: webhookPayload,
|
||||
secret: process.env.WEBHOOK_SECRET
|
||||
});
|
||||
|
||||
await membersAgent.post('/webhooks/stripe/')
|
||||
.body(webhookPayload)
|
||||
.header('stripe-signature', webhookSignature);
|
||||
|
||||
const {body} = await adminAgent.get(`/members/?search=${customer_id}@email.com`);
|
||||
assert.equal(body.members.length, 1, 'The member was not created');
|
||||
const member = body.members[0];
|
||||
|
||||
assert.equal(member.status, 'paid', 'The member should be "paid"');
|
||||
assert.equal(member.subscriptions.length, 1, 'The member should have a single subscription');
|
||||
|
||||
// Check whether MRR and status has been set
|
||||
await assertSubscription(member.subscriptions[0].id, {
|
||||
subscription_id: subscription.id,
|
||||
status: 'active',
|
||||
cancel_at_period_end: false,
|
||||
plan_amount: unit_amount,
|
||||
plan_interval: interval,
|
||||
plan_currency: 'usd',
|
||||
current_period_end: new Date(Math.floor(beforeNow / 1000) * 1000 + (60 * 60 * 24 * 31 * 1000)),
|
||||
mrr: mrr_with,
|
||||
offer_id: null
|
||||
});
|
||||
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberPaidSubscriptionEvent',
|
||||
memberId: member.id,
|
||||
asserts: [
|
||||
{
|
||||
mrr_delta: mrr_with
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Without the dashboardV5 flag', function () {
|
||||
|
@ -1605,7 +1795,7 @@ describe('Members API', function () {
|
|||
object: 'discount',
|
||||
checkout_session: null,
|
||||
coupon: {
|
||||
id: 'Z4OV52SU',
|
||||
id: couponId,
|
||||
object: 'coupon',
|
||||
amount_off: null,
|
||||
created: 1649774041,
|
||||
|
@ -1632,7 +1822,8 @@ describe('Members API', function () {
|
|||
discount,
|
||||
unit_amount: 500,
|
||||
interval: 'month',
|
||||
assert_mrr: 500
|
||||
assert_mrr: 500,
|
||||
offer_id: offer.id
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1642,7 +1833,7 @@ describe('Members API', function () {
|
|||
object: 'discount',
|
||||
checkout_session: null,
|
||||
coupon: {
|
||||
id: 'Z4OV52SU',
|
||||
id: couponId,
|
||||
object: 'coupon',
|
||||
amount_off: 1,
|
||||
created: 1649774041,
|
||||
|
@ -1669,7 +1860,8 @@ describe('Members API', function () {
|
|||
discount,
|
||||
unit_amount: 500,
|
||||
interval: 'month',
|
||||
assert_mrr: 500
|
||||
assert_mrr: 500,
|
||||
offer_id: offer.id
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
28
yarn.lock
28
yarn.lock
|
@ -2009,10 +2009,10 @@
|
|||
"@tryghost/domain-events" "^0.1.9"
|
||||
"@tryghost/member-events" "^0.4.1"
|
||||
|
||||
"@tryghost/members-api@5.9.2":
|
||||
version "5.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-5.9.2.tgz#4d4d633798578f45266cfc0e51e9a8203719778d"
|
||||
integrity sha512-tLH0c8n2nCGJ2OazFPGg4iJuZ+pQMQ3n+iCuiQf28eQzDRZ3ly+5PHSRDLT8UDG578Ygkiz3uQz5P5WPCi/m6Q==
|
||||
"@tryghost/members-api@5.11.1":
|
||||
version "5.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-5.11.1.tgz#5b32fe806d5ce6f5d6743525fda74ea6667ffff4"
|
||||
integrity sha512-yeaC6WpqNA+4qXqpLN5+Yo5ms3R1diMmQe6JnsMcjI6Nuby8ltPJi4uazFd09uFI2b+Bj/yucEzStbkT5lUS4g==
|
||||
dependencies:
|
||||
"@nexes/nql" "^0.6.0"
|
||||
"@tryghost/debug" "^0.1.2"
|
||||
|
@ -2023,7 +2023,7 @@
|
|||
"@tryghost/member-analytics-service" "^0.1.11"
|
||||
"@tryghost/member-events" "^0.4.1"
|
||||
"@tryghost/members-analytics-ingress" "^0.1.12"
|
||||
"@tryghost/members-payments" "^0.1.11"
|
||||
"@tryghost/members-payments" "^0.3.1"
|
||||
"@tryghost/members-stripe-service" "^0.10.0"
|
||||
"@tryghost/tpl" "^0.1.2"
|
||||
"@types/jsonwebtoken" "^8.5.1"
|
||||
|
@ -2067,21 +2067,21 @@
|
|||
"@tryghost/tpl" "^0.1.3"
|
||||
moment-timezone "^0.5.23"
|
||||
|
||||
"@tryghost/members-offers@0.10.9", "@tryghost/members-offers@^0.10.9":
|
||||
version "0.10.9"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-offers/-/members-offers-0.10.9.tgz#68b3936724ba6d4e832090ae26562e27cfb5bb94"
|
||||
integrity sha512-JB441c45JRBkjGmKUWvosZ+ALGf++H+WIKiAlKBkt+MYOJQo13FOa+V2byp6ucCwFMDSAN2s/GeomePSvqwALg==
|
||||
"@tryghost/members-offers@0.11.1", "@tryghost/members-offers@^0.11.1":
|
||||
version "0.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-offers/-/members-offers-0.11.1.tgz#f9f4704b187aabaa54b641cc954415c50cf0165d"
|
||||
integrity sha512-cA4/AVvL6SH4ATvud75u/S1UiBtVZa0/0V4gX5V/Ncq11UMXb/kZV20cRSIF/5iuaBtrfrCfaUvpXQfXZylRXQ==
|
||||
dependencies:
|
||||
"@nexes/mongo-utils" "^0.3.1"
|
||||
"@tryghost/string" "^0.1.20"
|
||||
|
||||
"@tryghost/members-payments@^0.1.11":
|
||||
version "0.1.11"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-payments/-/members-payments-0.1.11.tgz#bc95d0f7fcd728d9f9b30ca6f41cefe6887e6341"
|
||||
integrity sha512-YVyNs6vFCUmD6/QlVrHx3tkTiP12r9mbdh8ZkTDYljUE8b/2nWeH1FoPSdR6i92yklbPNI7YWI99CQcJqwSWww==
|
||||
"@tryghost/members-payments@^0.3.1":
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-payments/-/members-payments-0.3.1.tgz#cd52c87fcb02fbe86dbda9c843a5946f9dad40e0"
|
||||
integrity sha512-12aTBMHb9JcxC6rsGSeIQnwsEDNVpCtcp+ail9W7ghcBjbLTqf6menWq++DavsohFfzStwI3D5reKstA2M68+A==
|
||||
dependencies:
|
||||
"@tryghost/domain-events" "^0.1.9"
|
||||
"@tryghost/members-offers" "^0.10.9"
|
||||
"@tryghost/members-offers" "^0.11.1"
|
||||
|
||||
"@tryghost/members-ssr@1.0.23":
|
||||
version "1.0.23"
|
||||
|
|
Loading…
Reference in a new issue