Added donation events to activity feeds (#17632)

fixes TryGhost/Product#3698
fixes TryGhost/Product#3699
This commit is contained in:
Simon Backx 2023-08-10 09:16:47 +02:00 committed by GitHub
parent 374bfc405c
commit 874552bdbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 103 additions and 10 deletions

View File

@ -93,8 +93,18 @@
{{#let (parse-member-event event eventsFetcher.hasMultipleNewsletters) as |parsedEvent|}} {{#let (parse-member-event event eventsFetcher.hasMultipleNewsletters) as |parsedEvent|}}
<div class="gh-dashboard-list-item member-details"> <div class="gh-dashboard-list-item member-details">
<div class="gh-dashboard-list-item-sub"> <div class="gh-dashboard-list-item-sub">
<GhMemberAvatar @member={{parsedEvent.member}} @containerClass="w8 h8 mr3 flex-shrink-0" /> {{#if parsedEvent.member}}
<LinkTo class="gh-dashboard-list-text" @route="member" @model="{{parsedEvent.memberId}}" data-test-dashboard-member-activity-item>{{parsedEvent.subject}}</LinkTo> <GhMemberAvatar @member={{parsedEvent.member}} @containerClass="w8 h8 mr3 flex-shrink-0" />
<LinkTo class="gh-dashboard-list-text" @route="member" @model="{{parsedEvent.memberId}}" data-test-dashboard-member-activity-item>{{parsedEvent.subject}}</LinkTo>
{{else}}
{{#if parsedEvent.email}}
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w8 h8 mr3 flex-shrink-0" />
<a class="gh-dashboard-list-text" href="mailto:{{parsedEvent.email}}" target="_blank" rel="noopener noreferrer" title="{{parsedEvent.email}}">{{parsedEvent.subject}}</a>
{{else}}
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w8 h8 mr3 flex-shrink-0" />
<span class="gh-dashboard-list-text">{{parsedEvent.subject}}</span>
{{/if}}
{{/if}}
</div> </div>
<div class="gh-dashboard-list-item-sub"> <div class="gh-dashboard-list-item-sub">
{{svg-jar parsedEvent.icon }} {{svg-jar parsedEvent.icon }}

View File

@ -13,9 +13,9 @@ const stringToHslColor = function (str, saturation, lightness) {
export default class GhMemberAvatarComponent extends Component { export default class GhMemberAvatarComponent extends Component {
get memberName() { get memberName() {
let {member} = this.args; let {member, name} = this.args;
return member?.name || member?.email || 'NM'; return member?.name || member?.email || name || 'NM';
} }
get avatarImage() { get avatarImage() {

View File

@ -45,8 +45,18 @@
{{#let (parse-member-event event) as |parsedEvent|}} {{#let (parse-member-event event) as |parsedEvent|}}
<div class="gh-dashboard-list-item"> <div class="gh-dashboard-list-item">
<div class="gh-dashboard-list-item-sub"> <div class="gh-dashboard-list-item-sub">
<GhMemberAvatar @member={{parsedEvent.member}} @containerClass="w6 h6 mr3 flex-shrink-0" /> {{#if parsedEvent.member}}
<LinkTo class="gh-dashboard-list-text" @route="member" @model="{{parsedEvent.memberId}}" @query={{hash postAnalytics=@post.id}}>{{parsedEvent.subject}}</LinkTo> <GhMemberAvatar @member={{parsedEvent.member}} @containerClass="w6 h6 mr3 flex-shrink-0" />
<LinkTo class="gh-dashboard-list-text" @route="member" @model="{{parsedEvent.memberId}}" @query={{hash postAnalytics=@post.id}}>{{parsedEvent.subject}}</LinkTo>
{{else}}
{{#if parsedEvent.email}}
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w6 h6 mr3 flex-shrink-0" />
<a class="gh-dashboard-list-text" href="mailto:{{parsedEvent.email}}" target="_blank" rel="noopener noreferrer" title="{{parsedEvent.email}}">{{parsedEvent.subject}}</a>
{{else}}
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w6 h6 mr3 flex-shrink-0" />
<span class="gh-dashboard-list-text">{{parsedEvent.subject}}</span>
{{/if}}
{{/if}}
</div> </div>
<div class="gh-dashboard-list-item-sub"> <div class="gh-dashboard-list-item-sub">
{{svg-jar parsedEvent.icon }} {{svg-jar parsedEvent.icon }}

View File

@ -10,7 +10,7 @@ export default class ParseMemberEventHelper extends Helper {
@service membersUtils; @service membersUtils;
compute([event, hasMultipleNewsletters]) { compute([event, hasMultipleNewsletters]) {
const subject = event.data.member.name || event.data.member.email; const subject = event.data.member ? (event.data.member.name || event.data.member.email) : (event.data.name || event.data.email || '');
const icon = this.getIcon(event); const icon = this.getIcon(event);
const action = this.getAction(event, hasMultipleNewsletters); const action = this.getAction(event, hasMultipleNewsletters);
const info = this.getInfo(event); const info = this.getInfo(event);
@ -110,6 +110,10 @@ export default class ParseMemberEventHelper extends Helper {
} }
} }
if (event.type === 'donation_event') {
icon = 'subscriptions';
}
return 'event-' + icon; return 'event-' + icon;
} }
@ -203,6 +207,10 @@ export default class ParseMemberEventHelper extends Helper {
} }
return 'less like this'; return 'less like this';
} }
if (event.type === 'donation_event') {
return `Made a one-time payment`;
}
} }
/** /**
@ -222,7 +230,7 @@ export default class ParseMemberEventHelper extends Helper {
* Clickable object, shown between action and info, or in a separate column in some views * Clickable object, shown between action and info, or in a separate column in some views
*/ */
getObject(event) { getObject(event) {
if (event.type === 'signup_event' || event.type === 'subscription_event') { if (event.type === 'signup_event' || event.type === 'subscription_event' || event.type === 'donation_event') {
if (event.data.attribution?.title) { if (event.data.attribution?.title) {
return event.data.attribution.title; return event.data.attribution.title;
} }
@ -278,6 +286,12 @@ export default class ParseMemberEventHelper extends Helper {
return 'Free'; return 'Free';
} }
if (event.type === 'donation_event') {
const symbol = getSymbol(event.data.currency);
const formattedAmount = symbol + getNonDecimal(event.data.amount, event.data.currency);
return formattedAmount;
}
return; return;
} }
@ -304,7 +318,7 @@ export default class ParseMemberEventHelper extends Helper {
} }
} }
if (['signup_event', 'subscription_event'].includes(event.type)) { if (['signup_event', 'subscription_event', 'donation_event'].includes(event.type)) {
if (event.data.attribution && event.data.attribution.url) { if (event.data.attribution && event.data.attribution.url) {
return event.data.attribution.url; return event.data.attribution.url;
} }

View File

@ -182,6 +182,7 @@ function createApiInstance(config) {
} }
}, },
models: { models: {
DonationPaymentEvent: models.DonationPaymentEvent,
EmailRecipient: models.EmailRecipient, EmailRecipient: models.EmailRecipient,
StripeCustomer: models.MemberStripeCustomer, StripeCustomer: models.MemberStripeCustomer,
StripeCustomerSubscription: models.StripeCustomerSubscription, StripeCustomerSubscription: models.StripeCustomerSubscription,

View File

@ -37,6 +37,7 @@ module.exports = function MembersAPI({
getSubject getSubject
}, },
models: { models: {
DonationPaymentEvent,
EmailRecipient, EmailRecipient,
StripeCustomer, StripeCustomer,
StripeCustomerSubscription, StripeCustomerSubscription,
@ -107,6 +108,7 @@ module.exports = function MembersAPI({
}); });
const eventRepository = new EventRepository({ const eventRepository = new EventRepository({
DonationPaymentEvent,
EmailRecipient, EmailRecipient,
MemberSubscribeEvent, MemberSubscribeEvent,
MemberPaidSubscriptionEvent, MemberPaidSubscriptionEvent,

View File

@ -17,6 +17,7 @@ function replaceCustomFilterTransformer(filter) {
module.exports = class EventRepository { module.exports = class EventRepository {
constructor({ constructor({
DonationPaymentEvent,
EmailRecipient, EmailRecipient,
MemberSubscribeEvent, MemberSubscribeEvent,
MemberPaymentEvent, MemberPaymentEvent,
@ -32,6 +33,7 @@ module.exports = class EventRepository {
labsService, labsService,
memberAttributionService memberAttributionService
}) { }) {
this._DonationPaymentEvent = DonationPaymentEvent;
this._MemberSubscribeEvent = MemberSubscribeEvent; this._MemberSubscribeEvent = MemberSubscribeEvent;
this._MemberPaidSubscriptionEvent = MemberPaidSubscriptionEvent; this._MemberPaidSubscriptionEvent = MemberPaidSubscriptionEvent;
this._MemberPaymentEvent = MemberPaymentEvent; this._MemberPaymentEvent = MemberPaymentEvent;
@ -65,7 +67,8 @@ module.exports = class EventRepository {
{type: 'click_event', action: 'getClickEvents'}, {type: 'click_event', action: 'getClickEvents'},
{type: 'aggregated_click_event', action: 'getAggregatedClickEvents'}, {type: 'aggregated_click_event', action: 'getAggregatedClickEvents'},
{type: 'signup_event', action: 'getSignupEvents'}, {type: 'signup_event', action: 'getSignupEvents'},
{type: 'subscription_event', action: 'getSubscriptionEvents'} {type: 'subscription_event', action: 'getSubscriptionEvents'},
{type: 'donation_event', action: 'getDonationEvents'}
]; ];
// Some events are not filterable by post_id // Some events are not filterable by post_id
@ -352,6 +355,59 @@ module.exports = class EventRepository {
}; };
} }
async getDonationEvents(options = {}, filter) {
options = {
...options,
withRelated: [
'member',
'postAttribution',
'userAttribution',
'tagAttribution'
],
filter: 'member_id:-null+custom:true',
mongoTransformer: chainTransformers(
// First set the filter manually
replaceCustomFilterTransformer(filter),
// Map the used keys in that filter
...mapKeys({
'data.created_at': 'created_at',
'data.member_id': 'member_id'
}),
(f) => {
// Special one: when data.post_id is used, replace it with two filters: attribution_id:x+attribution_type:post
return expandFilters(f, [{
key: 'data.post_id',
replacement: 'attribution_id',
expansion: {attribution_type: 'post'}
}]);
}
)
};
const {data: models, meta} = await this._DonationPaymentEvent.findPage(options);
const data = models.map((model) => {
const json = model.toJSON(options);
delete json.postAttribution?.mobiledoc;
delete json.postAttribution?.lexical;
delete json.postAttribution?.plaintext;
return {
type: 'donation_event',
data: {
...json,
attribution: this._memberAttributionService.getEventAttribution(model)
}
};
});
return {
data,
meta
};
}
async getCommentEvents(options = {}, filter) { async getCommentEvents(options = {}, filter) {
options = { options = {
...options, ...options,