Fixed subscription status not showing correctly in emails
refs https://github.com/TryGhost/Team/issues/2674 - The segment detection doesn't work outside the main post content. So the data-gh-segment attribute didn't work. It is now replaced with just a simple email replacement that is empty for a free member. - Fixed that a trialing member was shown as 'paid'. This is now replaced with 'trialing'. This commit also includes E2E tests for a couple of member statusses.
This commit is contained in:
parent
eb1d63eac0
commit
0107d2bb77
File diff suppressed because it is too large
Load Diff
@ -13,7 +13,7 @@ const {settingsCache} = require('../../../../core/server/services/settings-helpe
|
||||
const DomainEvents = require('@tryghost/domain-events');
|
||||
const emailService = require('../../../../core/server/services/email-service');
|
||||
const should = require('should');
|
||||
const {mockSetting} = require('../../../utils/e2e-framework-mock-manager');
|
||||
const {mockSetting, stripeMocker} = require('../../../utils/e2e-framework-mock-manager');
|
||||
|
||||
const mobileDocExample = '{"version":"0.3.1","atoms":[],"cards":[],"markups":[],"sections":[[1,"p",[[0,[],0,"Hello world"]]]],"ghostVersion":"4.0"}';
|
||||
const mobileDocWithPaywall = '{"version":"0.3.1","markups":[],"atoms":[],"cards":[["paywall",{}]],"sections":[[1,"p",[[0,[],0,"Free content"]]],[10,0],[1,"p",[[0,[],0,"Members content"]]]]}';
|
||||
@ -263,6 +263,7 @@ describe('Batch sending tests', function () {
|
||||
// Allows for setting stubbedSend during tests
|
||||
return stubbedSend.call(this, ...arguments);
|
||||
});
|
||||
mockManager.mockStripe();
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
@ -1039,19 +1040,189 @@ describe('Batch sending tests', function () {
|
||||
await models.Newsletter.edit({show_comment_cta: true}, {id: defaultNewsletter.id});
|
||||
});
|
||||
|
||||
it('Shows subscription details box', async function () {
|
||||
it('Shows subscription details box for free members', async function () {
|
||||
// Create a new member without a first_name
|
||||
await models.Member.add({
|
||||
email: 'subscription-box-1@example.com',
|
||||
labels: [{name: 'subscription-box-tests'}],
|
||||
newsletters: [{
|
||||
id: fixtureManager.get('newsletters', 0).id
|
||||
}]
|
||||
});
|
||||
|
||||
mockSetting('email_track_clicks', false); // Disable link replacement for this test
|
||||
|
||||
const defaultNewsletter = await getDefaultNewsletter();
|
||||
await models.Newsletter.edit({show_subscription_details: true}, {id: defaultNewsletter.id});
|
||||
|
||||
const {html} = await sendEmail({
|
||||
const {html, plaintext} = await sendEmail({
|
||||
title: 'This is a test post title',
|
||||
mobiledoc: mobileDocExample
|
||||
});
|
||||
}, 'label:subscription-box-tests');
|
||||
|
||||
// Currently the link is not present in plaintext version (because no text)
|
||||
assert.equal(html.match(/#\/portal\/account/g).length, 1, 'Subscription details box should contain a link to the account page');
|
||||
|
||||
// Check text matches
|
||||
assert.match(plaintext, /You are receiving this because you are a free subscriber to Ghost\./);
|
||||
|
||||
await lastEmailMatchSnapshot();
|
||||
|
||||
// undo
|
||||
await models.Newsletter.edit({show_subscription_details: false}, {id: defaultNewsletter.id});
|
||||
});
|
||||
|
||||
it('Shows subscription details box for comped members', async function () {
|
||||
// Create a new member without a first_name
|
||||
await models.Member.add({
|
||||
email: 'subscription-box-comped@example.com',
|
||||
labels: [{name: 'subscription-box-comped-tests'}],
|
||||
newsletters: [{
|
||||
id: fixtureManager.get('newsletters', 0).id
|
||||
}],
|
||||
status: 'comped'
|
||||
});
|
||||
|
||||
mockSetting('email_track_clicks', false); // Disable link replacement for this test
|
||||
|
||||
const defaultNewsletter = await getDefaultNewsletter();
|
||||
await models.Newsletter.edit({show_subscription_details: true}, {id: defaultNewsletter.id});
|
||||
|
||||
const {html, plaintext} = await sendEmail({
|
||||
title: 'This is a test post title',
|
||||
mobiledoc: mobileDocExample
|
||||
}, 'label:subscription-box-comped-tests');
|
||||
|
||||
// Currently the link is not present in plaintext version (because no text)
|
||||
assert.equal(html.match(/#\/portal\/account/g).length, 1, 'Subscription details box should contain a link to the account page');
|
||||
|
||||
// Check text matches
|
||||
assert.match(plaintext, /You are receiving this because you are a complimentary subscriber to Ghost\./);
|
||||
|
||||
await lastEmailMatchSnapshot();
|
||||
|
||||
// undo
|
||||
await models.Newsletter.edit({show_subscription_details: false}, {id: defaultNewsletter.id});
|
||||
});
|
||||
|
||||
it('Shows subscription details box for trialing member', async function () {
|
||||
mockSetting('email_track_clicks', false); // Disable link replacement for this test
|
||||
|
||||
// Create a new member without a first_name
|
||||
const customer = stripeMocker.createCustomer({
|
||||
email: 'trialing-paid@example.com'
|
||||
});
|
||||
const price = await stripeMocker.getPriceForTier('default-product', 'month');
|
||||
await stripeMocker.createTrialSubscription({
|
||||
customer,
|
||||
price
|
||||
});
|
||||
|
||||
const member = await models.Member.findOne({email: customer.email}, {require: true});
|
||||
await models.Member.edit({
|
||||
labels: [{name: 'subscription-box-trialing-tests'}],
|
||||
newsletters: [{
|
||||
id: fixtureManager.get('newsletters', 0).id
|
||||
}]
|
||||
}, {id: member.id});
|
||||
|
||||
const defaultNewsletter = await getDefaultNewsletter();
|
||||
await models.Newsletter.edit({show_subscription_details: true}, {id: defaultNewsletter.id});
|
||||
|
||||
const {html, plaintext} = await sendEmail({
|
||||
title: 'This is a test post title',
|
||||
mobiledoc: mobileDocExample
|
||||
}, 'label:subscription-box-trialing-tests');
|
||||
|
||||
// Currently the link is not present in plaintext version (because no text)
|
||||
assert.equal(html.match(/#\/portal\/account/g).length, 1, 'Subscription details box should contain a link to the account page');
|
||||
|
||||
// Check text matches
|
||||
assert.match(plaintext, /You are receiving this because you are a trialing subscriber to Ghost\. Your free trial ends on \d+ \w+ \d+, at which time you will be charged the regular price\. You can always cancel before then\./);
|
||||
|
||||
await lastEmailMatchSnapshot();
|
||||
|
||||
// undo
|
||||
await models.Newsletter.edit({show_subscription_details: false}, {id: defaultNewsletter.id});
|
||||
});
|
||||
|
||||
it('Shows subscription details box for paid member', async function () {
|
||||
mockSetting('email_track_clicks', false); // Disable link replacement for this test
|
||||
|
||||
// Create a new member without a first_name
|
||||
const customer = stripeMocker.createCustomer({
|
||||
email: 'paid@example.com'
|
||||
});
|
||||
const price = await stripeMocker.getPriceForTier('default-product', 'month');
|
||||
await stripeMocker.createSubscription({
|
||||
customer,
|
||||
price
|
||||
});
|
||||
|
||||
const member = await models.Member.findOne({email: customer.email}, {require: true});
|
||||
await models.Member.edit({
|
||||
labels: [{name: 'subscription-box-paid-tests'}],
|
||||
newsletters: [{
|
||||
id: fixtureManager.get('newsletters', 0).id
|
||||
}]
|
||||
}, {id: member.id});
|
||||
|
||||
const defaultNewsletter = await getDefaultNewsletter();
|
||||
await models.Newsletter.edit({show_subscription_details: true}, {id: defaultNewsletter.id});
|
||||
|
||||
const {html, plaintext} = await sendEmail({
|
||||
title: 'This is a test post title',
|
||||
mobiledoc: mobileDocExample
|
||||
}, 'label:subscription-box-paid-tests');
|
||||
|
||||
// Currently the link is not present in plaintext version (because no text)
|
||||
assert.equal(html.match(/#\/portal\/account/g).length, 1, 'Subscription details box should contain a link to the account page');
|
||||
|
||||
// Check text matches
|
||||
assert.match(plaintext, /You are receiving this because you are a paid subscriber to Ghost\. Your subscription will renew on \d+ \w+ \d+\./);
|
||||
|
||||
await lastEmailMatchSnapshot();
|
||||
|
||||
// undo
|
||||
await models.Newsletter.edit({show_subscription_details: false}, {id: defaultNewsletter.id});
|
||||
});
|
||||
|
||||
it('Shows subscription details box for canceled paid member', async function () {
|
||||
mockSetting('email_track_clicks', false); // Disable link replacement for this test
|
||||
|
||||
// Create a new member without a first_name
|
||||
const customer = stripeMocker.createCustomer({
|
||||
email: 'canceled-paid@example.com'
|
||||
});
|
||||
const price = await stripeMocker.getPriceForTier('default-product', 'month');
|
||||
await stripeMocker.createSubscription({
|
||||
customer,
|
||||
price,
|
||||
cancel_at_period_end: true
|
||||
});
|
||||
|
||||
const member = await models.Member.findOne({email: customer.email}, {require: true});
|
||||
await models.Member.edit({
|
||||
labels: [{name: 'subscription-box-canceled-tests'}],
|
||||
newsletters: [{
|
||||
id: fixtureManager.get('newsletters', 0).id
|
||||
}]
|
||||
}, {id: member.id});
|
||||
|
||||
const defaultNewsletter = await getDefaultNewsletter();
|
||||
await models.Newsletter.edit({show_subscription_details: true}, {id: defaultNewsletter.id});
|
||||
|
||||
const {html, plaintext} = await sendEmail({
|
||||
title: 'This is a test post title',
|
||||
mobiledoc: mobileDocExample
|
||||
}, 'label:subscription-box-canceled-tests');
|
||||
|
||||
// Currently the link is not present in plaintext version (because no text)
|
||||
assert.equal(html.match(/#\/portal\/account/g).length, 1, 'Subscription details box should contain a link to the account page');
|
||||
|
||||
// Check text matches
|
||||
assert.match(plaintext, /You are receiving this because you are a paid subscriber to Ghost\. Your subscription has been canceled and will expire on \d+ \w+ \d+\. You can resume your subscription via your account settings\./);
|
||||
|
||||
await lastEmailMatchSnapshot();
|
||||
|
||||
// undo
|
||||
|
@ -85,7 +85,8 @@ class StripeMocker {
|
||||
customer,
|
||||
price,
|
||||
status: 'trialing',
|
||||
trial_end_at: (Date.now() + 1000 * 60 * 60 * 24 * 7) / 1000,
|
||||
trial_start: Date.now() / 1000,
|
||||
trial_end: (Date.now() + 1000 * 60 * 60 * 24 * 7) / 1000,
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ const tpl = require('@tryghost/tpl');
|
||||
|
||||
const messages = {
|
||||
subscriptionStatus: {
|
||||
free: 'You are currently subscribed to the free plan.',
|
||||
free: '',
|
||||
expired: 'Your subscription has expired.',
|
||||
canceled: 'Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.',
|
||||
active: 'Your subscription will renew on {date}.',
|
||||
@ -405,6 +405,29 @@ class EmailRenderer {
|
||||
return url.href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a paid member is trialing a subscription
|
||||
*/
|
||||
isMemberTrialing(member) {
|
||||
// Do we have an active subscription?
|
||||
if (member.status === 'paid') {
|
||||
let activeSubscription = member.subscriptions.find((subscription) => {
|
||||
return subscription.status === 'trialing';
|
||||
});
|
||||
|
||||
if (!activeSubscription) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Translate to a human readable string
|
||||
if (activeSubscription.trial_end_at && activeSubscription.trial_end_at > new Date() && activeSubscription.status === 'trialing') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MemberLike} member
|
||||
* @returns {string}
|
||||
@ -528,6 +551,9 @@ class EmailRenderer {
|
||||
if (member.status === 'comped') {
|
||||
return 'complimentary';
|
||||
}
|
||||
if (this.isMemberTrialing(member)) {
|
||||
return 'trialing';
|
||||
}
|
||||
return member.status;
|
||||
}
|
||||
},
|
||||
|
@ -180,8 +180,7 @@
|
||||
<td class="subscription-box">
|
||||
<h3>Subscription details</h3>
|
||||
<p style="margin-bottom: 16px;">
|
||||
<span>You are receiving this because you are a <strong>%%{status}%% subscriber</strong> to {{site.title}}.</span>
|
||||
<span data-gh-segment="status:-free">%%{status_text}%%</span>
|
||||
<span>You are receiving this because you are a <strong>%%{status}%% subscriber</strong> to {{site.title}}.</span> %%{status_text}%%
|
||||
</p>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
|
@ -203,6 +203,24 @@ describe('Email renderer', function () {
|
||||
assert.equal(replacements[0].getValue(member), 'complimentary');
|
||||
});
|
||||
|
||||
it('returns mapped trialing status', function () {
|
||||
member.status = 'paid';
|
||||
member.subscriptions = [
|
||||
{
|
||||
status: 'trialing',
|
||||
trial_end_at: new Date(2050, 2, 13, 12, 0),
|
||||
current_period_end: new Date(2023, 2, 13, 12, 0),
|
||||
cancel_at_period_end: false
|
||||
}
|
||||
];
|
||||
const html = 'Hello %%{status}%%,';
|
||||
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
|
||||
assert.equal(replacements.length, 1);
|
||||
assert.equal(replacements[0].token.toString(), '/%%\\{status\\}%%/g');
|
||||
assert.equal(replacements[0].id, 'status');
|
||||
assert.equal(replacements[0].getValue(member), 'trialing');
|
||||
});
|
||||
|
||||
it('returns manage_account_url', function () {
|
||||
const html = 'Hello %%{manage_account_url}%%,';
|
||||
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
|
||||
@ -214,11 +232,21 @@ describe('Email renderer', function () {
|
||||
|
||||
it('returns status_text', function () {
|
||||
const html = 'Hello %%{status_text}%%,';
|
||||
member.status = 'paid';
|
||||
member.subscriptions = [
|
||||
{
|
||||
status: 'trialing',
|
||||
trial_end_at: new Date(2050, 2, 13, 12, 0),
|
||||
current_period_end: new Date(2023, 2, 13, 12, 0),
|
||||
cancel_at_period_end: false
|
||||
}
|
||||
];
|
||||
|
||||
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
|
||||
assert.equal(replacements.length, 1);
|
||||
assert.equal(replacements[0].token.toString(), '/%%\\{status_text\\}%%/g');
|
||||
assert.equal(replacements[0].id, 'status_text');
|
||||
assert.equal(replacements[0].getValue(member), 'You are currently subscribed to the free plan.');
|
||||
assert.equal(replacements[0].getValue(member), 'Your free trial ends on 13 March 2050, at which time you will be charged the regular price. You can always cancel before then.');
|
||||
});
|
||||
|
||||
it('returns correct createdAt', function () {
|
||||
@ -279,6 +307,109 @@ describe('Email renderer', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMemberTrialing', function () {
|
||||
let emailRenderer;
|
||||
|
||||
beforeEach(function () {
|
||||
emailRenderer = new EmailRenderer({
|
||||
urlUtils: {
|
||||
urlFor: () => 'http://example.com/subdirectory/'
|
||||
},
|
||||
labs: {
|
||||
isSet: () => true
|
||||
},
|
||||
settingsCache: {
|
||||
get: (key) => {
|
||||
if (key === 'timezone') {
|
||||
return 'UTC';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns false for free member', function () {
|
||||
const member = {
|
||||
id: '456',
|
||||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'free'
|
||||
};
|
||||
|
||||
const result = emailRenderer.isMemberTrialing(member);
|
||||
assert.equal(result, false);
|
||||
});
|
||||
|
||||
it('Returns false for paid member without trial', function () {
|
||||
const member = {
|
||||
id: '456',
|
||||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'paid',
|
||||
subscriptions: [
|
||||
{
|
||||
status: 'active',
|
||||
current_period_end: new Date(2023, 2, 13, 12, 0),
|
||||
cancel_at_period_end: false
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = emailRenderer.isMemberTrialing(member);
|
||||
assert.equal(result, false);
|
||||
});
|
||||
|
||||
it('Returns true for trialing paid member', function () {
|
||||
const member = {
|
||||
id: '456',
|
||||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'paid',
|
||||
subscriptions: [
|
||||
{
|
||||
status: 'trialing',
|
||||
trial_end_at: new Date(2050, 2, 13, 12, 0),
|
||||
current_period_end: new Date(2023, 2, 13, 12, 0),
|
||||
cancel_at_period_end: false
|
||||
}
|
||||
],
|
||||
tiers: []
|
||||
};
|
||||
|
||||
const result = emailRenderer.isMemberTrialing(member);
|
||||
assert.equal(result, true);
|
||||
});
|
||||
|
||||
it('Returns false for expired trialing paid member', function () {
|
||||
const member = {
|
||||
id: '456',
|
||||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'paid',
|
||||
subscriptions: [
|
||||
{
|
||||
status: 'trialing',
|
||||
trial_end_at: new Date(2000, 2, 13, 12, 0),
|
||||
current_period_end: new Date(2023, 2, 13, 12, 0),
|
||||
cancel_at_period_end: false
|
||||
}
|
||||
],
|
||||
tiers: []
|
||||
};
|
||||
|
||||
const result = emailRenderer.isMemberTrialing(member);
|
||||
assert.equal(result, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMemberStatusText', function () {
|
||||
let emailRenderer;
|
||||
|
||||
@ -311,7 +442,7 @@ describe('Email renderer', function () {
|
||||
};
|
||||
|
||||
const result = emailRenderer.getMemberStatusText(member);
|
||||
assert.equal(result, 'You are currently subscribed to the free plan.');
|
||||
assert.equal(result, '');
|
||||
});
|
||||
|
||||
it('Returns for active paid member', function () {
|
||||
|
Loading…
Reference in New Issue
Block a user