diff --git a/ghost/members-api/lib/repositories/member.js b/ghost/members-api/lib/repositories/member.js index 0d696792e6..ed374e98fa 100644 --- a/ghost/members-api/lib/repositories/member.js +++ b/ghost/members-api/lib/repositories/member.js @@ -220,7 +220,9 @@ module.exports = class MemberRepository { const context = options && options.context || {}; let source; - if (context.internal) { + if (context.import) { + source = 'import'; + } else if (context.internal) { source = 'system'; } else if (context.user) { source = 'admin'; diff --git a/ghost/members-importer/lib/importer.js b/ghost/members-importer/lib/importer.js index e187eb1a63..fc8c4972f3 100644 --- a/ghost/members-importer/lib/importer.js +++ b/ghost/members-importer/lib/importer.js @@ -153,7 +153,11 @@ module.exports = class MembersCSVImporter { id: existingMember.id }); } 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) { diff --git a/ghost/members-importer/test/importer.test.js b/ghost/members-importer/test/importer.test.js index 5933287d88..ec7493ba08 100644 --- a/ghost/members-importer/test/importer.test.js +++ b/ghost/members-importer/test/importer.test.js @@ -32,6 +32,7 @@ describe('Importer', function () { id: 'default_product_id' }; + const memberCreateStub = sinon.stub().resolves(null); const membersApi = { productRepository: { list: async () => { @@ -44,9 +45,7 @@ describe('Importer', function () { get: async () => { return null; }, - create: async (row) => { - return row; - } + create: memberCreateStub } }; @@ -96,6 +95,10 @@ describe('Importer', function () { result.meta.originalImportSize.should.equal(2); fsWriteSpy.calledOnce.should.be.true(); + + // Called at least once + memberCreateStub.notCalled.should.be.false(); + memberCreateStub.firstCall.lastArg.context.import.should.be.true(); }); }); diff --git a/ghost/verification-trigger/lib/verification-trigger.js b/ghost/verification-trigger/lib/verification-trigger.js index be639bde9d..a812087a50 100644 --- a/ghost/verification-trigger/lib/verification-trigger.js +++ b/ghost/verification-trigger/lib/verification-trigger.js @@ -5,7 +5,7 @@ const {MemberSubscribeEvent} = require('@tryghost/member-events'); 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.`, 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.` }; @@ -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 * @property {boolean} needsVerification Whether the verification workflow was triggered diff --git a/ghost/verification-trigger/test/verification-trigger.test.js b/ghost/verification-trigger/test/verification-trigger.test.js index 2415dbb69e..14dde2920c 100644 --- a/ghost/verification-trigger/test/verification-trigger.test.js +++ b/ghost/verification-trigger/test/verification-trigger.test.js @@ -2,7 +2,7 @@ // const testUtils = require('./utils'); const sinon = require('sinon'); require('./utils'); -const VerificationTrigger = require('../lib/verification-trigger'); +const VerificationTrigger = require('../index'); const DomainEvents = require('@tryghost/domain-events'); const {MemberSubscribeEvent} = require('@tryghost/member-events'); @@ -150,7 +150,7 @@ describe('Email verification flow', function () { emailStub.lastCall.firstArg.should.eql({ 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 }); }); @@ -190,4 +190,47 @@ describe('Email verification flow', function () { eventStub.lastCall.lastArg['data.source'].should.eql(`data.source:'api'`); 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 + }); + }); });