From 10596f192120b388577d3f620eb2d299b668c315 Mon Sep 17 00:00:00 2001 From: Benjamin Piouffle Date: Fri, 14 Dec 2018 13:20:15 +0100 Subject: [PATCH] feat(VirtualCard): Send gift cards redeem link emails --- scripts/compile-email.js | 80 ++++++---- server/graphql/v1/mutations.js | 2 +- server/lib/emailTemplates.js | 2 + .../opencollective/virtualcard.js | 28 +++- templates/emails/user.card.invited.hbs | 27 ++++ templates/emails/user.card.invited.text.hbs | 4 + ...ymentMethods.opencollective.virtualcard.js | 141 ++++++++++++------ 7 files changed, 203 insertions(+), 81 deletions(-) create mode 100644 templates/emails/user.card.invited.hbs create mode 100644 templates/emails/user.card.invited.text.hbs diff --git a/scripts/compile-email.js b/scripts/compile-email.js index b8a92e9165f..0f802fcccde 100644 --- a/scripts/compile-email.js +++ b/scripts/compile-email.js @@ -58,7 +58,7 @@ data['collective.expense.paid'] = { viewLatestExpenses: 'https://opencollective.com/wwcodeaustin/expenses', }, }; -(data['user.card.claimed'] = { +data['user.card.claimed'] = { currency: 'USD', initialBalance: 10000, expiryDate: new Date().setMonth(new Date().getMonth() + 3), @@ -73,37 +73,53 @@ data['collective.expense.paid'] = { 'https://opencollective-production.s3-us-west-1.amazonaws.com/02f87560-b2f1-11e8-85a0-75f200a0e2db.png', }, loginLink: 'https://opencollective.com/signin?next=', -}), - (data['ticket.confirmed'] = { - recipient: { - name: 'Xavier Damman', - }, - event: { - name: 'Sustain 2019 San Francisco', - slug: 'sutain-2019-sf', - startsAt: '2019-06-19 17:15:00+00', - endsAt: '2019-06-19 21:15:00+00', - timezone: 'America/Los_Angeles', - locationName: 'Github HQ', - address: '88 Colin P Kelly Jr Street, San Francisco, CA', - }, - collective: { - slug: 'sustainoss', - }, - tier: { - id: 1, - name: 'Regular Ticket', - description: 'This gives you access to all the workshops', - amount: 1000, - currency: 'USD', - }, - order: { - id: 2312321, - quantity: 2, - totalAmount: 5000, - currency: 'USD', - }, - }); +}; +data['user.card.invited'] = { + currency: 'USD', + initialBalance: 10000, + expiryDate: new Date().setMonth(new Date().getMonth() + 3), + emitter: { + slug: 'triplebyte', + name: 'Triplebyte', + description: + 'Triplebyte lets talented software engineers skip resumes recruiters and go straight to final interviews at multiple top tech companies at once.', + image: 'https://opencollective-production.s3-us-west-1.amazonaws.com/02f87560-b2f1-11e8-85a0-75f200a0e2db.png', + backgroundImage: 'https://d.pr/free/i/GEbbjb+', + previewImage: + 'https://opencollective-production.s3-us-west-1.amazonaws.com/02f87560-b2f1-11e8-85a0-75f200a0e2db.png', + }, + redeemCode: '00000000', +}; +data['ticket.confirmed'] = { + recipient: { + name: 'Xavier Damman', + }, + event: { + name: 'Sustain 2019 San Francisco', + slug: 'sutain-2019-sf', + startsAt: '2019-06-19 17:15:00+00', + endsAt: '2019-06-19 21:15:00+00', + timezone: 'America/Los_Angeles', + locationName: 'Github HQ', + address: '88 Colin P Kelly Jr Street, San Francisco, CA', + }, + collective: { + slug: 'sustainoss', + }, + tier: { + id: 1, + name: 'Regular Ticket', + description: 'This gives you access to all the workshops', + amount: 1000, + currency: 'USD', + }, + order: { + id: 2312321, + quantity: 2, + totalAmount: 5000, + currency: 'USD', + }, +}; data['ticket.confirmed.sustainoss'] = data['ticket.confirmed']; data['ticket.confirmed.fearlesscitiesbrussels'] = data['ticket.confirmed']; data['github.signup'] = { diff --git a/server/graphql/v1/mutations.js b/server/graphql/v1/mutations.js index d33b93c081f..4ece8aa4432 100644 --- a/server/graphql/v1/mutations.js +++ b/server/graphql/v1/mutations.js @@ -1,4 +1,4 @@ -import { omit, times } from 'lodash'; +import { omit } from 'lodash'; import { claimCollective, createCollective, diff --git a/server/lib/emailTemplates.js b/server/lib/emailTemplates.js index aa1570c9783..2f7a403779a 100644 --- a/server/lib/emailTemplates.js +++ b/server/lib/emailTemplates.js @@ -70,6 +70,8 @@ const templateNames = [ 'thankyou.laprimaire', 'user.card.claimed', 'user.card.claimed.text', + 'user.card.invited', + 'user.card.invited.text', 'user.forgot.password', 'user.monthlyreport', 'user.monthlyreport.text', diff --git a/server/paymentProviders/opencollective/virtualcard.js b/server/paymentProviders/opencollective/virtualcard.js index dcbb867f20b..4961e772a28 100644 --- a/server/paymentProviders/opencollective/virtualcard.js +++ b/server/paymentProviders/opencollective/virtualcard.js @@ -5,6 +5,7 @@ import models, { Op, sequelize } from '../../models'; import * as libpayments from '../../lib/payments'; import * as currency from '../../lib/currency'; import { formatCurrency, isValidEmail } from '../../lib/utils'; +import emailLib from '../../lib/email'; /** * Virtual Card Payment method - This payment Method works basically as an alias @@ -147,7 +148,7 @@ async function create(args, remoteUser) { const sourcePaymentMethod = await getSourcePaymentMethodFromCreateArgs(args, collective); const createParams = getCreateParams(args, collective, sourcePaymentMethod, remoteUser); const virtualCard = await models.PaymentMethod.create(createParams); - // TODO send email + sendVirtualCardCreatedEmail(virtualCard, collective); return virtualCard; } @@ -192,7 +193,7 @@ export async function createVirtualCardsForEmails(args, remoteUser, emails) { getCreateParams({ ...args, data: { email } }, collective, sourcePaymentMethod, remoteUser), ); const virtualCards = models.PaymentMethod.bulkCreate(virtualCardsParams); - // TODO send emails + virtualCards.map(vc => sendVirtualCardCreatedEmail(vc, collective)); return virtualCards; } @@ -302,6 +303,29 @@ function getCreateParams(args, collective, sourcePaymentMethod, remoteUser) { }; } +/** + * Send an email with the virtual card redeem URL to the user. + * + * @param {object} virtualCard + */ +async function sendVirtualCardCreatedEmail(virtualCard, emitterCollective) { + const code = virtualCard.uuid.split('-')[0]; + const email = get(virtualCard, 'data.email'); + + if (!email) { + return false; + } + + return emailLib.send('user.card.invited', email, { + redeemCode: code, + initialBalance: virtualCard.initialBalance, + expiryDate: virtualCard.expiryDate, + name: virtualCard.name, + currency: virtualCard.currency, + emitter: emitterCollective, + }); +} + /** Claim the Virtual Card Payment Method By an (existing or not) user * @param {Object} args contains the parameters * @param {String} args.code The 8 last digits of the UUID diff --git a/templates/emails/user.card.invited.hbs b/templates/emails/user.card.invited.hbs new file mode 100644 index 00000000000..5d297b11467 --- /dev/null +++ b/templates/emails/user.card.invited.hbs @@ -0,0 +1,27 @@ +Subject: 🎁 {{emitter.name}} has granted you a {{{currency initialBalance currency=currency}}} gift card to donate on Open Collective! + +{{> header}} + + + + + + + +
+
+

{{#ifCond currency '===' 'USD'}}💵{{/ifCond}}{{#ifCond currency '===' 'EUR'}}💶{{/ifCond}}{{#ifCond currency '===' 'GBP'}}💷{{/ifCond}}

+

+ You're one step away to redeem the {{{currency initialBalance currency=currency}}} gift card + offered to you by {{emitter.name}}. Just + click on the button bellow and fill your info to redeem your gift card 🎁 +

+
+
Redeem
+
+
+
+ {{> collectivecard emitter}} +
+ +{{> footer}} \ No newline at end of file diff --git a/templates/emails/user.card.invited.text.hbs b/templates/emails/user.card.invited.text.hbs new file mode 100644 index 00000000000..6607926337f --- /dev/null +++ b/templates/emails/user.card.invited.text.hbs @@ -0,0 +1,4 @@ +You're one step away to redeem the {{{currency initialBalance currency=currency}}} gift card +offered to you by {{emitter.name}}. Just click on the button bellow and fill your info to redeem your gift card 🎁 + +{{config.host.website}}/redeem/{{redeemCode}} diff --git a/test/paymentMethods.opencollective.virtualcard.js b/test/paymentMethods.opencollective.virtualcard.js index 308af0ccd53..c9216f09f04 100644 --- a/test/paymentMethods.opencollective.virtualcard.js +++ b/test/paymentMethods.opencollective.virtualcard.js @@ -100,14 +100,16 @@ describe('opencollective.virtualcard', () => { }).then(c => (collective1 = c)), ); before('creates User 1', () => models.User.createUserWithCollective({ name: 'User 1' }).then(u => (user1 = u))); - before('user1 to become Admin of collective1', () => - models.Member.create({ + before('user1 to become Admin of collective1', () => { + return models.Member.create({ CreatedByUserId: user1.id, MemberCollectiveId: user1.CollectiveId, CollectiveId: collective1.id, role: 'ADMIN', - }), - ); + }).then(() => { + user1.populateRoles(); + }); + }); before('create a payment method', () => models.PaymentMethod.create({ name: '4242', @@ -126,7 +128,7 @@ describe('opencollective.virtualcard', () => { amount: 10000, currency: 'USD', }; - const paymentMethod = await virtualcard.create(args); + const paymentMethod = await virtualcard.create(args, user1); expect(paymentMethod).to.exist; expect(paymentMethod.CollectiveId).to.be.equal(collective1.id); expect(paymentMethod.initialBalance).to.be.equal(args.amount); @@ -150,7 +152,7 @@ describe('opencollective.virtualcard', () => { currency: 'USD', expiryDate: expiryDate, }; - const paymentMethod = await virtualcard.create(args); + const paymentMethod = await virtualcard.create(args, user1); expect(paymentMethod).to.exist; expect(paymentMethod.CollectiveId).to.be.equal(collective1.id); expect(paymentMethod.initialBalance).to.be.equal(args.amount); @@ -167,7 +169,7 @@ describe('opencollective.virtualcard', () => { monthlyLimitPerMember: 10000, currency: 'USD', }; - const paymentMethod = await virtualcard.create(args); + const paymentMethod = await virtualcard.create(args, user1); expect(paymentMethod).to.exist; expect(paymentMethod.CollectiveId).to.be.equal(collective1.id); expect(paymentMethod.service).to.be.equal('opencollective'); @@ -194,7 +196,7 @@ describe('opencollective.virtualcard', () => { currency: 'USD', expiryDate: expiryDate, }; - const paymentMethod = await virtualcard.create(args); + const paymentMethod = await virtualcard.create(args, user1); expect(paymentMethod).to.exist; expect(paymentMethod.CollectiveId).to.be.equal(collective1.id); expect(paymentMethod.service).to.be.equal('opencollective'); @@ -207,7 +209,7 @@ describe('opencollective.virtualcard', () => { }); /** End Of "#create" */ describe('#claim', async () => { - let collective1, paymentMethod1, virtualCardPaymentMethod; + let collective1, paymentMethod1, user1, virtualCardPaymentMethod; before(() => utils.resetTestDB()); before('create collective1(currency USD, No Host)', () => @@ -228,16 +230,27 @@ describe('opencollective.virtualcard', () => { }).then(pm => (paymentMethod1 = pm)), ); - before('create a virtual card payment method', () => - virtualcard - .create({ - description: 'virtual card test', - CollectiveId: collective1.id, - amount: 10000, - currency: 'USD', - }) - .then(pm => (virtualCardPaymentMethod = pm)), - ); + before('creates User 1', () => models.User.createUserWithCollective({ name: 'User 1' }).then(u => (user1 = u))); + before('user1 to become Admin of collective1', () => { + return models.Member.create({ + CreatedByUserId: user1.id, + MemberCollectiveId: user1.CollectiveId, + CollectiveId: collective1.id, + role: 'ADMIN', + }).then(() => { + user1.populateRoles(); + }); + }); + + before('create a virtual card payment method', () => { + const createParams = { + description: 'virtual card test', + CollectiveId: collective1.id, + amount: 10000, + currency: 'USD', + }; + return virtualcard.create(createParams, user1).then(pm => (virtualCardPaymentMethod = pm)); + }); it('new User should claim a virtual card', async () => { // setting correct code to claim virtual card by new User @@ -274,7 +287,7 @@ describe('opencollective.virtualcard', () => { }); /** End Of "#claim" */ describe('#processOrder', async () => { - let host1, collective1, collective2, paymentMethod1, virtualCardPaymentMethod, user, userCollective; + let host1, collective1, collective2, paymentMethod1, virtualCardPaymentMethod, user, user1, userCollective; before(() => utils.resetTestDB()); @@ -306,6 +319,19 @@ describe('opencollective.virtualcard', () => { isActive: true, }).then(c => (collective2 = c)), ); + + before('creates User 1', () => models.User.createUserWithCollective({ name: 'User 1' }).then(u => (user1 = u))); + before('user1 to become Admin of collective1', () => { + return models.Member.create({ + CreatedByUserId: user1.id, + MemberCollectiveId: user1.CollectiveId, + CollectiveId: collective1.id, + role: 'ADMIN', + }).then(() => { + user1.populateRoles(); + }); + }); + before('create a credit card payment method', () => models.PaymentMethod.create({ name: '4242', @@ -319,12 +345,15 @@ describe('opencollective.virtualcard', () => { beforeEach('create a virtual card payment method', () => virtualcard - .create({ - description: 'virtual card test', - CollectiveId: collective1.id, - amount: 10000, - currency: 'USD', - }) + .create( + { + description: 'virtual card test', + CollectiveId: collective1.id, + amount: 10000, + currency: 'USD', + }, + user1, + ) .then(pm => (virtualCardPaymentMethod = pm)), ); @@ -588,7 +617,7 @@ describe('opencollective.virtualcard', () => { }); /** End Of "#create" */ describe('#claim', async () => { - let collective1, paymentMethod1, virtualCardPaymentMethod; + let collective1, paymentMethod1, virtualCardPaymentMethod, user1; before(() => utils.resetTestDB()); @@ -612,14 +641,29 @@ describe('opencollective.virtualcard', () => { }).then(pm => (paymentMethod1 = pm)), ); + before('creates User 1', () => models.User.createUserWithCollective({ name: 'User 1' }).then(u => (user1 = u))); + before('user1 to become Admin of collective1', () => { + return models.Member.create({ + CreatedByUserId: user1.id, + MemberCollectiveId: user1.CollectiveId, + CollectiveId: collective1.id, + role: 'ADMIN', + }).then(() => { + user1.populateRoles(); + }); + }); + beforeEach('create a virtual card payment method', () => virtualcard - .create({ - description: 'virtual card test', - CollectiveId: collective1.id, - amount: 10000, - currency: 'USD', - }) + .create( + { + description: 'virtual card test', + CollectiveId: collective1.id, + amount: 10000, + currency: 'USD', + }, + user1, + ) .then(pm => (virtualCardPaymentMethod = pm)), ); @@ -722,7 +766,7 @@ describe('opencollective.virtualcard', () => { }); /** End Of "Existing User should claim a virtual card" */ }); /** End Of "#claim" */ - describe('#processOrder', async () => { + describe('#processOrder2', async () => { let host1, host2, collective1, @@ -779,14 +823,16 @@ describe('opencollective.virtualcard', () => { await collective2.update({ isActive: true }); }); before('creates User 1', () => models.User.createUserWithCollective({ name: 'User 1' }).then(u => (user1 = u))); - before('user1 to become Admin of collective1', () => - models.Member.create({ + before('user1 to become Admin of collective1', () => { + return models.Member.create({ CreatedByUserId: user1.id, MemberCollectiveId: user1.CollectiveId, CollectiveId: collective1.id, role: 'ADMIN', - }), - ); + }).then(() => { + user1.populateRoles(); + }); + }); before('create a credit card payment method', () => models.PaymentMethod.create({ name: '4242', @@ -800,14 +846,17 @@ describe('opencollective.virtualcard', () => { before('create a virtual card payment method', () => virtualcard - .create({ - description: 'virtual card test', - CollectiveId: collective1.id, - amount: 10000, - currency: 'USD', - limitedToHostCollectiveIds: [host1.id], - limitedToTags: ['open source'], - }) + .create( + { + description: 'virtual card test', + CollectiveId: collective1.id, + amount: 10000, + currency: 'USD', + limitedToHostCollectiveIds: [host1.id], + limitedToTags: ['open source'], + }, + user1, + ) .then(pm => (virtualCardPaymentMethod = pm)), );