Added linking between member and subscription created events (#15693)
fixes https://github.com/TryGhost/Team/issues/2160 - Adds a `batch_id` to both events that contain the same ID if they were created at the same time. - Removes duplicate signup/conversion events using the batch_id - Requires an update in mongo-knex to work (refs https://ghost.slack.com/archives/C02G9E68C/p1666773313272409?thread_ts=1666767872.375009&cid=C02G9E68C) - Some dependencies needed an update to load the latest mongo-knex - Added tiers to membersUtils, loaded on startup (we can start to use this instead of fetching it every time)
This commit is contained in:
parent
30327d62cd
commit
076e3c02b2
@ -23,6 +23,7 @@ export default class ModalTierPrice extends ModalBase {
|
||||
@service feature;
|
||||
@service settings;
|
||||
@service config;
|
||||
@service membersUtils;
|
||||
@tracked model;
|
||||
@tracked tier;
|
||||
@tracked periodVal;
|
||||
@ -185,6 +186,9 @@ export default class ModalTierPrice extends ModalBase {
|
||||
this.hasSaved = true;
|
||||
yield this.confirm();
|
||||
this.send('closeModal');
|
||||
|
||||
// Reload in the background (no await here)
|
||||
this.membersUtils.reload();
|
||||
} catch (error) {
|
||||
if (error === undefined) {
|
||||
// Validation error
|
||||
|
@ -53,9 +53,14 @@
|
||||
<span class="gh-dashboard-list-subtext">
|
||||
<span class="gh-members-activity-description">
|
||||
<span class="gh-members-activity-event-text">{{capitalize-first-letter parsedEvent.action}}</span>
|
||||
{{#if parsedEvent.description}}
|
||||
<span class="gh-members-activity-event-dash">–</span>
|
||||
<a class="ghost-members-activity-object-link" href="{{parsedEvent.url}}" target="_blank" rel="noopener noreferrer">{{parsedEvent.description}}</a>
|
||||
{{#if parsedEvent.info}}
|
||||
<span class="highlight">{{parsedEvent.info}}</span>
|
||||
{{/if}}
|
||||
{{#if (eq this.eventType "clicked")}}
|
||||
{{#if (and parsedEvent.description parsedEvent.url) }}
|
||||
<span class="gh-members-activity-event-dash">–</span>
|
||||
<a class="ghost-members-activity-object-link" href="{{parsedEvent.url}}" target="_blank" rel="noopener noreferrer">{{parsedEvent.description}}</a>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</span>
|
||||
</span>
|
||||
|
@ -277,6 +277,9 @@ export default class StripeSettingsForm extends Component {
|
||||
|
||||
try {
|
||||
const updatedTier = yield tier.save();
|
||||
|
||||
// Reload in the background (no await here)
|
||||
this.membersUtils.reload();
|
||||
return updatedTier;
|
||||
} catch (error) {
|
||||
if (error.payload?.errors && error.payload.errors[0].code === 'STRIPE_NOT_CONFIGURED') {
|
||||
|
@ -6,6 +6,7 @@ import {inject as service} from '@ember/service';
|
||||
export default class ParseMemberEventHelper extends Helper {
|
||||
@service feature;
|
||||
@service utils;
|
||||
@service membersUtils;
|
||||
|
||||
compute([event, hasMultipleNewsletters]) {
|
||||
const subject = event.data.member.name || event.data.member.email;
|
||||
@ -42,10 +43,6 @@ export default class ParseMemberEventHelper extends Helper {
|
||||
getIcon(event) {
|
||||
let icon;
|
||||
|
||||
if (event.type === 'signup_event') {
|
||||
icon = 'signed-up';
|
||||
}
|
||||
|
||||
if (event.type === 'login_event') {
|
||||
icon = 'logged-in';
|
||||
}
|
||||
@ -70,6 +67,10 @@ export default class ParseMemberEventHelper extends Helper {
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === 'signup_event' || (event.type === 'subscription_event' && event.data.type === 'created' && event.data.signup)) {
|
||||
icon = 'signed-up';
|
||||
}
|
||||
|
||||
if (event.type === 'email_opened_event') {
|
||||
icon = 'opened-email';
|
||||
}
|
||||
@ -102,7 +103,7 @@ export default class ParseMemberEventHelper extends Helper {
|
||||
}
|
||||
|
||||
getAction(event, hasMultipleNewsletters) {
|
||||
if (event.type === 'signup_event') {
|
||||
if (event.type === 'signup_event' || (event.type === 'subscription_event' && event.data.type === 'created' && event.data.signup)) {
|
||||
return 'signed up';
|
||||
}
|
||||
|
||||
@ -248,10 +249,21 @@ export default class ParseMemberEventHelper extends Helper {
|
||||
if (mrrDelta === 0) {
|
||||
return;
|
||||
}
|
||||
let sign = mrrDelta > 0 ? '+' : '-';
|
||||
let symbol = getSymbol(event.data.currency);
|
||||
return `(MRR ${sign}${symbol}${Math.abs(mrrDelta)})`;
|
||||
const symbol = getSymbol(event.data.currency);
|
||||
|
||||
if (event.data.type === 'created') {
|
||||
const sign = mrrDelta > 0 ? '' : '-';
|
||||
const tierName = this.membersUtils.hasMultipleTiers ? (event.data.tierName ?? 'MRR') : 'paid';
|
||||
return `(${tierName} - ${sign}${symbol}${Math.abs(mrrDelta)}/month)`;
|
||||
}
|
||||
const sign = mrrDelta > 0 ? '+' : '-';
|
||||
return `(MRR - ${sign}${symbol}${Math.abs(mrrDelta)})`;
|
||||
}
|
||||
|
||||
if (event.type === 'signup_event' && this.membersUtils.paidMembersEnabled) {
|
||||
return '(free)';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,8 @@ export default class MembersUtilsService extends Service {
|
||||
@service feature;
|
||||
@service store;
|
||||
|
||||
paidTiers = null;
|
||||
|
||||
get isMembersEnabled() {
|
||||
return this.settings.membersEnabled;
|
||||
}
|
||||
@ -18,6 +20,26 @@ export default class MembersUtilsService extends Service {
|
||||
return this.settings.membersInviteOnly;
|
||||
}
|
||||
|
||||
get hasMultipleTiers() {
|
||||
return this.paidMembersEnabled && this.paidTiers && this.paidTiers.length > 1;
|
||||
}
|
||||
|
||||
async fetch() {
|
||||
if (this.paidTiers !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.store.query('tier', {filter: 'type:paid+active:true', limit: 'all'}).then((tiers) => {
|
||||
this.paidTiers = tiers;
|
||||
});
|
||||
}
|
||||
|
||||
async reload() {
|
||||
return this.store.query('tier', {filter: 'type:paid+active:true', limit: 'all'}).then((tiers) => {
|
||||
this.paidTiers = tiers;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: always use paidMembersEnabled! Only use this getter for the Stripe Connection UI.
|
||||
*/
|
||||
|
@ -18,6 +18,7 @@ export default class SessionService extends ESASessionService {
|
||||
@service ui;
|
||||
@service upgradeStatus;
|
||||
@service whatsNew;
|
||||
@service membersUtils;
|
||||
|
||||
@tracked user = null;
|
||||
|
||||
@ -37,7 +38,8 @@ export default class SessionService extends ESASessionService {
|
||||
await RSVP.all([
|
||||
this.config.fetchAuthenticated(),
|
||||
this.feature.fetch(),
|
||||
this.settings.fetch()
|
||||
this.settings.fetch(),
|
||||
this.membersUtils.fetch()
|
||||
]);
|
||||
|
||||
await this.frontend.loginIfNeeded();
|
||||
|
@ -49,8 +49,8 @@
|
||||
"@tryghost/limit-service": "1.2.3",
|
||||
"@tryghost/members-csv": "0.0.0",
|
||||
"@tryghost/mobiledoc-kit": "0.12.5-ghost.2",
|
||||
"@tryghost/nql": "0.9.2",
|
||||
"@tryghost/nql-lang": "0.3.2",
|
||||
"@tryghost/nql": "0.11.0",
|
||||
"@tryghost/nql-lang": "0.5.0",
|
||||
"@tryghost/string": "0.2.1",
|
||||
"@tryghost/timezone-data": "0.2.73",
|
||||
"autoprefixer": "9.8.6",
|
||||
@ -181,4 +181,4 @@
|
||||
"path-browserify": "1.0.1",
|
||||
"webpack": "5.74.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,6 +117,13 @@ const activityFeedMapper = (event, frame) => {
|
||||
if (event.data?.attribution) {
|
||||
event.data.attribution = serializeAttribution(event.data.attribution);
|
||||
}
|
||||
// TODO: add dedicated mappers for other event types
|
||||
if (event.data?.batch_id) {
|
||||
delete event.data.batch_id;
|
||||
}
|
||||
if (event.data?.subscriptionCreatedEvent) {
|
||||
delete event.data.subscriptionCreatedEvent;
|
||||
}
|
||||
return event;
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,7 @@
|
||||
const {createAddColumnMigration} = require('../../utils');
|
||||
|
||||
module.exports = createAddColumnMigration('members_created_events', 'batch_id', {
|
||||
type: 'string',
|
||||
maxlength: 24,
|
||||
nullable: true
|
||||
});
|
@ -0,0 +1,7 @@
|
||||
const {createAddColumnMigration} = require('../../utils');
|
||||
|
||||
module.exports = createAddColumnMigration('members_subscription_created_events', 'batch_id', {
|
||||
type: 'string',
|
||||
maxlength: 24,
|
||||
nullable: true
|
||||
});
|
@ -0,0 +1,72 @@
|
||||
const _ = require('lodash');
|
||||
const logging = require('@tryghost/logging');
|
||||
const ObjectId = require('bson-objectid').default;
|
||||
const {createTransactionalMigration} = require('../../utils');
|
||||
const DatabaseInfo = require('@tryghost/database-info');
|
||||
|
||||
// This migration links together members_created_events and members_subscription_created_events
|
||||
|
||||
module.exports = createTransactionalMigration(
|
||||
async function up(knex) {
|
||||
if (DatabaseInfo.isSQLite(knex)) {
|
||||
logging.info('Skipped linking members_created_events and members_subscription_created_events on SQLite');
|
||||
return;
|
||||
}
|
||||
|
||||
// All events that happened within 15 minutes of each other will be linked
|
||||
const rows = await knex('members_created_events as m')
|
||||
.select('m.id as m_id', 's.id as s_id', 'm.member_id as member_id', 's.subscription_id as subscription_id')
|
||||
.join('members_subscription_created_events AS s', 's.member_id', 'm.member_id')
|
||||
.whereRaw('TIMESTAMPDIFF(MINUTE, s.created_at, m.created_at) between -15 and 15');
|
||||
|
||||
if (!rows.length) {
|
||||
logging.info('Did not find linkable members_created_events and members_subscription_created_events');
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach a unique id to each row
|
||||
for (const row of rows) { // eslint-disable-line no-restricted-syntax
|
||||
row.batch_id = ObjectId().toHexString();
|
||||
}
|
||||
|
||||
// Create batches (insertBatch doesn't support the onConflict option)
|
||||
const batches = _.chunk(rows, 1000);
|
||||
|
||||
for (const batch of batches) { // eslint-disable-line no-restricted-syntax
|
||||
// Update the members_created_events table using INSERT ON DUPLICATE KEY UPDATE trick
|
||||
const response1 = await knex('members_created_events').insert(batch.map((r) => {
|
||||
return {
|
||||
id: r.m_id,
|
||||
batch_id: r.batch_id,
|
||||
member_id: r.member_id, // added to make the insert work
|
||||
source: '', // added to make the insert work
|
||||
created_at: knex.raw('NOW()') // added to make the insert work
|
||||
};
|
||||
})).onConflict('id').merge(['batch_id']);
|
||||
|
||||
if (response1[0] !== 0) {
|
||||
logging.error(`Inserted ${response1[0]} members_created_events, expected 0`);
|
||||
throw new Error('Rolling back');
|
||||
}
|
||||
|
||||
const response2 = await knex('members_subscription_created_events').insert(batch.map((r) => {
|
||||
return {
|
||||
id: r.s_id,
|
||||
batch_id: r.batch_id,
|
||||
member_id: r.member_id, // added to make the insert work
|
||||
subscription_id: r.subscription_id, // added to make the insert work
|
||||
created_at: knex.raw('NOW()') // added to make the insert work
|
||||
};
|
||||
})).onConflict('id').merge(['batch_id']);
|
||||
|
||||
if (response2[0] !== 0) {
|
||||
logging.error(`Inserted ${response1[0]} members_subscription_created_events, expected 0`);
|
||||
throw new Error('Rolling back');
|
||||
}
|
||||
}
|
||||
logging.info(`Linked ${rows.length} members_created_events and members_subscription_created_events`);
|
||||
},
|
||||
async function down() {
|
||||
// noop
|
||||
}
|
||||
);
|
@ -517,7 +517,8 @@ module.exports = {
|
||||
type: 'string', maxlength: 50, nullable: false, validations: {
|
||||
isIn: [['member', 'import', 'system', 'api', 'admin']]
|
||||
}
|
||||
}
|
||||
},
|
||||
batch_id: {type: 'string', maxlength: 24, nullable: true}
|
||||
},
|
||||
members_cancel_events: {
|
||||
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
||||
@ -697,7 +698,8 @@ module.exports = {
|
||||
attribution_url: {type: 'string', maxlength: 2000, nullable: true},
|
||||
referrer_source: {type: 'string', maxlength: 191, nullable: true},
|
||||
referrer_medium: {type: 'string', maxlength: 191, nullable: true},
|
||||
referrer_url: {type: 'string', maxlength: 2000, nullable: true}
|
||||
referrer_url: {type: 'string', maxlength: 2000, nullable: true},
|
||||
batch_id: {type: 'string', maxlength: 24, nullable: true}
|
||||
},
|
||||
offer_redemptions: {
|
||||
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
||||
|
@ -8,6 +8,13 @@ const MemberCreatedEvent = ghostBookshelf.Model.extend({
|
||||
return this.belongsTo('Member', 'member_id', 'id');
|
||||
},
|
||||
|
||||
/**
|
||||
* The subscription created event that happend at the same time (if any)
|
||||
*/
|
||||
subscriptionCreatedEvent() {
|
||||
return this.belongsTo('SubscriptionCreatedEvent', 'batch_id', 'batch_id');
|
||||
},
|
||||
|
||||
postAttribution() {
|
||||
return this.belongsTo('Post', 'attribution_id', 'id');
|
||||
},
|
||||
@ -18,6 +25,22 @@ const MemberCreatedEvent = ghostBookshelf.Model.extend({
|
||||
|
||||
tagAttribution() {
|
||||
return this.belongsTo('Tag', 'attribution_id', 'id');
|
||||
},
|
||||
|
||||
filterRelations() {
|
||||
return {
|
||||
subscriptionCreatedEvent: {
|
||||
// Mongo-knex doesn't support belongsTo relations
|
||||
tableName: 'members_subscription_created_events',
|
||||
tableNameAs: 'subscriptionCreatedEvent',
|
||||
type: 'manyToMany',
|
||||
joinTable: 'members_created_events',
|
||||
joinFrom: 'id',
|
||||
joinToForeign: 'batch_id',
|
||||
joinTo: 'batch_id',
|
||||
joinType: 'leftJoin'
|
||||
}
|
||||
};
|
||||
}
|
||||
}, {
|
||||
async edit() {
|
||||
|
@ -8,6 +8,10 @@ const MemberPaidSubscriptionEvent = ghostBookshelf.Model.extend({
|
||||
return this.belongsTo('Member', 'member_id', 'id');
|
||||
},
|
||||
|
||||
stripeSubscription() {
|
||||
return this.belongsTo('StripeCustomerSubscription', 'subscription_id', 'id');
|
||||
},
|
||||
|
||||
subscriptionCreatedEvent() {
|
||||
return this.belongsTo('SubscriptionCreatedEvent', 'subscription_id', 'subscription_id');
|
||||
},
|
||||
|
@ -8,6 +8,13 @@ const SubscriptionCreatedEvent = ghostBookshelf.Model.extend({
|
||||
return this.belongsTo('Member', 'member_id', 'id');
|
||||
},
|
||||
|
||||
/**
|
||||
* The member created event that happend at the same time (if any)
|
||||
*/
|
||||
memberCreatedEvent() {
|
||||
return this.belongsTo('MemberCreatedEvent', 'batch_id', 'batch_id');
|
||||
},
|
||||
|
||||
subscription() {
|
||||
return this.belongsTo('StripeCustomerSubscription', 'subscription_id', 'id');
|
||||
},
|
||||
|
@ -59,7 +59,7 @@
|
||||
"@tryghost/api-framework": "0.0.0",
|
||||
"@tryghost/api-version-compatibility-service": "0.0.0",
|
||||
"@tryghost/audience-feedback": "0.0.0",
|
||||
"@tryghost/bookshelf-plugins": "0.5.4",
|
||||
"@tryghost/bookshelf-plugins": "0.6.0",
|
||||
"@tryghost/bootstrap-socket": "0.0.0",
|
||||
"@tryghost/color-utils": "0.1.21",
|
||||
"@tryghost/config-url-helpers": "1.0.3",
|
||||
@ -107,7 +107,7 @@
|
||||
"@tryghost/mw-session-from-token": "0.0.0",
|
||||
"@tryghost/mw-vhost": "0.0.0",
|
||||
"@tryghost/nodemailer": "0.3.29",
|
||||
"@tryghost/nql": "0.9.2",
|
||||
"@tryghost/nql": "0.11.0",
|
||||
"@tryghost/oembed-service": "0.0.0",
|
||||
"@tryghost/package-json": "0.0.0",
|
||||
"@tryghost/pretty-cli": "1.2.31",
|
||||
|
@ -81,7 +81,7 @@ exports[`Activity Feed API Can filter events by post id 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": "23206",
|
||||
"content-length": "20770",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
@ -741,7 +741,7 @@ exports[`Activity Feed API Returns signup events in activity feed 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": "23027",
|
||||
"content-length": "23155",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
|
@ -401,7 +401,7 @@ exports[`Members API Member attribution Returns subscription created attribution
|
||||
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": "14813",
|
||||
"content-length": "7962",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
|
@ -35,7 +35,7 @@ const validateRouteSettings = require('../../../../../core/server/services/route
|
||||
*/
|
||||
describe('DB version integrity', function () {
|
||||
// Only these variables should need updating
|
||||
const currentSchemaHash = 'f94be1265fce0bfbe5c98cb02610f9de';
|
||||
const currentSchemaHash = 'bb48c3c754f74f02a6ce020af6b0e6a7';
|
||||
const currentFixturesHash = 'dcb7ba7c66b4b98d6c26a722985e756a';
|
||||
const currentSettingsHash = '2978a5684a2d5fcf089f61f5d368a0c0';
|
||||
const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';
|
||||
|
@ -16,7 +16,7 @@
|
||||
"lib"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@tryghost/nql-lang": "0.3.2",
|
||||
"@tryghost/nql-lang": "0.5.0",
|
||||
"c8": "7.12.0",
|
||||
"mocha": "10.1.0",
|
||||
"should": "13.2.3",
|
||||
|
@ -28,7 +28,7 @@
|
||||
"bson-objectid": "2.0.3",
|
||||
"@tryghost/errors": "1.2.18",
|
||||
"@tryghost/tpl": "0.1.19",
|
||||
"@tryghost/nql": "0.9.2",
|
||||
"@tryghost/nql": "0.11.0",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4"
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
/**
|
||||
* @typedef {object} MemberCreatedEventData
|
||||
* @prop {string} memberId
|
||||
* @prop {string} batchId
|
||||
* @prop {'import' | 'system' | 'api' | 'admin' | 'member'} source
|
||||
* @prop {import('@tryghost/member-attribution/lib/attribution').Attribution} [attribution] Attribution
|
||||
*/
|
||||
|
@ -2,6 +2,7 @@
|
||||
* @typedef {object} SubscriptionCreatedEventData
|
||||
* @prop {string} source
|
||||
* @prop {string} memberId
|
||||
* @prop {string} batchId
|
||||
* @prop {string} tierId
|
||||
* @prop {string} subscriptionId
|
||||
* @prop {string} offerId
|
||||
|
@ -173,7 +173,16 @@ module.exports = class EventRepository {
|
||||
|
||||
options = {
|
||||
...options,
|
||||
withRelated: ['member', 'subscriptionCreatedEvent.postAttribution', 'subscriptionCreatedEvent.userAttribution', 'subscriptionCreatedEvent.tagAttribution'],
|
||||
withRelated: [
|
||||
'member',
|
||||
'subscriptionCreatedEvent.postAttribution',
|
||||
'subscriptionCreatedEvent.userAttribution',
|
||||
'subscriptionCreatedEvent.tagAttribution',
|
||||
'subscriptionCreatedEvent.memberCreatedEvent',
|
||||
|
||||
// This is rediculous, but we need the tier name (we'll be able to shorten this later when we switch to the subscriptions table)
|
||||
'stripeSubscription.stripePrice.stripeProduct.product'
|
||||
],
|
||||
filter: []
|
||||
};
|
||||
if (filters['data.created_at']) {
|
||||
@ -191,12 +200,16 @@ module.exports = class EventRepository {
|
||||
const {data: models, meta} = await this._MemberPaidSubscriptionEvent.findPage(options);
|
||||
|
||||
const data = models.map((model) => {
|
||||
const d = {
|
||||
...model.toJSON(options),
|
||||
attribution: model.get('type') === 'created' && model.related('subscriptionCreatedEvent') && model.related('subscriptionCreatedEvent').id ? this._memberAttributionService.getEventAttribution(model.related('subscriptionCreatedEvent')) : null,
|
||||
signup: model.get('type') === 'created' && model.related('subscriptionCreatedEvent') && model.related('subscriptionCreatedEvent').id && model.related('subscriptionCreatedEvent').related('memberCreatedEvent') && model.related('subscriptionCreatedEvent').related('memberCreatedEvent').id ? true : false,
|
||||
tierName: model.related('stripeSubscription') && model.related('stripeSubscription').related('stripePrice') && model.related('stripeSubscription').related('stripePrice').related('stripeProduct') && model.related('stripeSubscription').related('stripePrice').related('stripeProduct').related('product') ? model.related('stripeSubscription').related('stripePrice').related('stripeProduct').related('product').get('name') : null
|
||||
};
|
||||
delete d.stripeSubscription;
|
||||
return {
|
||||
type: 'subscription_event',
|
||||
data: {
|
||||
...model.toJSON(options),
|
||||
attribution: model.get('type') === 'created' && model.related('subscriptionCreatedEvent') && model.related('subscriptionCreatedEvent').id ? this._memberAttributionService.getEventAttribution(model.related('subscriptionCreatedEvent')) : null
|
||||
}
|
||||
data: d
|
||||
};
|
||||
});
|
||||
|
||||
@ -300,8 +313,13 @@ module.exports = class EventRepository {
|
||||
async getCreatedEvents(options = {}, filters = {}) {
|
||||
options = {
|
||||
...options,
|
||||
withRelated: ['member', 'postAttribution', 'userAttribution', 'tagAttribution'],
|
||||
filter: []
|
||||
withRelated: [
|
||||
'member',
|
||||
'postAttribution',
|
||||
'userAttribution',
|
||||
'tagAttribution'
|
||||
],
|
||||
filter: ['subscriptionCreatedEvent.id:null']
|
||||
};
|
||||
if (filters['data.created_at']) {
|
||||
options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'created_at:'));
|
||||
|
@ -227,6 +227,11 @@ module.exports = class MemberRepository {
|
||||
options = {};
|
||||
}
|
||||
|
||||
if (!options.batch_id) {
|
||||
// We'll use this to link related events
|
||||
options.batch_id = ObjectId().toHexString();
|
||||
}
|
||||
|
||||
const {labels, stripeCustomer, offerId, attribution} = data;
|
||||
|
||||
if (labels) {
|
||||
@ -339,7 +344,7 @@ module.exports = class MemberRepository {
|
||||
subscription,
|
||||
offerId,
|
||||
attribution
|
||||
});
|
||||
}, {batch_id: options.batch_id});
|
||||
} catch (err) {
|
||||
if (err.code !== 'ER_DUP_ENTRY' && err.code !== 'SQLITE_CONSTRAINT') {
|
||||
throw err;
|
||||
@ -352,6 +357,7 @@ module.exports = class MemberRepository {
|
||||
}
|
||||
this.dispatchEvent(MemberCreatedEvent.create({
|
||||
memberId: member.id,
|
||||
batchId: options.batch_id,
|
||||
attribution: data.attribution,
|
||||
source
|
||||
}, eventData.created_at), options);
|
||||
@ -807,6 +813,11 @@ module.exports = class MemberRepository {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!options.batch_id) {
|
||||
options.batch_id = ObjectId().toHexString();
|
||||
}
|
||||
|
||||
const member = await this._Member.findOne({
|
||||
id: data.id
|
||||
}, {...options, forUpdate: true});
|
||||
@ -1010,7 +1021,8 @@ module.exports = class MemberRepository {
|
||||
memberId: member.id,
|
||||
subscriptionId: subscriptionModel.get('id'),
|
||||
offerId: data.offerId,
|
||||
attribution: data.attribution
|
||||
attribution: data.attribution,
|
||||
batchId: options.batch_id
|
||||
});
|
||||
this.dispatchEvent(event, options);
|
||||
}
|
||||
|
@ -36,7 +36,7 @@
|
||||
"@tryghost/member-events": "0.0.0",
|
||||
"@tryghost/members-analytics-ingress": "0.0.0",
|
||||
"@tryghost/members-payments": "0.0.0",
|
||||
"@tryghost/nql": "0.9.2",
|
||||
"@tryghost/nql": "0.11.0",
|
||||
"@tryghost/tpl": "0.1.19",
|
||||
"@types/jsonwebtoken": "8.5.9",
|
||||
"body-parser": "1.20.1",
|
||||
|
@ -34,7 +34,8 @@ class EventStorage {
|
||||
source: event.data.source,
|
||||
referrer_source: attribution?.referrerSource ?? null,
|
||||
referrer_medium: attribution?.referrerMedium ?? null,
|
||||
referrer_url: attribution?.referrerUrl ?? null
|
||||
referrer_url: attribution?.referrerUrl ?? null,
|
||||
batch_id: event.data.batchId ?? null
|
||||
});
|
||||
});
|
||||
|
||||
@ -50,7 +51,8 @@ class EventStorage {
|
||||
attribution_type: attribution?.type ?? null,
|
||||
referrer_source: attribution?.referrerSource ?? null,
|
||||
referrer_medium: attribution?.referrerMedium ?? null,
|
||||
referrer_url: attribution?.referrerUrl ?? null
|
||||
referrer_url: attribution?.referrerUrl ?? null,
|
||||
batch_id: event.data.batchId ?? null
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -25,7 +25,7 @@
|
||||
"dependencies": {
|
||||
"@tryghost/domain-events": "0.0.0",
|
||||
"@tryghost/errors": "1.2.18",
|
||||
"@tryghost/mongo-utils": "0.3.5",
|
||||
"@tryghost/mongo-utils": "0.5.0",
|
||||
"@tryghost/string": "0.2.1",
|
||||
"lodash": "4.17.21"
|
||||
}
|
||||
|
58
yarn.lock
58
yarn.lock
@ -4181,14 +4181,14 @@
|
||||
"@tryghost/debug" "^0.1.19"
|
||||
lodash "^4.17.21"
|
||||
|
||||
"@tryghost/bookshelf-filter@^0.4.15":
|
||||
version "0.4.15"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-filter/-/bookshelf-filter-0.4.15.tgz#1291e1754d4bc704457f791972450d303cc9855d"
|
||||
integrity sha512-rpB5yR2XR3QDgbRtYzXrKlYuyHQvT5J6bZ0YSjsMieRKKNP/WigyhH/M6zKCSvtU4jHYEcv7SqldFtmf+htCyQ==
|
||||
"@tryghost/bookshelf-filter@^0.5.0":
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-filter/-/bookshelf-filter-0.5.0.tgz#ed9535eebec0e8362fdcc4653c54d6a3f8bc127d"
|
||||
integrity sha512-OiKIuzMXFFbUo6pra+HV02wSilBC0P6ymDcx/QAN0z7rboZwEEZJsF6cULihs5YQmaKHWPNyMi3kOM3jyQLJcw==
|
||||
dependencies:
|
||||
"@tryghost/debug" "^0.1.19"
|
||||
"@tryghost/errors" "^1.2.18"
|
||||
"@tryghost/nql" "^0.9.0"
|
||||
"@tryghost/nql" "^0.11.0"
|
||||
"@tryghost/tpl" "^0.1.19"
|
||||
|
||||
"@tryghost/bookshelf-has-posts@^0.1.19":
|
||||
@ -4223,15 +4223,15 @@
|
||||
"@tryghost/tpl" "^0.1.19"
|
||||
lodash "^4.17.21"
|
||||
|
||||
"@tryghost/bookshelf-plugins@0.5.4":
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-plugins/-/bookshelf-plugins-0.5.4.tgz#f75c7099f4219eb73778aa8c6b8486d979edd3c4"
|
||||
integrity sha512-p8Gi6E4JSg+wqtaats13BmTe6Z+CjQ/2OV3u0E+BDGvOX/Z7sifUZfBQpaQAj6G9z5WDj9DesmGXTzDBnq3BsA==
|
||||
"@tryghost/bookshelf-plugins@0.6.0":
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-plugins/-/bookshelf-plugins-0.6.0.tgz#6f037714cd381e90192bd8436a020f3299fce1e5"
|
||||
integrity sha512-WK0+Ap/cSImDbka2sksNcTZ3EGgB1g7YVGhSn6wblBu8p8JEEa7rIj6XiyOWKTaINoVmdX4mbUCMiZrGoHqW6g==
|
||||
dependencies:
|
||||
"@tryghost/bookshelf-collision" "^0.1.28"
|
||||
"@tryghost/bookshelf-custom-query" "^0.1.15"
|
||||
"@tryghost/bookshelf-eager-load" "^0.1.18"
|
||||
"@tryghost/bookshelf-filter" "^0.4.15"
|
||||
"@tryghost/bookshelf-filter" "^0.5.0"
|
||||
"@tryghost/bookshelf-has-posts" "^0.1.19"
|
||||
"@tryghost/bookshelf-include-count" "^0.3.1"
|
||||
"@tryghost/bookshelf-order" "^0.1.15"
|
||||
@ -4545,18 +4545,18 @@
|
||||
mobiledoc-dom-renderer "0.7.0"
|
||||
mobiledoc-text-renderer "0.4.0"
|
||||
|
||||
"@tryghost/mongo-knex@^0.6.4":
|
||||
version "0.6.4"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/mongo-knex/-/mongo-knex-0.6.4.tgz#760d91d794cf3bf65336a00f6329fe26849f863b"
|
||||
integrity sha512-249oNobgZvf2e3SI2x8F9AuaPsydgt003HbtKaMtLEGfobU9RsNlmaZNDJGXyaecukO4swymtt3aeHFTjWekyg==
|
||||
"@tryghost/mongo-knex@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/mongo-knex/-/mongo-knex-0.8.0.tgz#089a4948bf915a108baf62ead9f2d25d945a756c"
|
||||
integrity sha512-WTsLW7Q6L/mmX9jB7DDHDIR3k7SADjdXTP0dsJAiEwevMFByEQZmGba5bc1FhkTrbNWaVgk0wEnzE/r4SwhXvw==
|
||||
dependencies:
|
||||
debug "^4.3.3"
|
||||
lodash "^4.17.21"
|
||||
|
||||
"@tryghost/mongo-utils@0.3.5", "@tryghost/mongo-utils@^0.3.5":
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/mongo-utils/-/mongo-utils-0.3.5.tgz#85167cbdefaaa4924261a3a9cd6cbe94a8e5875d"
|
||||
integrity sha512-ycxWoC5D1t3Us5qFK8jN7cCKPfxEwmqhbK2T1hcb7fMXOv2WBZNfbQ3MAU3uICeT8QfyM6dWHPM18QLxycIhrw==
|
||||
"@tryghost/mongo-utils@0.5.0", "@tryghost/mongo-utils@^0.5.0":
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/mongo-utils/-/mongo-utils-0.5.0.tgz#4d697b3374ec8dd3ebe206a269907aff661fa3fb"
|
||||
integrity sha512-7eRb+pQdTe+NK+wQ44WUNQxC8DIPdCYg8jTp8Mk0G6BqlvaYdRtbrZU/uqeR7Z6efLiG4uPfLG7BNkNx5MjvMg==
|
||||
dependencies:
|
||||
lodash "^4.17.11"
|
||||
|
||||
@ -4572,21 +4572,21 @@
|
||||
nodemailer-mailgun-transport "^2.1.5"
|
||||
nodemailer-stub-transport "^1.1.0"
|
||||
|
||||
"@tryghost/nql-lang@0.3.2", "@tryghost/nql-lang@^0.3.2":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/nql-lang/-/nql-lang-0.3.2.tgz#618bff56963d58b211873a4d3f86d9ff04ceb26a"
|
||||
integrity sha512-KlO+x32nTi7q7HAinF1e2XyH0zeDBQOPL6TCDjV+bjDGaJ4h0GzR406/79MosoYbPE3kiMGVIrIdMeQcysGoyg==
|
||||
"@tryghost/nql-lang@0.5.0", "@tryghost/nql-lang@^0.5.0":
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/nql-lang/-/nql-lang-0.5.0.tgz#e34082997eca361a71b9d2de6ea6f7dd5a0654b6"
|
||||
integrity sha512-Nxmz82Zm0uRXy0GL+Bd7G2mvjakoYbT6QDiyj6W1v90zXyzMedRfb9289lMzYX8tUG4VkoVOj9rWzFm5iSdpbw==
|
||||
dependencies:
|
||||
date-fns "^2.28.0"
|
||||
|
||||
"@tryghost/nql@0.9.2", "@tryghost/nql@^0.9.0":
|
||||
version "0.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/nql/-/nql-0.9.2.tgz#76e645ecf5927b84f4c1ef60953811e8324d404d"
|
||||
integrity sha512-XFpZ/1bBGpYtoDA1bTgtjxmq//aimhngeYnKu3anKQ9Ri/UxzHP7ML9NnRhvIPAfrGKaSSb+epv6Zi41LA/d9Q==
|
||||
"@tryghost/nql@0.11.0", "@tryghost/nql@^0.11.0":
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/nql/-/nql-0.11.0.tgz#137a05aa3e3c733c8f646afc59dcb9eeb2c6269a"
|
||||
integrity sha512-B8AZUC97NVp6atZ2Jg6mL+03AbQf5gzCkqhCgVjhHeeyQh5xvU1uIMyvuio2wq9MFWxHPD8w8cSltidpY9wTuA==
|
||||
dependencies:
|
||||
"@tryghost/mongo-knex" "^0.6.4"
|
||||
"@tryghost/mongo-utils" "^0.3.5"
|
||||
"@tryghost/nql-lang" "^0.3.2"
|
||||
"@tryghost/mongo-knex" "^0.8.0"
|
||||
"@tryghost/mongo-utils" "^0.5.0"
|
||||
"@tryghost/nql-lang" "^0.5.0"
|
||||
mingo "^2.2.2"
|
||||
|
||||
"@tryghost/pretty-cli@1.2.29":
|
||||
|
Loading…
Reference in New Issue
Block a user