Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🎨 Added staff notification when a sub is canceled due to failed payments #20534

Merged
merged 9 commits into from
Jul 15, 2024
Prev Previous commit
Next Next commit
Passed cancelation / expiration dates to the staff notification
  • Loading branch information
sagzy committed Jul 4, 2024
commit 6bb02b5d67121e81ce2ac93989bd8afd0301d7d3
4 changes: 3 additions & 1 deletion ghost/core/test/e2e-api/members/webhooks.test.js
Original file line number Diff line number Diff line change
@@ -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;
});

2 changes: 2 additions & 0 deletions ghost/member-events/lib/SubscriptionCancelledEvent.js
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@
* @prop {string} memberId
* @prop {string} tierId
* @prop {string} subscriptionId
* @prop {Date} expiryAt
* @prop {Date} canceledAt
*/

module.exports = class SubscriptionCancelledEvent {
10 changes: 7 additions & 3 deletions ghost/members-api/lib/repositories/MemberRepository.js
Original file line number Diff line number Diff line change
@@ -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);
}
5 changes: 3 additions & 2 deletions ghost/staff-service/lib/StaffService.js
Original file line number Diff line number Diff line change
@@ -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
});
}
}
6 changes: 3 additions & 3 deletions ghost/staff-service/lib/StaffServiceEmails.js
Original file line number Diff line number Diff line change
@@ -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 || ''
};

13 changes: 8 additions & 5 deletions ghost/staff-service/test/staff-service.test.js
Original file line number Diff line number Diff line change
@@ -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);