Ghost/ghost/members-api/test/unit/lib/repositories/member.test.js

463 lines
15 KiB
JavaScript
Raw Normal View History

const assert = require('assert/strict');
const sinon = require('sinon');
const DomainEvents = require('@tryghost/domain-events');
const MemberRepository = require('../../../../lib/repositories/MemberRepository');
🐛 Fixed offer redemptions for free members redeeming an offer (#20571) ref https://linear.app/tryghost/issue/ENG-1251/support-escalation-re-offers-not-tracking - Offer Redemptions were not being persisted in the database for existing free members who upgrade to a paid plan with an offer, which resulted in inaccurate offer redemption counts. This made it difficult to assess the performance of an offer. - Previously, Ghost recorded an offer redemption in the DB in response to the `SubscriptionCreatedEvent`, under the assumption that the offer details would be included in this event. This assumption was valid for brand new members starting a subscription with an offer, but not for existing free members upgrading to a paid plan with an offer. - For existing free members, the subscription is first stored in Ghost in response to the `customer.subscription.created` Stripe webhook. At this point, the offer/discount is not attached to the subscription, so the `SubscriptionCreatedEvent` triggers without the offer information, and the offer redemption is not recorded. After the `checkout.session.completed` webhook is received (which _does_ include the offer details), the subscription is updated in Ghost, but the Offer Redemption is not stored. - For brand new members, the `customer.subscription.created` webhook no-ops, because the member and Stripe Customer don't exist yet. Therefore, the subscription is first created in Ghost in response to the `checkout.session.completed` webhook, which _does_ include the offer information, so the offer information is included in the `SubscriptionCreatedEvent` and the offer redemption is recorded as expected. - This change adds a new `OfferRedemptionEvent`, which triggers either: (1) when a new subscription is created with an offer (as in the case of a brand new member), or (2) when an existing subscription is first updated to include an offer (as in the case of an existing free member upgrading with an offer). The Offer Redemption is then persisted in the DB in response to the `OfferRedemptionEvent` rather than the `SubscriptionCreatedEvent`.
2024-07-10 02:05:26 +03:00
const {SubscriptionCreatedEvent, OfferRedemptionEvent} = require('@tryghost/member-events');
const mockOfferRedemption = {
🐛 Fixed offer redemptions for free members redeeming an offer (#20571) ref https://linear.app/tryghost/issue/ENG-1251/support-escalation-re-offers-not-tracking - Offer Redemptions were not being persisted in the database for existing free members who upgrade to a paid plan with an offer, which resulted in inaccurate offer redemption counts. This made it difficult to assess the performance of an offer. - Previously, Ghost recorded an offer redemption in the DB in response to the `SubscriptionCreatedEvent`, under the assumption that the offer details would be included in this event. This assumption was valid for brand new members starting a subscription with an offer, but not for existing free members upgrading to a paid plan with an offer. - For existing free members, the subscription is first stored in Ghost in response to the `customer.subscription.created` Stripe webhook. At this point, the offer/discount is not attached to the subscription, so the `SubscriptionCreatedEvent` triggers without the offer information, and the offer redemption is not recorded. After the `checkout.session.completed` webhook is received (which _does_ include the offer details), the subscription is updated in Ghost, but the Offer Redemption is not stored. - For brand new members, the `customer.subscription.created` webhook no-ops, because the member and Stripe Customer don't exist yet. Therefore, the subscription is first created in Ghost in response to the `checkout.session.completed` webhook, which _does_ include the offer information, so the offer information is included in the `SubscriptionCreatedEvent` and the offer redemption is recorded as expected. - This change adds a new `OfferRedemptionEvent`, which triggers either: (1) when a new subscription is created with an offer (as in the case of a brand new member), or (2) when an existing subscription is first updated to include an offer (as in the case of an existing free member upgrading with an offer). The Offer Redemption is then persisted in the DB in response to the `OfferRedemptionEvent` rather than the `SubscriptionCreatedEvent`.
2024-07-10 02:05:26 +03:00
add: sinon.stub(),
findOne: sinon.stub()
};
describe('MemberRepository', function () {
afterEach(function () {
sinon.restore();
});
describe('#isComplimentarySubscription', function () {
it('Does not error when subscription.plan is null', function () {
const repo = new MemberRepository({OfferRedemption: mockOfferRedemption});
repo.isComplimentarySubscription({});
});
});
describe('#resolveContextSource', function (){
it('Maps context to source', function (){
const repo = new MemberRepository({OfferRedemption: mockOfferRedemption});
let source = repo._resolveContextSource({
import: true
});
assert.equal(source, 'import');
source = repo._resolveContextSource({
importer: true
});
assert.equal(source, 'import');
source = repo._resolveContextSource({
user: true
});
assert.equal(source, 'admin');
source = repo._resolveContextSource({
user: true,
api_key: true
});
assert.equal(source, 'api');
source = repo._resolveContextSource({
api_key: true
});
assert.equal(source, 'api');
source = repo._resolveContextSource({
});
assert.equal(source, 'member');
source = repo._resolveContextSource({
generic_context: true
});
assert.equal(source, 'member');
});
});
describe('setComplimentarySubscription', function () {
let Member;
let productRepository;
beforeEach(function () {
Member = {
findOne: sinon.stub().resolves({
id: 'member_id_123',
related: () => {
return {
fetch: () => {
return {
models: []
};
}
};
}
})
};
});
it('throws an error when there is no default product', async function () {
productRepository = {
getDefaultProduct: sinon.stub().resolves(null)
};
const repo = new MemberRepository({
Member,
stripeAPIService: {
configured: true
},
productRepository,
OfferRedemption: mockOfferRedemption
});
try {
await repo.setComplimentarySubscription({
id: 'member_id_123'
}, {
transacting: true
});
assert.fail('setComplimentarySubscription should have thrown');
} catch (err) {
assert.equal(err.message, 'Could not find Product "default"');
}
});
it('uses the right options for fetching default product', async function () {
productRepository = {
getDefaultProduct: sinon.stub().resolves({
toJSON: () => {
return null;
}
})
};
const repo = new MemberRepository({
Member,
stripeAPIService: {
configured: true
},
productRepository,
OfferRedemption: mockOfferRedemption
});
try {
await repo.setComplimentarySubscription({
id: 'member_id_123'
}, {
transacting: true,
withRelated: ['labels']
});
assert.fail('setComplimentarySubscription should have thrown');
} catch (err) {
productRepository.getDefaultProduct.calledWith({withRelated: ['stripePrices'], transacting: true}).should.be.true();
assert.equal(err.message, 'Could not find Product "default"');
}
});
});
describe('newsletter subscriptions', function () {
let Member;
let MemberProductEvent;
let productRepository;
let stripeAPIService;
let existingNewsletters;
let MemberSubscribeEvent;
beforeEach(async function () {
sinon.spy();
existingNewsletters = [
{
id: 'newsletter_id_123',
attributes: {
status: 'active'
},
get: sinon.stub().withArgs('status').returns('active')
},
{
id: 'newsletter_id_1234_archive',
attributes: {
status: 'archived'
},
get: sinon.stub().withArgs('status').returns('archived')
}
];
Member = {
findOne: sinon.stub().resolves({
get: sinon.stub().returns('member_id_123'),
related: sinon.stub().withArgs('newsletters').returns({
models: existingNewsletters
}),
toJSON: sinon.stub().returns({})
}),
edit: sinon.stub().resolves({
attributes: {},
_previousAttributes: {}
})
};
stripeAPIService = {
configured: false
};
MemberSubscribeEvent = {
add: sinon.stub().resolves()
};
});
it('Does not create false archived newsletter events', async function () {
const repo = new MemberRepository({
Member,
MemberProductEvent,
productRepository,
stripeAPIService,
MemberSubscribeEventModel: MemberSubscribeEvent,
OfferRedemption: mockOfferRedemption
});
await repo.update({
email: 'test@email.com',
newsletters: [{
id: 'newsletter_id_123'
},
{
id: 'newsletter_id_456'
},
{
id: 'newsletter_id_new'
},
{
id: 'newsletter_id_1234_archive'
}]
},{
transacting: {
executionPromise: Promise.resolve()
},
context: {}
});
MemberSubscribeEvent.add.calledTwice.should.be.true();
});
});
describe('linkSubscription', function (){
let Member;
let MemberPaidSubscriptionEvent;
let StripeCustomerSubscription;
let MemberProductEvent;
let stripeAPIService;
let productRepository;
let offerRepository;
let labsService;
let subscriptionData;
🐛 Fixed offer redemptions for free members redeeming an offer (#20571) ref https://linear.app/tryghost/issue/ENG-1251/support-escalation-re-offers-not-tracking - Offer Redemptions were not being persisted in the database for existing free members who upgrade to a paid plan with an offer, which resulted in inaccurate offer redemption counts. This made it difficult to assess the performance of an offer. - Previously, Ghost recorded an offer redemption in the DB in response to the `SubscriptionCreatedEvent`, under the assumption that the offer details would be included in this event. This assumption was valid for brand new members starting a subscription with an offer, but not for existing free members upgrading to a paid plan with an offer. - For existing free members, the subscription is first stored in Ghost in response to the `customer.subscription.created` Stripe webhook. At this point, the offer/discount is not attached to the subscription, so the `SubscriptionCreatedEvent` triggers without the offer information, and the offer redemption is not recorded. After the `checkout.session.completed` webhook is received (which _does_ include the offer details), the subscription is updated in Ghost, but the Offer Redemption is not stored. - For brand new members, the `customer.subscription.created` webhook no-ops, because the member and Stripe Customer don't exist yet. Therefore, the subscription is first created in Ghost in response to the `checkout.session.completed` webhook, which _does_ include the offer information, so the offer information is included in the `SubscriptionCreatedEvent` and the offer redemption is recorded as expected. - This change adds a new `OfferRedemptionEvent`, which triggers either: (1) when a new subscription is created with an offer (as in the case of a brand new member), or (2) when an existing subscription is first updated to include an offer (as in the case of an existing free member upgrading with an offer). The Offer Redemption is then persisted in the DB in response to the `OfferRedemptionEvent` rather than the `SubscriptionCreatedEvent`.
2024-07-10 02:05:26 +03:00
let subscriptionCreatedNotifySpy;
let offerRedemptionNotifySpy;
afterEach(function () {
sinon.restore();
});
beforeEach(async function () {
🐛 Fixed offer redemptions for free members redeeming an offer (#20571) ref https://linear.app/tryghost/issue/ENG-1251/support-escalation-re-offers-not-tracking - Offer Redemptions were not being persisted in the database for existing free members who upgrade to a paid plan with an offer, which resulted in inaccurate offer redemption counts. This made it difficult to assess the performance of an offer. - Previously, Ghost recorded an offer redemption in the DB in response to the `SubscriptionCreatedEvent`, under the assumption that the offer details would be included in this event. This assumption was valid for brand new members starting a subscription with an offer, but not for existing free members upgrading to a paid plan with an offer. - For existing free members, the subscription is first stored in Ghost in response to the `customer.subscription.created` Stripe webhook. At this point, the offer/discount is not attached to the subscription, so the `SubscriptionCreatedEvent` triggers without the offer information, and the offer redemption is not recorded. After the `checkout.session.completed` webhook is received (which _does_ include the offer details), the subscription is updated in Ghost, but the Offer Redemption is not stored. - For brand new members, the `customer.subscription.created` webhook no-ops, because the member and Stripe Customer don't exist yet. Therefore, the subscription is first created in Ghost in response to the `checkout.session.completed` webhook, which _does_ include the offer information, so the offer information is included in the `SubscriptionCreatedEvent` and the offer redemption is recorded as expected. - This change adds a new `OfferRedemptionEvent`, which triggers either: (1) when a new subscription is created with an offer (as in the case of a brand new member), or (2) when an existing subscription is first updated to include an offer (as in the case of an existing free member upgrading with an offer). The Offer Redemption is then persisted in the DB in response to the `OfferRedemptionEvent` rather than the `SubscriptionCreatedEvent`.
2024-07-10 02:05:26 +03:00
subscriptionCreatedNotifySpy = sinon.spy();
offerRedemptionNotifySpy = sinon.spy();
subscriptionData = {
id: 'sub_123',
customer: 'cus_123',
status: 'active',
items: {
type: 'list',
data: [{
id: 'item_123',
price: {
id: 'price_123',
product: 'product_123',
active: true,
nickname: 'Monthly',
currency: 'usd',
recurring: {
interval: 'month'
},
unit_amount: 500,
type: 'recurring'
}
}]
},
start_date: Date.now() / 1000,
current_period_end: Date.now() / 1000 + (60 * 60 * 24 * 31),
cancel_at_period_end: false
};
Member = {
findOne: sinon.stub().resolves({
related: (relation) => {
return {
query: sinon.stub().returns({
fetchOne: sinon.stub().resolves({})
}),
toJSON: sinon.stub().returns(relation === 'products' ? [] : {}),
fetch: sinon.stub().resolves({
🐛 Fixed offer redemptions for free members redeeming an offer (#20571) ref https://linear.app/tryghost/issue/ENG-1251/support-escalation-re-offers-not-tracking - Offer Redemptions were not being persisted in the database for existing free members who upgrade to a paid plan with an offer, which resulted in inaccurate offer redemption counts. This made it difficult to assess the performance of an offer. - Previously, Ghost recorded an offer redemption in the DB in response to the `SubscriptionCreatedEvent`, under the assumption that the offer details would be included in this event. This assumption was valid for brand new members starting a subscription with an offer, but not for existing free members upgrading to a paid plan with an offer. - For existing free members, the subscription is first stored in Ghost in response to the `customer.subscription.created` Stripe webhook. At this point, the offer/discount is not attached to the subscription, so the `SubscriptionCreatedEvent` triggers without the offer information, and the offer redemption is not recorded. After the `checkout.session.completed` webhook is received (which _does_ include the offer details), the subscription is updated in Ghost, but the Offer Redemption is not stored. - For brand new members, the `customer.subscription.created` webhook no-ops, because the member and Stripe Customer don't exist yet. Therefore, the subscription is first created in Ghost in response to the `checkout.session.completed` webhook, which _does_ include the offer information, so the offer information is included in the `SubscriptionCreatedEvent` and the offer redemption is recorded as expected. - This change adds a new `OfferRedemptionEvent`, which triggers either: (1) when a new subscription is created with an offer (as in the case of a brand new member), or (2) when an existing subscription is first updated to include an offer (as in the case of an existing free member upgrading with an offer). The Offer Redemption is then persisted in the DB in response to the `OfferRedemptionEvent` rather than the `SubscriptionCreatedEvent`.
2024-07-10 02:05:26 +03:00
toJSON: sinon.stub().returns(relation === 'products' ? [] : {}),
models: []
})
};
},
toJSON: sinon.stub().returns({})
}),
edit: sinon.stub().resolves({
attributes: {},
_previousAttributes: {}
})
};
MemberPaidSubscriptionEvent = {
add: sinon.stub().resolves()
};
StripeCustomerSubscription = {
add: sinon.stub().resolves({
get: sinon.stub().returns()
🐛 Fixed offer redemptions for free members redeeming an offer (#20571) ref https://linear.app/tryghost/issue/ENG-1251/support-escalation-re-offers-not-tracking - Offer Redemptions were not being persisted in the database for existing free members who upgrade to a paid plan with an offer, which resulted in inaccurate offer redemption counts. This made it difficult to assess the performance of an offer. - Previously, Ghost recorded an offer redemption in the DB in response to the `SubscriptionCreatedEvent`, under the assumption that the offer details would be included in this event. This assumption was valid for brand new members starting a subscription with an offer, but not for existing free members upgrading to a paid plan with an offer. - For existing free members, the subscription is first stored in Ghost in response to the `customer.subscription.created` Stripe webhook. At this point, the offer/discount is not attached to the subscription, so the `SubscriptionCreatedEvent` triggers without the offer information, and the offer redemption is not recorded. After the `checkout.session.completed` webhook is received (which _does_ include the offer details), the subscription is updated in Ghost, but the Offer Redemption is not stored. - For brand new members, the `customer.subscription.created` webhook no-ops, because the member and Stripe Customer don't exist yet. Therefore, the subscription is first created in Ghost in response to the `checkout.session.completed` webhook, which _does_ include the offer information, so the offer information is included in the `SubscriptionCreatedEvent` and the offer redemption is recorded as expected. - This change adds a new `OfferRedemptionEvent`, which triggers either: (1) when a new subscription is created with an offer (as in the case of a brand new member), or (2) when an existing subscription is first updated to include an offer (as in the case of an existing free member upgrading with an offer). The Offer Redemption is then persisted in the DB in response to the `OfferRedemptionEvent` rather than the `SubscriptionCreatedEvent`.
2024-07-10 02:05:26 +03:00
}),
edit: sinon.stub().resolves({
get: sinon.stub().returns()
})
};
MemberProductEvent = {
add: sinon.stub().resolves({})
};
stripeAPIService = {
configured: true,
getSubscription: sinon.stub().resolves(subscriptionData)
};
productRepository = {
get: sinon.stub().resolves({
get: sinon.stub().returns(),
toJSON: sinon.stub().returns({})
}),
update: sinon.stub().resolves({})
};
labsService = {
isSet: sinon.stub().returns(true)
};
offerRepository = {
getById: sinon.stub().resolves({
id: 'offer_123'
})
};
});
it('dispatches paid subscription event', async function (){
const repo = new MemberRepository({
stripeAPIService,
StripeCustomerSubscription,
MemberPaidSubscriptionEvent,
MemberProductEvent,
productRepository,
labsService,
Member,
OfferRedemption: mockOfferRedemption
});
sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);
🐛 Fixed offer redemptions for free members redeeming an offer (#20571) ref https://linear.app/tryghost/issue/ENG-1251/support-escalation-re-offers-not-tracking - Offer Redemptions were not being persisted in the database for existing free members who upgrade to a paid plan with an offer, which resulted in inaccurate offer redemption counts. This made it difficult to assess the performance of an offer. - Previously, Ghost recorded an offer redemption in the DB in response to the `SubscriptionCreatedEvent`, under the assumption that the offer details would be included in this event. This assumption was valid for brand new members starting a subscription with an offer, but not for existing free members upgrading to a paid plan with an offer. - For existing free members, the subscription is first stored in Ghost in response to the `customer.subscription.created` Stripe webhook. At this point, the offer/discount is not attached to the subscription, so the `SubscriptionCreatedEvent` triggers without the offer information, and the offer redemption is not recorded. After the `checkout.session.completed` webhook is received (which _does_ include the offer details), the subscription is updated in Ghost, but the Offer Redemption is not stored. - For brand new members, the `customer.subscription.created` webhook no-ops, because the member and Stripe Customer don't exist yet. Therefore, the subscription is first created in Ghost in response to the `checkout.session.completed` webhook, which _does_ include the offer information, so the offer information is included in the `SubscriptionCreatedEvent` and the offer redemption is recorded as expected. - This change adds a new `OfferRedemptionEvent`, which triggers either: (1) when a new subscription is created with an offer (as in the case of a brand new member), or (2) when an existing subscription is first updated to include an offer (as in the case of an existing free member upgrading with an offer). The Offer Redemption is then persisted in the DB in response to the `OfferRedemptionEvent` rather than the `SubscriptionCreatedEvent`.
2024-07-10 02:05:26 +03:00
DomainEvents.subscribe(SubscriptionCreatedEvent, subscriptionCreatedNotifySpy);
DomainEvents.subscribe(OfferRedemptionEvent, offerRedemptionNotifySpy);
await repo.linkSubscription({
subscription: subscriptionData
}, {
transacting: {
executionPromise: Promise.resolve()
},
context: {}
});
🐛 Fixed offer redemptions for free members redeeming an offer (#20571) ref https://linear.app/tryghost/issue/ENG-1251/support-escalation-re-offers-not-tracking - Offer Redemptions were not being persisted in the database for existing free members who upgrade to a paid plan with an offer, which resulted in inaccurate offer redemption counts. This made it difficult to assess the performance of an offer. - Previously, Ghost recorded an offer redemption in the DB in response to the `SubscriptionCreatedEvent`, under the assumption that the offer details would be included in this event. This assumption was valid for brand new members starting a subscription with an offer, but not for existing free members upgrading to a paid plan with an offer. - For existing free members, the subscription is first stored in Ghost in response to the `customer.subscription.created` Stripe webhook. At this point, the offer/discount is not attached to the subscription, so the `SubscriptionCreatedEvent` triggers without the offer information, and the offer redemption is not recorded. After the `checkout.session.completed` webhook is received (which _does_ include the offer details), the subscription is updated in Ghost, but the Offer Redemption is not stored. - For brand new members, the `customer.subscription.created` webhook no-ops, because the member and Stripe Customer don't exist yet. Therefore, the subscription is first created in Ghost in response to the `checkout.session.completed` webhook, which _does_ include the offer information, so the offer information is included in the `SubscriptionCreatedEvent` and the offer redemption is recorded as expected. - This change adds a new `OfferRedemptionEvent`, which triggers either: (1) when a new subscription is created with an offer (as in the case of a brand new member), or (2) when an existing subscription is first updated to include an offer (as in the case of an existing free member upgrading with an offer). The Offer Redemption is then persisted in the DB in response to the `OfferRedemptionEvent` rather than the `SubscriptionCreatedEvent`.
2024-07-10 02:05:26 +03:00
subscriptionCreatedNotifySpy.calledOnce.should.be.true();
offerRedemptionNotifySpy.called.should.be.false();
});
🐛 Fixed offer redemptions for free members redeeming an offer (#20571) ref https://linear.app/tryghost/issue/ENG-1251/support-escalation-re-offers-not-tracking - Offer Redemptions were not being persisted in the database for existing free members who upgrade to a paid plan with an offer, which resulted in inaccurate offer redemption counts. This made it difficult to assess the performance of an offer. - Previously, Ghost recorded an offer redemption in the DB in response to the `SubscriptionCreatedEvent`, under the assumption that the offer details would be included in this event. This assumption was valid for brand new members starting a subscription with an offer, but not for existing free members upgrading to a paid plan with an offer. - For existing free members, the subscription is first stored in Ghost in response to the `customer.subscription.created` Stripe webhook. At this point, the offer/discount is not attached to the subscription, so the `SubscriptionCreatedEvent` triggers without the offer information, and the offer redemption is not recorded. After the `checkout.session.completed` webhook is received (which _does_ include the offer details), the subscription is updated in Ghost, but the Offer Redemption is not stored. - For brand new members, the `customer.subscription.created` webhook no-ops, because the member and Stripe Customer don't exist yet. Therefore, the subscription is first created in Ghost in response to the `checkout.session.completed` webhook, which _does_ include the offer information, so the offer information is included in the `SubscriptionCreatedEvent` and the offer redemption is recorded as expected. - This change adds a new `OfferRedemptionEvent`, which triggers either: (1) when a new subscription is created with an offer (as in the case of a brand new member), or (2) when an existing subscription is first updated to include an offer (as in the case of an existing free member upgrading with an offer). The Offer Redemption is then persisted in the DB in response to the `OfferRedemptionEvent` rather than the `SubscriptionCreatedEvent`.
2024-07-10 02:05:26 +03:00
it('dispatches the offer redemption event for a new member starting a subscription', async function (){
// When a new member starts a paid subscription, the subscription is created with the offer ID
const repo = new MemberRepository({
stripeAPIService,
StripeCustomerSubscription,
MemberPaidSubscriptionEvent,
MemberProductEvent,
productRepository,
offerRepository,
labsService,
Member,
OfferRedemption: mockOfferRedemption
});
🐛 Fixed offer redemptions for free members redeeming an offer (#20571) ref https://linear.app/tryghost/issue/ENG-1251/support-escalation-re-offers-not-tracking - Offer Redemptions were not being persisted in the database for existing free members who upgrade to a paid plan with an offer, which resulted in inaccurate offer redemption counts. This made it difficult to assess the performance of an offer. - Previously, Ghost recorded an offer redemption in the DB in response to the `SubscriptionCreatedEvent`, under the assumption that the offer details would be included in this event. This assumption was valid for brand new members starting a subscription with an offer, but not for existing free members upgrading to a paid plan with an offer. - For existing free members, the subscription is first stored in Ghost in response to the `customer.subscription.created` Stripe webhook. At this point, the offer/discount is not attached to the subscription, so the `SubscriptionCreatedEvent` triggers without the offer information, and the offer redemption is not recorded. After the `checkout.session.completed` webhook is received (which _does_ include the offer details), the subscription is updated in Ghost, but the Offer Redemption is not stored. - For brand new members, the `customer.subscription.created` webhook no-ops, because the member and Stripe Customer don't exist yet. Therefore, the subscription is first created in Ghost in response to the `checkout.session.completed` webhook, which _does_ include the offer information, so the offer information is included in the `SubscriptionCreatedEvent` and the offer redemption is recorded as expected. - This change adds a new `OfferRedemptionEvent`, which triggers either: (1) when a new subscription is created with an offer (as in the case of a brand new member), or (2) when an existing subscription is first updated to include an offer (as in the case of an existing free member upgrading with an offer). The Offer Redemption is then persisted in the DB in response to the `OfferRedemptionEvent` rather than the `SubscriptionCreatedEvent`.
2024-07-10 02:05:26 +03:00
// No existing subscription
sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);
🐛 Fixed offer redemptions for free members redeeming an offer (#20571) ref https://linear.app/tryghost/issue/ENG-1251/support-escalation-re-offers-not-tracking - Offer Redemptions were not being persisted in the database for existing free members who upgrade to a paid plan with an offer, which resulted in inaccurate offer redemption counts. This made it difficult to assess the performance of an offer. - Previously, Ghost recorded an offer redemption in the DB in response to the `SubscriptionCreatedEvent`, under the assumption that the offer details would be included in this event. This assumption was valid for brand new members starting a subscription with an offer, but not for existing free members upgrading to a paid plan with an offer. - For existing free members, the subscription is first stored in Ghost in response to the `customer.subscription.created` Stripe webhook. At this point, the offer/discount is not attached to the subscription, so the `SubscriptionCreatedEvent` triggers without the offer information, and the offer redemption is not recorded. After the `checkout.session.completed` webhook is received (which _does_ include the offer details), the subscription is updated in Ghost, but the Offer Redemption is not stored. - For brand new members, the `customer.subscription.created` webhook no-ops, because the member and Stripe Customer don't exist yet. Therefore, the subscription is first created in Ghost in response to the `checkout.session.completed` webhook, which _does_ include the offer information, so the offer information is included in the `SubscriptionCreatedEvent` and the offer redemption is recorded as expected. - This change adds a new `OfferRedemptionEvent`, which triggers either: (1) when a new subscription is created with an offer (as in the case of a brand new member), or (2) when an existing subscription is first updated to include an offer (as in the case of an existing free member upgrading with an offer). The Offer Redemption is then persisted in the DB in response to the `OfferRedemptionEvent` rather than the `SubscriptionCreatedEvent`.
2024-07-10 02:05:26 +03:00
DomainEvents.subscribe(SubscriptionCreatedEvent, subscriptionCreatedNotifySpy);
DomainEvents.subscribe(OfferRedemptionEvent, offerRedemptionNotifySpy);
await repo.linkSubscription({
id: 'member_id_123',
subscription: subscriptionData,
offerId: 'offer_123'
}, {
transacting: {
executionPromise: Promise.resolve()
},
context: {}
});
🐛 Fixed offer redemptions for free members redeeming an offer (#20571) ref https://linear.app/tryghost/issue/ENG-1251/support-escalation-re-offers-not-tracking - Offer Redemptions were not being persisted in the database for existing free members who upgrade to a paid plan with an offer, which resulted in inaccurate offer redemption counts. This made it difficult to assess the performance of an offer. - Previously, Ghost recorded an offer redemption in the DB in response to the `SubscriptionCreatedEvent`, under the assumption that the offer details would be included in this event. This assumption was valid for brand new members starting a subscription with an offer, but not for existing free members upgrading to a paid plan with an offer. - For existing free members, the subscription is first stored in Ghost in response to the `customer.subscription.created` Stripe webhook. At this point, the offer/discount is not attached to the subscription, so the `SubscriptionCreatedEvent` triggers without the offer information, and the offer redemption is not recorded. After the `checkout.session.completed` webhook is received (which _does_ include the offer details), the subscription is updated in Ghost, but the Offer Redemption is not stored. - For brand new members, the `customer.subscription.created` webhook no-ops, because the member and Stripe Customer don't exist yet. Therefore, the subscription is first created in Ghost in response to the `checkout.session.completed` webhook, which _does_ include the offer information, so the offer information is included in the `SubscriptionCreatedEvent` and the offer redemption is recorded as expected. - This change adds a new `OfferRedemptionEvent`, which triggers either: (1) when a new subscription is created with an offer (as in the case of a brand new member), or (2) when an existing subscription is first updated to include an offer (as in the case of an existing free member upgrading with an offer). The Offer Redemption is then persisted in the DB in response to the `OfferRedemptionEvent` rather than the `SubscriptionCreatedEvent`.
2024-07-10 02:05:26 +03:00
subscriptionCreatedNotifySpy.calledOnce.should.be.true();
subscriptionCreatedNotifySpy.calledWith(sinon.match((event) => {
if (event.data.offerId === 'offer_123') {
return true;
}
return false;
})).should.be.true();
offerRedemptionNotifySpy.called.should.be.true();
offerRedemptionNotifySpy.calledWith(sinon.match((event) => {
if (event.data.offerId === 'offer_123') {
return true;
}
return false;
})).should.be.true();
});
it('dispatches the offer redemption event for an existing member upgrading to a paid subscription', async function (){
// When an existing free member upgrades to a paid subscription, the subscription is first created _without_ the offer id
// Then it is updated with the offer id after the checkout.completed webhook is received
const repo = new MemberRepository({
stripeAPIService,
StripeCustomerSubscription,
MemberPaidSubscriptionEvent,
MemberProductEvent,
productRepository,
offerRepository,
labsService,
Member,
OfferRedemption: mockOfferRedemption
});
sinon.stub(repo, 'getSubscriptionByStripeID').resolves({
get: sinon.stub().withArgs('offer_id').returns(null)
});
DomainEvents.subscribe(SubscriptionCreatedEvent, subscriptionCreatedNotifySpy);
DomainEvents.subscribe(OfferRedemptionEvent, offerRedemptionNotifySpy);
await repo.linkSubscription({
id: 'member_id_123',
subscription: subscriptionData,
offerId: 'offer_123'
}, {
transacting: {
executionPromise: Promise.resolve()
},
context: {}
});
subscriptionCreatedNotifySpy.calledOnce.should.be.false();
offerRedemptionNotifySpy.called.should.be.true();
offerRedemptionNotifySpy.calledWith(sinon.match((event) => {
if (event.data.offerId === 'offer_123') {
return true;
}
return false;
})).should.be.true();
});
});
});