mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-24 19:33:02 +03:00
Email verification for imports based on 30 days of import
refs: https://github.com/TryGhost/Toolbox/issues/293 Things needed to create this: * MemberSubscriptionEvent now has an import source * Importer now creates events with this type * Verification trigger logic changed to use 30 day window of imports
This commit is contained in:
parent
b76d850620
commit
455778662c
@ -220,7 +220,9 @@ module.exports = class MemberRepository {
|
|||||||
const context = options && options.context || {};
|
const context = options && options.context || {};
|
||||||
let source;
|
let source;
|
||||||
|
|
||||||
if (context.internal) {
|
if (context.import) {
|
||||||
|
source = 'import';
|
||||||
|
} else if (context.internal) {
|
||||||
source = 'system';
|
source = 'system';
|
||||||
} else if (context.user) {
|
} else if (context.user) {
|
||||||
source = 'admin';
|
source = 'admin';
|
||||||
|
@ -153,7 +153,11 @@ module.exports = class MembersCSVImporter {
|
|||||||
id: existingMember.id
|
id: existingMember.id
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
member = await membersApi.members.create(row, options);
|
member = await membersApi.members.create(row, Object.assign({}, options, {
|
||||||
|
context: {
|
||||||
|
import: true
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.stripe_customer_id) {
|
if (row.stripe_customer_id) {
|
||||||
|
@ -32,6 +32,7 @@ describe('Importer', function () {
|
|||||||
id: 'default_product_id'
|
id: 'default_product_id'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const memberCreateStub = sinon.stub().resolves(null);
|
||||||
const membersApi = {
|
const membersApi = {
|
||||||
productRepository: {
|
productRepository: {
|
||||||
list: async () => {
|
list: async () => {
|
||||||
@ -44,9 +45,7 @@ describe('Importer', function () {
|
|||||||
get: async () => {
|
get: async () => {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
create: async (row) => {
|
create: memberCreateStub
|
||||||
return row;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,6 +95,10 @@ describe('Importer', function () {
|
|||||||
result.meta.originalImportSize.should.equal(2);
|
result.meta.originalImportSize.should.equal(2);
|
||||||
|
|
||||||
fsWriteSpy.calledOnce.should.be.true();
|
fsWriteSpy.calledOnce.should.be.true();
|
||||||
|
|
||||||
|
// Called at least once
|
||||||
|
memberCreateStub.notCalled.should.be.false();
|
||||||
|
memberCreateStub.firstCall.lastArg.context.import.should.be.true();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ const {MemberSubscribeEvent} = require('@tryghost/member-events');
|
|||||||
const messages = {
|
const messages = {
|
||||||
emailVerificationNeeded: `We're hard at work processing your import. To make sure you get great deliverability on a list of that size, we'll need to enable some extra features for your account. A member of our team will be in touch with you by email to review your account make sure everything is configured correctly so you're ready to go.`,
|
emailVerificationNeeded: `We're hard at work processing your import. To make sure you get great deliverability on a list of that size, we'll need to enable some extra features for your account. A member of our team will be in touch with you by email to review your account make sure everything is configured correctly so you're ready to go.`,
|
||||||
emailVerificationEmailSubject: `Email needs verification`,
|
emailVerificationEmailSubject: `Email needs verification`,
|
||||||
emailVerificationEmailMessageImport: `Email verification needed for site: {siteUrl}, just imported: {importedNumber} members.`,
|
emailVerificationEmailMessageImport: `Email verification needed for site: {siteUrl}, has imported: {importedNumber} members in the last 30 days.`,
|
||||||
emailVerificationEmailMessageAPI: `Email verification needed for site: {siteUrl} has added: {importedNumber} members through the API in the last 30 days.`
|
emailVerificationEmailMessageAPI: `Email verification needed for site: {siteUrl} has added: {importedNumber} members through the API in the last 30 days.`
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,6 +68,33 @@ class VerificationTrigger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testImportThreshold() {
|
||||||
|
const createdAt = new Date();
|
||||||
|
createdAt.setDate(createdAt.getDate() - 30);
|
||||||
|
const events = await this._eventRepository.getNewsletterSubscriptionEvents({}, {
|
||||||
|
'data.source': `data.source:'import'`,
|
||||||
|
'data.created_at': `data.created_at:>'${createdAt.toISOString().replace('T', ' ').substring(0, 19)}'`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isFinite(this._configThreshold)) {
|
||||||
|
// Inifinte threshold, quick path
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const membersTotal = await this._membersStats.getTotalMembers();
|
||||||
|
|
||||||
|
// Import threshold is either the total number of members (discounting any created by imports in
|
||||||
|
// the last 30 days) or the threshold defined in config, whichever is greater.
|
||||||
|
const importThreshold = Math.max(membersTotal - events.meta.pagination.total, this._configThreshold);
|
||||||
|
if (isFinite(importThreshold) && events.meta.pagination.total > importThreshold) {
|
||||||
|
await this.startVerificationProcess({
|
||||||
|
amountImported: events.meta.pagination.total,
|
||||||
|
throwOnTrigger: false,
|
||||||
|
source: 'import'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef IVerificationResult
|
* @typedef IVerificationResult
|
||||||
* @property {boolean} needsVerification Whether the verification workflow was triggered
|
* @property {boolean} needsVerification Whether the verification workflow was triggered
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// const testUtils = require('./utils');
|
// const testUtils = require('./utils');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
require('./utils');
|
require('./utils');
|
||||||
const VerificationTrigger = require('../lib/verification-trigger');
|
const VerificationTrigger = require('../index');
|
||||||
const DomainEvents = require('@tryghost/domain-events');
|
const DomainEvents = require('@tryghost/domain-events');
|
||||||
const {MemberSubscribeEvent} = require('@tryghost/member-events');
|
const {MemberSubscribeEvent} = require('@tryghost/member-events');
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ describe('Email verification flow', function () {
|
|||||||
|
|
||||||
emailStub.lastCall.firstArg.should.eql({
|
emailStub.lastCall.firstArg.should.eql({
|
||||||
subject: 'Email needs verification',
|
subject: 'Email needs verification',
|
||||||
message: 'Email verification needed for site: {siteUrl}, just imported: {importedNumber} members.',
|
message: 'Email verification needed for site: {siteUrl}, has imported: {importedNumber} members in the last 30 days.',
|
||||||
amountImported: 10
|
amountImported: 10
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -190,4 +190,47 @@ describe('Email verification flow', function () {
|
|||||||
eventStub.lastCall.lastArg['data.source'].should.eql(`data.source:'api'`);
|
eventStub.lastCall.lastArg['data.source'].should.eql(`data.source:'api'`);
|
||||||
eventStub.lastCall.lastArg['data.created_at'].should.startWith(`data.created_at:>'`);
|
eventStub.lastCall.lastArg['data.created_at'].should.startWith(`data.created_at:>'`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Triggers when a number of members are imported', async function () {
|
||||||
|
const emailStub = sinon.stub().resolves(null);
|
||||||
|
const settingsStub = sinon.stub().resolves(null);
|
||||||
|
const eventStub = sinon.stub().resolves({
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
total: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const trigger = new VerificationTrigger({
|
||||||
|
configThreshold: 2,
|
||||||
|
Settings: {
|
||||||
|
edit: settingsStub
|
||||||
|
},
|
||||||
|
membersStats: {
|
||||||
|
getTotalMembers: () => 15
|
||||||
|
},
|
||||||
|
isVerified: () => false,
|
||||||
|
isVerificationRequired: () => false,
|
||||||
|
sendVerificationEmail: emailStub,
|
||||||
|
eventRepository: {
|
||||||
|
getNewsletterSubscriptionEvents: eventStub
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await trigger.testImportThreshold();
|
||||||
|
|
||||||
|
eventStub.callCount.should.eql(1);
|
||||||
|
eventStub.lastCall.lastArg.should.have.property('data.source');
|
||||||
|
eventStub.lastCall.lastArg.should.have.property('data.created_at');
|
||||||
|
eventStub.lastCall.lastArg['data.source'].should.eql(`data.source:'admin'`);
|
||||||
|
eventStub.lastCall.lastArg['data.created_at'].should.startWith(`data.created_at:>'`);
|
||||||
|
|
||||||
|
emailStub.callCount.should.eql(1);
|
||||||
|
emailStub.lastCall.firstArg.should.eql({
|
||||||
|
subject: 'Email needs verification',
|
||||||
|
message: 'Email verification needed for site: {siteUrl}, has imported: {importedNumber} members in the last 30 days.',
|
||||||
|
amountImported: 10
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user