From b37ed2756923b2cc5d50270c3ba84e6d8b95636c Mon Sep 17 00:00:00 2001 From: Sag Date: Wed, 3 Jul 2024 19:27:22 +0200 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=8E=A8=20Added=20staff=20notification?= =?UTF-8?q?=20when=20a=20sub=20is=20canceled=20due=20to=20failed=20payment?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes https://linear.app/tryghost/issue/ENG-1254 - when a subscription is canceled automatically by Stripe (e.g. due to multiple failed payments), send a staff notification - so far, we were sending a cancellation staff notification only when a subscription was canceled manually by a member in Portal --- .../lib/repositories/MemberRepository.js | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/ghost/members-api/lib/repositories/MemberRepository.js b/ghost/members-api/lib/repositories/MemberRepository.js index 63f6e7de1d3..ac81be46704 100644 --- a/ghost/members-api/lib/repositories/MemberRepository.js +++ b/ghost/members-api/lib/repositories/MemberRepository.js @@ -1066,7 +1066,7 @@ module.exports = class MemberRepository { const originalMrrDelta = model.get('mrr'); const updatedMrrDelta = updated.get('mrr'); - const getEventName = (originalStatus, updatedStatus) => { + const getEventType = (originalStatus, updatedStatus) => { if (originalStatus === updatedStatus) { return 'updated'; } @@ -1080,12 +1080,14 @@ module.exports = class MemberRepository { const originalStatus = getStatus(model); const updatedStatus = getStatus(updated); + const eventType = getEventType(originalStatus, updatedStatus); const mrrDelta = updatedMrrDelta - originalMrrDelta; + await this._MemberPaidSubscriptionEvent.add({ member_id: member.id, source: 'stripe', - type: getEventName(originalStatus, updatedStatus), + type: eventType, subscription_id: updated.id, from_plan: model.get('plan_id'), to_plan: updated.get('status') === 'canceled' ? null : updated.get('plan_id'), @@ -1110,6 +1112,23 @@ module.exports = class MemberRepository { }); this.dispatchEvent(event, options); } + + // Dispatch cancellation event if: + // 1. The subscription has been set to cancel at period end, by the member in Portal + // 2. The subscription has been immediately canceled (e.g. due to multiple failed payments) + if ((updatedStatus === 'canceled') || (originalStatus !== 'canceled' && updatedStatus === 'expired')) { + const context = options?.context || {}; + const source = this._resolveContextSource(context); + + const event = SubscriptionCancelledEvent.create({ + source, + tierId: ghostProduct?.get('id'), + memberId: member.id, + subscriptionId: updated.get('id') + }, subscription.canceled_at); + + this.dispatchEvent(event, options); + } } } else { eventData.created_at = new Date(subscription.start_date * 1000); @@ -1456,34 +1475,6 @@ module.exports = class MemberRepository { id: member.id, subscription: updatedSubscription }, options); - - // Dispatch cancellation event - if (data.subscription.cancel_at_period_end) { - const stripeProductId = _.get(updatedSubscription, 'items.data[0].price.product'); - - let ghostProduct; - try { - ghostProduct = await this._productRepository.get( - {stripe_product_id: stripeProductId}, - {...sharedOptions, forUpdate: true} - ); - } catch (e) { - ghostProduct = null; - } - - const context = options?.context || {}; - const source = this._resolveContextSource(context); - const cancellationTimestamp = updatedSubscription.canceled_at - ? new Date(updatedSubscription.canceled_at * 1000) - : new Date(); - const cancelEventData = { - source, - memberId: member.id, - subscriptionId: subscriptionModel.get('id'), - tierId: ghostProduct?.get('id') - }; - this.dispatchEvent(SubscriptionCancelledEvent.create(cancelEventData, cancellationTimestamp), options); - } } } From 6bb02b5d67121e81ce2ac93989bd8afd0301d7d3 Mon Sep 17 00:00:00 2001 From: Sag Date: Wed, 3 Jul 2024 21:31:35 +0200 Subject: [PATCH 2/9] Passed cancelation / expiration dates to the staff notification --- ghost/core/test/e2e-api/members/webhooks.test.js | 4 +++- .../member-events/lib/SubscriptionCancelledEvent.js | 2 ++ .../lib/repositories/MemberRepository.js | 10 +++++++--- ghost/staff-service/lib/StaffService.js | 5 +++-- ghost/staff-service/lib/StaffServiceEmails.js | 6 +++--- ghost/staff-service/test/staff-service.test.js | 13 ++++++++----- 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/ghost/core/test/e2e-api/members/webhooks.test.js b/ghost/core/test/e2e-api/members/webhooks.test.js index a0089d8a42d..403f9c8573d 100644 --- a/ghost/core/test/e2e-api/members/webhooks.test.js +++ b/ghost/core/test/e2e-api/members/webhooks.test.js @@ -289,7 +289,7 @@ describe('Members API', function () { // Set the subscription to cancel at the end of the period set(subscription, { ...subscription, - status: 'active', + canceled_at: Date.now() / 1000, cancel_at_period_end: true, metadata: { cancellation_reason: 'I want to break free' @@ -425,6 +425,7 @@ describe('Members API', function () { set(subscription, { ...subscription, status: 'canceled', + canceled_at: Date.now() / 1000, cancellation_details: { reason: 'payment_failed' } @@ -507,6 +508,7 @@ describe('Members API', function () { ] }); + canceledPaidMember = updatedMember; }); diff --git a/ghost/member-events/lib/SubscriptionCancelledEvent.js b/ghost/member-events/lib/SubscriptionCancelledEvent.js index 184a4399bfa..2e44e73c13e 100644 --- a/ghost/member-events/lib/SubscriptionCancelledEvent.js +++ b/ghost/member-events/lib/SubscriptionCancelledEvent.js @@ -4,6 +4,8 @@ * @prop {string} memberId * @prop {string} tierId * @prop {string} subscriptionId + * @prop {Date} expiryAt + * @prop {Date} canceledAt */ module.exports = class SubscriptionCancelledEvent { diff --git a/ghost/members-api/lib/repositories/MemberRepository.js b/ghost/members-api/lib/repositories/MemberRepository.js index ac81be46704..047ff0dbcf4 100644 --- a/ghost/members-api/lib/repositories/MemberRepository.js +++ b/ghost/members-api/lib/repositories/MemberRepository.js @@ -1116,16 +1116,20 @@ module.exports = class MemberRepository { // Dispatch cancellation event if: // 1. The subscription has been set to cancel at period end, by the member in Portal // 2. The subscription has been immediately canceled (e.g. due to multiple failed payments) - if ((updatedStatus === 'canceled') || (originalStatus !== 'canceled' && updatedStatus === 'expired')) { + if (this.isActiveSubscriptionStatus(originalStatus) && (updatedStatus === 'canceled' || updatedStatus === 'expired')) { const context = options?.context || {}; const source = this._resolveContextSource(context); + const canceledAt = new Date(subscription.canceled_at * 1000); + const expiryAt = updatedStatus === 'expired' ? canceledAt : updated.get('current_period_end'); const event = SubscriptionCancelledEvent.create({ source, tierId: ghostProduct?.get('id'), memberId: member.id, - subscriptionId: updated.get('id') - }, subscription.canceled_at); + subscriptionId: updated.get('id'), + canceledAt, + expiryAt + }); this.dispatchEvent(event, options); } diff --git a/ghost/staff-service/lib/StaffService.js b/ghost/staff-service/lib/StaffService.js index 24e67e4d21c..42f117963c4 100644 --- a/ghost/staff-service/lib/StaffService.js +++ b/ghost/staff-service/lib/StaffService.js @@ -119,11 +119,12 @@ class StaffService { attribution }); } else if (type === SubscriptionCancelledEvent) { - subscription.canceledAt = event.timestamp; await this.emails.notifyPaidSubscriptionCanceled({ member, tier, - subscription + subscription, + expiryAt: event.data.expiryAt, + canceledAt: event.data.canceledAt }); } } diff --git a/ghost/staff-service/lib/StaffServiceEmails.js b/ghost/staff-service/lib/StaffServiceEmails.js index abb272ea902..0cda91c0085 100644 --- a/ghost/staff-service/lib/StaffServiceEmails.js +++ b/ghost/staff-service/lib/StaffServiceEmails.js @@ -122,7 +122,7 @@ class StaffServiceEmails { } } - async notifyPaidSubscriptionCanceled({member, tier, subscription}, options = {}) { + async notifyPaidSubscriptionCanceled({member, tier, subscription, expiryAt, canceledAt}, options = {}) { const users = await this.models.User.getEmailAlertUsers('paid-canceled', options); for (const user of users) { @@ -140,8 +140,8 @@ class StaffServiceEmails { }; const subscriptionData = { - expiryAt: this.getFormattedDate(subscription.cancelAt), - canceledAt: this.getFormattedDate(subscription.canceledAt), + expiryAt: this.getFormattedDate(expiryAt), + canceledAt: this.getFormattedDate(canceledAt), cancellationReason: subscription.cancellationReason || '' }; diff --git a/ghost/staff-service/test/staff-service.test.js b/ghost/staff-service/test/staff-service.test.js index b9383ee3729..01f6500f56b 100644 --- a/ghost/staff-service/test/staff-service.test.js +++ b/ghost/staff-service/test/staff-service.test.js @@ -706,6 +706,8 @@ describe('StaffService', function () { let member; let tier; let subscription; + let expiryAt; + let canceledAt; before(function () { member = { name: 'Ghost', @@ -722,17 +724,18 @@ describe('StaffService', function () { subscription = { amount: 5000, currency: 'USD', - interval: 'month', - cancelAt: '2024-08-01T07:30:39.882Z', - canceledAt: '2022-08-05T07:30:39.882Z' + interval: 'month' }; + + expiryAt = '2024-08-01T07:30:39.882Z'; + canceledAt = '2022-08-05T07:30:39.882Z'; }); it('sends paid subscription cancel alert', async function () { await service.emails.notifyPaidSubscriptionCanceled({member, tier, subscription: { ...subscription, cancellationReason: 'Changed my mind!' - }}, options); + }, expiryAt, canceledAt}, options); mailStub.calledOnce.should.be.true(); testCommonPaidSubCancelMailData(stubs); @@ -762,7 +765,7 @@ describe('StaffService', function () { }); it('sends paid subscription cancel alert without reason', async function () { - await service.emails.notifyPaidSubscriptionCanceled({member, tier, subscription}, options); + await service.emails.notifyPaidSubscriptionCanceled({member, tier, subscription, expiryAt, canceledAt}, options); mailStub.calledOnce.should.be.true(); testCommonPaidSubCancelMailData(stubs); From ece1491375f23222fc0d5e57cf5aa56ca5264b7a Mon Sep 17 00:00:00 2001 From: Sag Date: Thu, 4 Jul 2024 10:33:03 +0200 Subject: [PATCH 3/9] Fixed linting --- ghost/core/test/e2e-api/members/webhooks.test.js | 1 - ghost/members-api/lib/repositories/MemberRepository.js | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ghost/core/test/e2e-api/members/webhooks.test.js b/ghost/core/test/e2e-api/members/webhooks.test.js index 403f9c8573d..7d5a6dcf5bb 100644 --- a/ghost/core/test/e2e-api/members/webhooks.test.js +++ b/ghost/core/test/e2e-api/members/webhooks.test.js @@ -508,7 +508,6 @@ describe('Members API', function () { ] }); - canceledPaidMember = updatedMember; }); diff --git a/ghost/members-api/lib/repositories/MemberRepository.js b/ghost/members-api/lib/repositories/MemberRepository.js index 047ff0dbcf4..12b74597b20 100644 --- a/ghost/members-api/lib/repositories/MemberRepository.js +++ b/ghost/members-api/lib/repositories/MemberRepository.js @@ -1113,9 +1113,9 @@ module.exports = class MemberRepository { this.dispatchEvent(event, options); } - // Dispatch cancellation event if: - // 1. The subscription has been set to cancel at period end, by the member in Portal - // 2. The subscription has been immediately canceled (e.g. due to multiple failed payments) + // Dispatch cancellation event, i.e. send paid cancellation staff notification, if: + // 1. The subscription has been set to cancel at period end, by the member in Portal, status 'canceled' + // 2. The subscription has been immediately canceled (e.g. due to multiple failed payments), status 'expired' if (this.isActiveSubscriptionStatus(originalStatus) && (updatedStatus === 'canceled' || updatedStatus === 'expired')) { const context = options?.context || {}; const source = this._resolveContextSource(context); From 8228a5b78d3038b9e10efa19a3b805adf84344a0 Mon Sep 17 00:00:00 2001 From: Sag Date: Thu, 4 Jul 2024 11:18:05 +0200 Subject: [PATCH 4/9] Added tests to check that staff email notifications are sent correctly --- .../test/e2e-api/members/webhooks.test.js | 26 ++++++++++++++++++- .../test/utils/fixtures/data-generator.js | 3 ++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/ghost/core/test/e2e-api/members/webhooks.test.js b/ghost/core/test/e2e-api/members/webhooks.test.js index 7d5a6dcf5bb..e3457796ca8 100644 --- a/ghost/core/test/e2e-api/members/webhooks.test.js +++ b/ghost/core/test/e2e-api/members/webhooks.test.js @@ -353,7 +353,18 @@ describe('Members API', function () { ] }); - canceledPaidMember = updatedMember; + // Check that the staff notifications has been sent + await DomainEvents.allSettled(); + + mockManager.assert.sentEmail({ + subject: /Paid subscription started: Cancel me at the end of the billing cycle/, + to: 'jbloggs@example.com' + }); + + mockManager.assert.sentEmail({ + subject: /Cancellation: Cancel me at the end of the billing cycle/, + to: 'jbloggs@example.com' + }); }); it('Handles immediate cancellation of paid subscriptions', async function () { @@ -508,6 +519,19 @@ describe('Members API', function () { ] }); + // Check that the staff notifications has been sent + await DomainEvents.allSettled(); + + mockManager.assert.sentEmail({ + subject: /Paid subscription started: Cancel me now/, + to: 'jbloggs@example.com' + }); + + mockManager.assert.sentEmail({ + subject: /Cancellation: Cancel me now/, + to: 'jbloggs@example.com' + }); + canceledPaidMember = updatedMember; }); diff --git a/ghost/core/test/utils/fixtures/data-generator.js b/ghost/core/test/utils/fixtures/data-generator.js index be1a079d808..b1c5997acb7 100644 --- a/ghost/core/test/utils/fixtures/data-generator.js +++ b/ghost/core/test/utils/fixtures/data-generator.js @@ -133,7 +133,8 @@ DataGenerator.Content = { slug: 'joe-bloggs', email: 'jbloggs@example.com', password: 'Sl1m3rson99', - profile_image: 'https://example.com/super_photo.jpg' + profile_image: 'https://example.com/super_photo.jpg', + paid_subscription_canceled_notification: true }, { // admin From 8de74983d45737780490d48d6436c34a1d5c815a Mon Sep 17 00:00:00 2001 From: Sag Date: Thu, 11 Jul 2024 09:30:53 +0200 Subject: [PATCH 5/9] Updated cancellation email template --- .../member-events/lib/SubscriptionCancelledEvent.js | 1 + .../members-api/lib/repositories/MemberRepository.js | 4 +++- ghost/staff-service/lib/StaffService.js | 3 +-- ghost/staff-service/lib/StaffServiceEmails.js | 3 ++- .../lib/email-templates/new-paid-cancellation.hbs | 12 +++++++++--- .../lib/email-templates/new-paid-cancellation.txt.js | 2 +- 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/ghost/member-events/lib/SubscriptionCancelledEvent.js b/ghost/member-events/lib/SubscriptionCancelledEvent.js index 2e44e73c13e..81fa4222279 100644 --- a/ghost/member-events/lib/SubscriptionCancelledEvent.js +++ b/ghost/member-events/lib/SubscriptionCancelledEvent.js @@ -4,6 +4,7 @@ * @prop {string} memberId * @prop {string} tierId * @prop {string} subscriptionId + * @prop {boolean} cancelNow * @prop {Date} expiryAt * @prop {Date} canceledAt */ diff --git a/ghost/members-api/lib/repositories/MemberRepository.js b/ghost/members-api/lib/repositories/MemberRepository.js index 12b74597b20..c2830940f85 100644 --- a/ghost/members-api/lib/repositories/MemberRepository.js +++ b/ghost/members-api/lib/repositories/MemberRepository.js @@ -1119,14 +1119,16 @@ module.exports = class MemberRepository { if (this.isActiveSubscriptionStatus(originalStatus) && (updatedStatus === 'canceled' || updatedStatus === 'expired')) { const context = options?.context || {}; const source = this._resolveContextSource(context); + const cancelNow = updatedStatus === 'expired'; const canceledAt = new Date(subscription.canceled_at * 1000); - const expiryAt = updatedStatus === 'expired' ? canceledAt : updated.get('current_period_end'); + const expiryAt = cancelNow ? canceledAt : updated.get('current_period_end'); const event = SubscriptionCancelledEvent.create({ source, tierId: ghostProduct?.get('id'), memberId: member.id, subscriptionId: updated.get('id'), + cancelNow, canceledAt, expiryAt }); diff --git a/ghost/staff-service/lib/StaffService.js b/ghost/staff-service/lib/StaffService.js index 42f117963c4..02d4e1b4dd7 100644 --- a/ghost/staff-service/lib/StaffService.js +++ b/ghost/staff-service/lib/StaffService.js @@ -123,8 +123,7 @@ class StaffService { member, tier, subscription, - expiryAt: event.data.expiryAt, - canceledAt: event.data.canceledAt + ...event.data }); } } diff --git a/ghost/staff-service/lib/StaffServiceEmails.js b/ghost/staff-service/lib/StaffServiceEmails.js index 0cda91c0085..b107af3ee10 100644 --- a/ghost/staff-service/lib/StaffServiceEmails.js +++ b/ghost/staff-service/lib/StaffServiceEmails.js @@ -122,7 +122,7 @@ class StaffServiceEmails { } } - async notifyPaidSubscriptionCanceled({member, tier, subscription, expiryAt, canceledAt}, options = {}) { + async notifyPaidSubscriptionCanceled({member, tier, subscription, cancelNow, expiryAt, canceledAt}, options = {}) { const users = await this.models.User.getEmailAlertUsers('paid-canceled', options); for (const user of users) { @@ -141,6 +141,7 @@ class StaffServiceEmails { const subscriptionData = { expiryAt: this.getFormattedDate(expiryAt), + cancelNow: cancelNow, canceledAt: this.getFormattedDate(canceledAt), cancellationReason: subscription.cancellationReason || '' }; diff --git a/ghost/staff-service/lib/email-templates/new-paid-cancellation.hbs b/ghost/staff-service/lib/email-templates/new-paid-cancellation.hbs index 40e4e67cdec..208b4b15147 100644 --- a/ghost/staff-service/lib/email-templates/new-paid-cancellation.hbs +++ b/ghost/staff-service/lib/email-templates/new-paid-cancellation.hbs @@ -19,7 +19,7 @@ {{#if subscriptionData.cancellationReason}} Reason: {{subscriptionData.cancellationReason}} {{else}} - A paid member has just canceled their subscription. + A paid member's subscription has just been canceled. {{/if}} {{/inline}} {{/preview}} @@ -33,7 +33,7 @@

