e476eebd2d
ref https://linear.app/tryghost/issue/ENG-1254 - when a subscription is canceled automatically by Stripe (e.g. due to multiple failed payments), we now send a staff notification - logic before: if a member cancels a sub in Portal, then send a staff notification - logic now: if a subscription was active, but is now set to cancel immediately or at the end of the billing period, then send a staff notification. - with that logic change, we now send a cancellation staff notification when: 1. A member cancels their sub in Portal (existing) 2. A staff member cancels a member sub in Stripe (new) 3. A staff member cancels a member sub in Admin (new) 4. A sub is canceled automatically by Stripe because of multiple failed payments (new) - the copy of the staff notification email has also been updated to take into account 1) manual vs automatic cancellations, and 2) immediate vs end of billing period cancellations
171 lines
6.5 KiB
JavaScript
171 lines
6.5 KiB
JavaScript
const {MemberCreatedEvent, SubscriptionCancelledEvent, SubscriptionActivatedEvent} = require('@tryghost/member-events');
|
|
const {MilestoneCreatedEvent} = require('@tryghost/milestones');
|
|
|
|
// @NOTE: 'StaffService' is a vague name that does not describe what it's actually doing.
|
|
// Possibly, "StaffNotificationService" or "StaffEventNotificationService" would be a more accurate name
|
|
class StaffService {
|
|
constructor({logging, models, mailer, settingsCache, settingsHelpers, urlUtils, DomainEvents, labs, memberAttributionService}) {
|
|
this.logging = logging;
|
|
this.labs = labs;
|
|
/** @private */
|
|
this.settingsCache = settingsCache;
|
|
this.models = models;
|
|
this.DomainEvents = DomainEvents;
|
|
this.memberAttributionService = memberAttributionService;
|
|
|
|
const Emails = require('./StaffServiceEmails');
|
|
|
|
this.emails = new Emails({
|
|
logging,
|
|
models,
|
|
mailer,
|
|
settingsHelpers,
|
|
settingsCache,
|
|
urlUtils,
|
|
labs
|
|
});
|
|
}
|
|
|
|
/** @private */
|
|
getSerializedData({member, tier = null, subscription = null, offer = null}) {
|
|
return {
|
|
offer: offer ? {
|
|
name: offer.name,
|
|
type: offer.discount_type,
|
|
currency: offer.currency,
|
|
duration: offer.duration,
|
|
durationInMonths: offer.duration_in_months,
|
|
amount: offer.discount_amount
|
|
} : null,
|
|
subscription: subscription ? {
|
|
id: subscription.id,
|
|
amount: subscription.plan?.amount,
|
|
interval: subscription.plan?.interval,
|
|
currency: subscription.plan?.currency,
|
|
startDate: subscription.start_date,
|
|
cancelAt: subscription.current_period_end,
|
|
cancellationReason: subscription.cancellation_reason
|
|
} : null,
|
|
member: member ? {
|
|
id: member.id,
|
|
name: member.name,
|
|
email: member.email,
|
|
geolocation: member.geolocation,
|
|
status: member.status,
|
|
created_at: member.created_at
|
|
} : null,
|
|
tier: tier ? {
|
|
id: tier.id,
|
|
name: tier.name
|
|
} : null
|
|
};
|
|
}
|
|
|
|
/** @private */
|
|
async getDataFromIds({memberId, tierId = null, subscriptionId = null, offerId = null}) {
|
|
const memberModel = memberId ? await this.models.Member.findOne({id: memberId}) : null;
|
|
const tierModel = tierId ? await this.models.Product.findOne({id: tierId}) : null;
|
|
const subscriptionModel = subscriptionId ? await this.models.StripeCustomerSubscription.findOne({id: subscriptionId}) : null;
|
|
const offerModel = offerId ? await this.models.Offer.findOne({id: offerId}) : null;
|
|
|
|
return this.getSerializedData({
|
|
member: memberModel?.toJSON(),
|
|
tier: tierModel?.toJSON(),
|
|
subscription: subscriptionModel?.toJSON(),
|
|
offer: offerModel?.toJSON()
|
|
});
|
|
}
|
|
|
|
/** @private */
|
|
async handleEvent(type, event) {
|
|
if (type === MilestoneCreatedEvent && event.data.milestone) {
|
|
await this.emails.notifyMilestoneReceived(event.data);
|
|
}
|
|
|
|
if (!['api', 'member'].includes(event.data.source)) {
|
|
return;
|
|
}
|
|
|
|
const {member, tier, subscription, offer} = await this.getDataFromIds({
|
|
memberId: event.data.memberId,
|
|
tierId: event.data.tierId,
|
|
subscriptionId: event.data.subscriptionId,
|
|
offerId: event.data.offerId
|
|
});
|
|
|
|
if (type === MemberCreatedEvent && member.status === 'free') {
|
|
let attribution;
|
|
try {
|
|
attribution = await this.memberAttributionService.getMemberCreatedAttribution(event.data.memberId);
|
|
} catch (e) {
|
|
this.logging.warn(`Failed to get attribution for member - ${event?.data?.memberId}`);
|
|
}
|
|
await this.emails.notifyFreeMemberSignup({
|
|
member,
|
|
attribution
|
|
});
|
|
} else if (type === SubscriptionActivatedEvent) {
|
|
let attribution;
|
|
try {
|
|
attribution = await this.memberAttributionService.getSubscriptionCreatedAttribution(event.data.subscriptionId);
|
|
} catch (e) {
|
|
this.logging.warn(`Failed to get attribution for member - ${event?.data?.memberId}`);
|
|
}
|
|
await this.emails.notifyPaidSubscriptionStarted({
|
|
member,
|
|
offer,
|
|
tier,
|
|
subscription,
|
|
attribution
|
|
});
|
|
} else if (type === SubscriptionCancelledEvent) {
|
|
await this.emails.notifyPaidSubscriptionCanceled({
|
|
member,
|
|
tier,
|
|
subscription,
|
|
...event.data
|
|
});
|
|
}
|
|
}
|
|
|
|
subscribeEvents() {
|
|
// Trigger email for free member signup
|
|
this.DomainEvents.subscribe(MemberCreatedEvent, async (event) => {
|
|
try {
|
|
await this.handleEvent(MemberCreatedEvent, event);
|
|
} catch (e) {
|
|
this.logging.error(e, `Failed to notify free member signup - ${event?.data?.memberId}`);
|
|
}
|
|
});
|
|
|
|
// Trigger email on paid subscription start
|
|
this.DomainEvents.subscribe(SubscriptionActivatedEvent, async (event) => {
|
|
try {
|
|
await this.handleEvent(SubscriptionActivatedEvent, event);
|
|
} catch (e) {
|
|
this.logging.error(e, `Failed to notify paid member subscription start - ${event?.data?.memberId}`);
|
|
}
|
|
});
|
|
|
|
// Trigger email when a member cancels their subscription
|
|
this.DomainEvents.subscribe(SubscriptionCancelledEvent, async (event) => {
|
|
try {
|
|
await this.handleEvent(SubscriptionCancelledEvent, event);
|
|
} catch (e) {
|
|
this.logging.error(e, `Failed to notify paid member subscription cancel - ${event?.data?.memberId}`);
|
|
}
|
|
});
|
|
|
|
// Trigger email when a new milestone is reached
|
|
this.DomainEvents.subscribe(MilestoneCreatedEvent, async (event) => {
|
|
try {
|
|
await this.handleEvent(MilestoneCreatedEvent, event);
|
|
} catch (e) {
|
|
this.logging.error(e, `Failed to notify milestone`);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = StaffService;
|