Ghost/ghost/staff-service/lib/staff-service.js
Rishabh Garg c71582877c
Added page attribution to member email alerts (#16381)
refs https://github.com/TryGhost/Team/issues/2489

- adds attribution title and url for new free members and paid subscriptions to email alert

---------

Co-authored-by: Peter Zimon <peter.zimon@gmail.com>
2023-03-10 20:14:53 +05:30

185 lines
7.1 KiB
JavaScript

const {MemberCreatedEvent, SubscriptionCancelledEvent, SubscriptionActivatedEvent} = require('@tryghost/member-events');
const {MentionCreatedEvent} = require('@tryghost/webmentions');
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('./emails');
/** @private */
this.emails = new Emails({
logging,
models,
mailer,
settingsHelpers,
settingsCache,
urlUtils
});
}
/** @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 === MentionCreatedEvent && event.data.mention && this.labs.isSet('webmentions') && this.labs.isSet('webmentionEmails')) {
return await this.emails.notifyMentionReceived(event.data);
}
if (type === MilestoneCreatedEvent && event.data.milestone && this.labs.isSet('milestoneEmails')) {
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) {
subscription.canceledAt = event.timestamp;
await this.emails.notifyPaidSubscriptionCanceled({
member,
tier,
subscription
});
}
}
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 webmention is received
this.DomainEvents.subscribe(MentionCreatedEvent, async (event) => {
try {
await this.handleEvent(MentionCreatedEvent, event);
} catch (e) {
this.logging.error(e, `Failed to notify webmention`);
}
});
// 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;