Hey there,

-

A paid member has just canceled their subscription.

+

A paid member's subscription has just been canceled.

@@ -51,7 +51,9 @@ {{#if memberData.showEmail}} {{/if}} -

Canceled on {{subscriptionData.canceledAt}}

+ {{#unless subscriptionData.cancelNow}} +

Canceled on {{subscriptionData.canceledAt}}

+ {{/unless}}
@@ -66,7 +68,11 @@ + {{#if cancelNow}} +

Subscription expired on

+ {{else}}

Subscription will expire on

+ {{/if}}

{{subscriptionData.expiryAt}}

diff --git a/ghost/staff-service/lib/email-templates/new-paid-cancellation.txt.js b/ghost/staff-service/lib/email-templates/new-paid-cancellation.txt.js index 40c2bc15233..f2a4a269583 100644 --- a/ghost/staff-service/lib/email-templates/new-paid-cancellation.txt.js +++ b/ghost/staff-service/lib/email-templates/new-paid-cancellation.txt.js @@ -3,7 +3,7 @@ module.exports = function (data) { return ` Hey there, -A paid member has just cancelled their subscription: "${data.memberData.name}" +A paid member's subscription has just been canceled: "${data.memberData.name}" --- From 5beb251c8521a61e3f15cc25919afcc18c817ca0 Mon Sep 17 00:00:00 2001 From: Sag Date: Thu, 11 Jul 2024 09:33:15 +0200 Subject: [PATCH 6/9] Updated regression test snapshot, as fixtures have been changed --- .../api/admin/__snapshots__/authentication.test.js.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/core/test/regression/api/admin/__snapshots__/authentication.test.js.snap b/ghost/core/test/regression/api/admin/__snapshots__/authentication.test.js.snap index 825e3e030b6..3c8192c98b9 100644 --- a/ghost/core/test/regression/api/admin/__snapshots__/authentication.test.js.snap +++ b/ghost/core/test/regression/api/admin/__snapshots__/authentication.test.js.snap @@ -594,7 +594,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "test user edit", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -614,7 +614,7 @@ exports[`Authentication API Blog setup update setup 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": "794", + "content-length": "793", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, From da63e0b4b1b9399a4399530ea0b66b04dd54a966 Mon Sep 17 00:00:00 2001 From: Sag Date: Thu, 11 Jul 2024 09:58:45 +0200 Subject: [PATCH 7/9] Updated staff service tests --- .../email-templates/new-paid-cancellation.hbs | 2 +- .../staff-service/test/staff-service.test.js | 63 +++++++++++++++---- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/ghost/staff-service/lib/email-templates/new-paid-cancellation.hbs b/ghost/staff-service/lib/email-templates/new-paid-cancellation.hbs index 208b4b15147..3a44cff1197 100644 --- a/ghost/staff-service/lib/email-templates/new-paid-cancellation.hbs +++ b/ghost/staff-service/lib/email-templates/new-paid-cancellation.hbs @@ -68,7 +68,7 @@ - {{#if cancelNow}} + {{#if subscriptionData.cancelNow}}

Subscription expired on

{{else}}

Subscription will expire on

diff --git a/ghost/staff-service/test/staff-service.test.js b/ghost/staff-service/test/staff-service.test.js index 01f6500f56b..bf8b03d0b54 100644 --- a/ghost/staff-service/test/staff-service.test.js +++ b/ghost/staff-service/test/staff-service.test.js @@ -708,6 +708,7 @@ describe('StaffService', function () { let subscription; let expiryAt; let canceledAt; + let cancelNow; before(function () { member = { name: 'Ghost', @@ -727,29 +728,31 @@ describe('StaffService', function () { interval: 'month' }; - expiryAt = '2024-08-01T07:30:39.882Z'; + expiryAt = '2024-09-05T07:30:39.882Z'; canceledAt = '2022-08-05T07:30:39.882Z'; + cancelNow = false; }); - it('sends paid subscription cancel alert', async function () { + it('sends paid subscription cancel notification when sub is canceled at the end of billing period', async function () { await service.emails.notifyPaidSubscriptionCanceled({member, tier, subscription: { ...subscription, cancellationReason: 'Changed my mind!' - }, expiryAt, canceledAt}, options); + }, expiryAt, canceledAt, cancelNow}, options); mailStub.calledOnce.should.be.true(); testCommonPaidSubCancelMailData(stubs); mailStub.calledWith( - sinon.match.has('html', sinon.match('Subscription will expire on')) + sinon.match.has('html', sinon.match('Canceled on 5 Aug 2022')) ).should.be.true(); + // Expiration sentence is in the future tense mailStub.calledWith( - sinon.match.has('html', sinon.match('Canceled on 5 Aug 2022')) + sinon.match.has('html', sinon.match('Subscription will expire on')) ).should.be.true(); mailStub.calledWith( - sinon.match.has('html', sinon.match('1 Aug 2024')) + sinon.match.has('html', sinon.match('5 Sep 2024')) ).should.be.true(); mailStub.calledWith( @@ -764,34 +767,68 @@ describe('StaffService', function () { ).should.be.true(); }); - it('sends paid subscription cancel alert without reason', async function () { - await service.emails.notifyPaidSubscriptionCanceled({member, tier, subscription, expiryAt, canceledAt}, options); + it('sends paid subscription cancel alert when sub is canceled without reason', async function () { + await service.emails.notifyPaidSubscriptionCanceled({member, tier, subscription, expiryAt, canceledAt, cancelNow}, options); mailStub.calledOnce.should.be.true(); testCommonPaidSubCancelMailData(stubs); mailStub.calledWith( - sinon.match.has('html', sinon.match('Subscription will expire on')) + sinon.match.has('html', sinon.match('Canceled on 5 Aug 2022')) ).should.be.true(); + // Expiration sentence is in the future tense mailStub.calledWith( - sinon.match.has('html', sinon.match('Canceled on 5 Aug 2022')) + sinon.match.has('html', sinon.match('Subscription will expire on')) ).should.be.true(); mailStub.calledWith( - sinon.match.has('html', sinon.match('1 Aug 2024')) + sinon.match.has('html', sinon.match('5 Sep 2024')) ).should.be.true(); + // Cancellation reason block is hidden mailStub.calledWith( sinon.match.has('html', sinon.match('Reason: ')) ).should.be.false(); mailStub.calledWith( sinon.match.has('html', sinon.match('Cancellation reason')) ).should.be.false(); + }); + + it('sends paid subscription cancel alert when subscription is canceled immediately', async function () { + cancelNow = true; + await service.emails.notifyPaidSubscriptionCanceled({member, tier, subscription: { + ...subscription, + cancellationReason: 'Payment failed' + }, expiryAt, canceledAt, cancelNow}, options); + + mailStub.calledOnce.should.be.true(); + testCommonPaidSubCancelMailData(stubs); + + // We don't show "Canceled on" when subscription is canceled immediately + mailStub.calledWith( + sinon.match.has('html', sinon.match('Canceled on')) + ).should.be.false(); + + // Expiration sentence is in the past tense + mailStub.calledWith( + sinon.match.has('html', sinon.match('Subscription expired on')) + ).should.be.true(); + + mailStub.calledWith( + sinon.match.has('html', sinon.match('5 Sep 2024')) + ).should.be.true(); + + mailStub.calledWith( + sinon.match.has('html', 'Offer') + ).should.be.false(); + + mailStub.calledWith( + sinon.match.has('html', sinon.match('Cancellation reason')) + ).should.be.true(); - // check preview text mailStub.calledWith( - sinon.match.has('html', sinon.match('A paid member has just canceled their subscription.')) + sinon.match.has('html', sinon.match('Reason: Payment failed')) ).should.be.true(); }); }); From 05c58b8d45b28c339efdf6c88316d627789a21ef Mon Sep 17 00:00:00 2001 From: Sag Date: Thu, 11 Jul 2024 10:07:08 +0200 Subject: [PATCH 8/9] Updated snapshots --- .../e2e-api/admin/__snapshots__/collections.test.js.snap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/collections.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/collections.test.js.snap index c7747b2ec6a..8c24463095a 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/collections.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/collections.test.js.snap @@ -207,7 +207,7 @@ exports[`Collections API Automatic Collection Filtering Creates an automatic Col 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": "12653", + "content-length": "12649", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -728,7 +728,7 @@ exports[`Collections API Automatic Collection Filtering Creates an automatic Col 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": "78564", + "content-length": "78560", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -987,7 +987,7 @@ exports[`Collections API Automatic Collection Filtering Creates an automatic Col 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": "14427", + "content-length": "14423", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1246,7 +1246,7 @@ exports[`Collections API Automatic Collection Filtering Creates an automatic Col 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": "14427", + "content-length": "14423", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, From 3e9de5a3eef40a973790d3b23e33766edf3fa6eb Mon Sep 17 00:00:00 2001 From: Sag Date: Thu, 11 Jul 2024 10:38:18 +0200 Subject: [PATCH 9/9] Updated more snapshots --- .../admin/__snapshots__/members.test.js.snap | 2 +- .../admin/__snapshots__/pages.test.js.snap | 12 +++--- .../admin/__snapshots__/posts.test.js.snap | 36 ++++++++-------- .../admin/__snapshots__/session.test.js.snap | 4 +- .../__snapshots__/pages.test.js.snap | 42 +++++++++---------- .../__snapshots__/posts.test.js.snap | 42 +++++++++---------- 6 files changed, 69 insertions(+), 69 deletions(-) diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap index 23a80d62ff1..dc329b91d45 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap @@ -381,7 +381,7 @@ exports[`Members API - member attribution Returns sign up attributions of all ty 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": "8054", + "content-length": "8053", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/pages.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/pages.test.js.snap index 9af3952e41c..fd6baad54e9 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/pages.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/pages.test.js.snap @@ -647,7 +647,7 @@ exports[`Pages API Convert can convert a mobiledoc page to lexical 2: [headers] 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": "4065", + "content-length": "4063", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -753,7 +753,7 @@ exports[`Pages API Convert can convert a mobiledoc page to lexical 4: [headers] 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": "4065", + "content-length": "4063", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -947,7 +947,7 @@ exports[`Pages API Copy Can copy a page 3: [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": "3859", + "content-length": "3857", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1053,7 +1053,7 @@ exports[`Pages API Create Can create a page with html 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": "4089", + "content-length": "4087", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1333,7 +1333,7 @@ exports[`Pages API Update Access Visibility is set to tiers Saves only paid tier 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": "3494", + "content-length": "3492", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1438,7 +1438,7 @@ exports[`Pages API Update Can modify show_title_and_feature_image property 2: [h 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": "3860", + "content-length": "3858", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap index dceba661f65..3e90545e516 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap @@ -197,7 +197,7 @@ exports[`Posts 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": "10736", + "content-length": "10732", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -421,7 +421,7 @@ exports[`Posts API Can browse filtering by a collection 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": "11731", + "content-length": "11727", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -766,7 +766,7 @@ exports[`Posts API Can browse with formats 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": "13534", + "content-length": "13530", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -874,7 +874,7 @@ exports[`Posts API Convert can convert a mobiledoc post to lexical 2: [headers] 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": "4102", + "content-length": "4100", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -983,7 +983,7 @@ exports[`Posts API Convert can convert a mobiledoc post to lexical 4: [headers] 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": "4102", + "content-length": "4100", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1089,7 +1089,7 @@ exports[`Posts API Copy Can copy a post 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": "3894", + "content-length": "3892", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1198,7 +1198,7 @@ exports[`Posts API Create Can create a post with html 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": "4124", + "content-length": "4122", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1307,7 +1307,7 @@ exports[`Posts API Create Can create a post with lexical 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": "4136", + "content-length": "4134", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1416,7 +1416,7 @@ exports[`Posts API Create Can create a post with mobiledoc 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": "3952", + "content-length": "3950", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1703,7 +1703,7 @@ exports[`Posts API Delete Can delete posts belonging to a collection and returns 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": "11731", + "content-length": "11727", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2054,7 +2054,7 @@ exports[`Posts API Update Access Visibility is set to tiers Saves only paid tier 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": "3527", + "content-length": "3525", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2161,7 +2161,7 @@ exports[`Posts API Update Can add and remove collections 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": "3908", + "content-length": "3906", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2384,7 +2384,7 @@ exports[`Posts API Update Can add and remove collections 4: [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": "5504", + "content-length": "5502", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2607,7 +2607,7 @@ exports[`Posts API Update Can add and remove collections 6: [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": "5498", + "content-length": "5496", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2716,7 +2716,7 @@ exports[`Posts API Update Can update a post with lexical 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": "4087", + "content-length": "4085", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2825,7 +2825,7 @@ exports[`Posts API Update Can update a post with lexical 4: [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": "4084", + "content-length": "4082", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2934,7 +2934,7 @@ exports[`Posts API Update Can update a post with mobiledoc 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": "3897", + "content-length": "3895", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -3043,7 +3043,7 @@ exports[`Posts API Update Can update a post with mobiledoc 4: [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": "3894", + "content-length": "3892", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/session.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/session.test.js.snap index 7a43819c7aa..1b6a4e31cc6 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/session.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/session.test.js.snap @@ -48,7 +48,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -67,7 +67,7 @@ exports[`Sessions API can read session now the owner is logged in 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": "803", + "content-length": "802", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-webhooks/__snapshots__/pages.test.js.snap b/ghost/core/test/e2e-webhooks/__snapshots__/pages.test.js.snap index 7da288e7d4d..a52e57c2b0f 100644 --- a/ghost/core/test/e2e-webhooks/__snapshots__/pages.test.js.snap +++ b/ghost/core/test/e2e-webhooks/__snapshots__/pages.test.js.snap @@ -33,7 +33,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -86,7 +86,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -190,7 +190,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -270,7 +270,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -338,7 +338,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -492,7 +492,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -560,7 +560,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -714,7 +714,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -782,7 +782,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -935,7 +935,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1003,7 +1003,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1156,7 +1156,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1224,7 +1224,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1378,7 +1378,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1446,7 +1446,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1644,7 +1644,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1712,7 +1712,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1850,7 +1850,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1918,7 +1918,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -2071,7 +2071,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -2139,7 +2139,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, diff --git a/ghost/core/test/e2e-webhooks/__snapshots__/posts.test.js.snap b/ghost/core/test/e2e-webhooks/__snapshots__/posts.test.js.snap index 0ffa5816c5f..8620e388510 100644 --- a/ghost/core/test/e2e-webhooks/__snapshots__/posts.test.js.snap +++ b/ghost/core/test/e2e-webhooks/__snapshots__/posts.test.js.snap @@ -33,7 +33,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -89,7 +89,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -192,7 +192,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -271,7 +271,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -341,7 +341,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -493,7 +493,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -584,7 +584,7 @@ Header Level 3 "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -738,7 +738,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -808,7 +808,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -960,7 +960,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1030,7 +1030,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1182,7 +1182,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1252,7 +1252,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1405,7 +1405,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1496,7 +1496,7 @@ Header Level 3 "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1694,7 +1694,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1785,7 +1785,7 @@ Header Level 3 "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -1923,7 +1923,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -2014,7 +2014,7 @@ Header Level 3 "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -2167,7 +2167,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true, @@ -2237,7 +2237,7 @@ Object { "meta_title": null, "milestone_notifications": true, "name": "Joe Bloggs", - "paid_subscription_canceled_notification": false, + "paid_subscription_canceled_notification": true, "paid_subscription_started_notification": true, "profile_image": "https://example.com/super_photo.jpg", "recommendation_notifications": true,