From 369752783cc07685a9e73bbc710d86ee9a1ce14e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 17 Nov 2022 13:46:59 +0100 Subject: [PATCH 01/41] Translate and HTML'lize email 'sendAccountActivation' --- .../emails/accountMultiRegistration/html.pug | 8 +++---- .../src/emails/sendAccountActivation/html.pug | 20 ++++++++++++++++ .../emails/sendAccountActivation/subject.pug | 1 + backend/src/emails/sendEmailTranslated.ts | 4 ++-- backend/src/emails/sendEmailVariants.ts | 24 ++++++++++++++++++- backend/src/graphql/resolver/UserResolver.ts | 11 +++++---- backend/src/locales/de.json | 13 ++++++++-- backend/src/locales/en.json | 21 +++++++++++----- 8 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 backend/src/emails/sendAccountActivation/html.pug create mode 100644 backend/src/emails/sendAccountActivation/subject.pug diff --git a/backend/src/emails/accountMultiRegistration/html.pug b/backend/src/emails/accountMultiRegistration/html.pug index e285c940b..b3764403b 100644 --- a/backend/src/emails/accountMultiRegistration/html.pug +++ b/backend/src/emails/accountMultiRegistration/html.pug @@ -1,11 +1,11 @@ doctype html -html(lang="en") +html(lang=locale) head title= t('emails.accountMultiRegistration.subject') body h1(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.subject') #container.col - p(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.helloName', { firstName, lastName }) + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) p= t('emails.accountMultiRegistration.emailReused') br span= t('emails.accountMultiRegistration.emailExists') @@ -17,6 +17,6 @@ html(lang="en") p= t('emails.accountMultiRegistration.ifYouAreNotTheOne') br a(href='https://gradido.net/de/contact/') https://gradido.net/de/contact/ - p(style='margin-top: 24px;')= t('emails.accountMultiRegistration.sincerelyYours') + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') br - span= t('emails.accountMultiRegistration.yourGradidoTeam') + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/sendAccountActivation/html.pug b/backend/src/emails/sendAccountActivation/html.pug new file mode 100644 index 000000000..18bce36b5 --- /dev/null +++ b/backend/src/emails/sendAccountActivation/html.pug @@ -0,0 +1,20 @@ +doctype html +html(lang=locale) + head + title= t('emails.sendAccountActivation.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.sendAccountActivation.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.sendAccountActivation.emailRegistered') + p= t('emails.sendAccountActivation.pleaseClickLink') + br + a(href=activationLink) #{activationLink} + br + span= t('emails.sendAccountActivation.orCopyLink') + p= t('emails.sendAccountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes }) + br + a(href=resendLink) #{resendLink} + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/sendAccountActivation/subject.pug b/backend/src/emails/sendAccountActivation/subject.pug new file mode 100644 index 000000000..09586310f --- /dev/null +++ b/backend/src/emails/sendAccountActivation/subject.pug @@ -0,0 +1 @@ += t('emails.sendAccountActivation.subject') \ No newline at end of file diff --git a/backend/src/emails/sendEmailTranslated.ts b/backend/src/emails/sendEmailTranslated.ts index 3fe4177f4..fc1161b8a 100644 --- a/backend/src/emails/sendEmailTranslated.ts +++ b/backend/src/emails/sendEmailTranslated.ts @@ -12,7 +12,7 @@ export const sendEmailTranslated = async (params: { cc?: string } template: string - locals: Record + locals: Record }): Promise | null> => { let resultSend: Record | null = null @@ -50,7 +50,7 @@ export const sendEmailTranslated = async (params: { }, }) - i18n.setLocale(params.locals.locale) // for email + i18n.setLocale(params.locals.locale as string) // for email // TESTING: see 'README.md' const email = new Email({ diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index fb142f206..3ee749354 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -11,9 +11,31 @@ export const sendAccountMultiRegistrationEmail = (data: { receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, template: 'accountMultiRegistration', locals: { - locale: data.language, firstName: data.firstName, lastName: data.lastName, + locale: data.language, + resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, + }, + }) +} + +export const sendAccountActivationEmail = (data: { + firstName: string + lastName: string + email: string + language: string + activationLink: string + timeDurationObject: Record +}): Promise | null> => { + return sendEmailTranslated({ + receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, + template: 'sendAccountActivation', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + activationLink: data.activationLink, + timeDurationObject: data.timeDurationObject, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, }, }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 81d0bab0f..d9e7cefa7 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -18,8 +18,10 @@ import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs' import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware' import { OptInType } from '@enum/OptInType' import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail' -import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' -import { sendAccountMultiRegistrationEmail } from '@/emails/sendEmailVariants' +import { + sendAccountActivationEmail, + sendAccountMultiRegistrationEmail, +} from '@/emails/sendEmailVariants' import { klicktippSignIn } from '@/apis/KlicktippController' import { RIGHTS } from '@/auth/RIGHTS' import { hasElopageBuys } from '@/util/hasElopageBuys' @@ -543,11 +545,12 @@ export class UserResolver { // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendAccountActivationEmail({ - link: activationLink, firstName, lastName, email, - duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME), + language, + activationLink, + timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), }) logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`) eventSendConfirmEmail.userId = dbUser.id diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index 6c270f148..2b97d19f0 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -3,13 +3,22 @@ "accountMultiRegistration": { "emailExists": "Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto.", "emailReused": "Deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.", - "helloName": "Hallo {firstName} {lastName}", "ifYouAreNotTheOne": "Wenn du nicht derjenige bist, der sich versucht hat erneut zu registrieren, wende dich bitte an unseren support:", "onForgottenPasswordClickLink": "Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest:", "onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", + "subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail" + }, + "general": { + "helloName": "Hallo {firstName} {lastName}", "sincerelyYours": "Mit freundlichen Grüßen,", - "subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail", "yourGradidoTeam": "dein Gradido-Team" + }, + "sendAccountActivation": { + "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", + "emailRegistered": "Deine E-Mail-Adresse wurde soeben bei Gradido registriert.", + "pleaseClickLink": "Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:", + "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", + "subject": "Gradido: E-Mail Überprüfung" } } } \ No newline at end of file diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index 7655aae6a..e9f43e416 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -1,15 +1,24 @@ { - "emails": { - "accountMultiRegistration": { + "emails": { + "accountMultiRegistration": { "emailExists": "However, an account already exists for your email address.", "emailReused": "Your email address has just been used again to register an account with Gradido.", - "helloName": "Hello {firstName} {lastName}", "ifYouAreNotTheOne": "If you are not the one who tried to register again, please contact our support:", "onForgottenPasswordClickLink": "Please click on the following link if you have forgotten your password:", "onForgottenPasswordCopyLink": "or copy the link above into your browser window.", + "subject": "Gradido: Try To Register Again With Your Email" + }, + "general": { + "helloName": "Hello {firstName} {lastName}", "sincerelyYours": "Sincerely yours,", - "subject": "Gradido: Try To Register Again With Your Email", "yourGradidoTeam": "your Gradido team" - } - } + }, + "sendAccountActivation": { + "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", + "emailRegistered": "Your email address has just been registered with Gradido.", + "pleaseClickLink": "Please click on this link to complete the registration and activate your Gradido account:", + "orCopyLink": "or copy the link above into your browser window.", + "subject": "Gradido: Email Verification" + } + } } \ No newline at end of file From 0fdb3e4401686f52128ecb07209e0c1ee83f4a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 17 Nov 2022 15:27:07 +0100 Subject: [PATCH 02/41] Test 'sendAccountActivationEmail' --- backend/src/emails/sendEmailVariants.test.ts | 71 +++++++++++++++++++- backend/src/emails/sendEmailVariants.ts | 36 +++++----- 2 files changed, 88 insertions(+), 19 deletions(-) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 4ac8221a7..017efc270 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -1,5 +1,5 @@ import CONFIG from '@/config' -import { sendAccountMultiRegistrationEmail } from './sendEmailVariants' +import { sendAccountActivationEmail, sendAccountMultiRegistrationEmail } from './sendEmailVariants' import { sendEmailTranslated } from './sendEmailTranslated' CONFIG.EMAIL = true @@ -19,6 +19,75 @@ jest.mock('./sendEmailTranslated', () => { describe('sendEmailVariants', () => { let result: Record | null + describe('sendAccountActivationEmail', () => { + beforeAll(async () => { + result = await sendAccountActivationEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + activationLink: 'http://localhost/checkEmail/6627633878930542284', + timeDurationObject: { hours: 24, minutes: 0 }, + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'sendAccountActivation', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + activationLink: 'http://localhost/checkEmail/6627633878930542284', + timeDurationObject: { hours: 24, minutes: 0 }, + resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, + }, + }) + }) + + it('has expected result', () => { + expect(result).toMatchObject({ + envelope: { + from: 'info@gradido.net', + to: ['peter@lustig.de'], + }, + message: expect.any(String), + originalMessage: expect.objectContaining({ + to: 'Peter Lustig ', + from: 'Gradido (nicht antworten) ', + attachments: [], + subject: 'Gradido: Email Verification', + html: + expect.stringContaining('Gradido: Email Verification') && + expect.stringContaining('>Gradido: Email Verification') && + expect.stringContaining( + 'Your email address has just been registered with Gradido.', + ) && + expect.stringContaining( + 'Please click on this link to complete the registration and activate your Gradido account:', + ) && + expect.stringContaining( + 'http://localhost/checkEmail/6627633878930542284', + ) && + expect.stringContaining('or copy the link above into your browser window.') && + expect.stringContaining( + 'The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:', + ) && + expect.stringContaining( + `${CONFIG.EMAIL_LINK_FORGOTPASSWORD}`, + ) && + expect.stringContaining('Sincerely yours,
your Gradido team'), + text: expect.stringContaining('GRADIDO: EMAIL VERIFICATION'), + }), + }) + }) + }) + }) + describe('sendAccountMultiRegistrationEmail', () => { beforeAll(async () => { result = await sendAccountMultiRegistrationEmail({ diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index 3ee749354..c507f6ace 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -1,24 +1,6 @@ import CONFIG from '@/config' import { sendEmailTranslated } from './sendEmailTranslated' -export const sendAccountMultiRegistrationEmail = (data: { - firstName: string - lastName: string - email: string - language: string -}): Promise | null> => { - return sendEmailTranslated({ - receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, - template: 'accountMultiRegistration', - locals: { - firstName: data.firstName, - lastName: data.lastName, - locale: data.language, - resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, - }, - }) -} - export const sendAccountActivationEmail = (data: { firstName: string lastName: string @@ -40,3 +22,21 @@ export const sendAccountActivationEmail = (data: { }, }) } + +export const sendAccountMultiRegistrationEmail = (data: { + firstName: string + lastName: string + email: string + language: string +}): Promise | null> => { + return sendEmailTranslated({ + receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, + template: 'accountMultiRegistration', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, + }, + }) +} From 560aed056aa97de953e38e2f8d0d6f0063a5a51b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 21 Nov 2022 12:20:51 +0100 Subject: [PATCH 03/41] Fix spelling and punctuation in German email translations --- backend/src/locales/de.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index 2b97d19f0..78d9d50b3 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -2,20 +2,20 @@ "emails": { "accountMultiRegistration": { "emailExists": "Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto.", - "emailReused": "Deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.", + "emailReused": "deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.", "ifYouAreNotTheOne": "Wenn du nicht derjenige bist, der sich versucht hat erneut zu registrieren, wende dich bitte an unseren support:", "onForgottenPasswordClickLink": "Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest:", "onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail" }, "general": { - "helloName": "Hallo {firstName} {lastName}", + "helloName": "Hallo {firstName} {lastName},", "sincerelyYours": "Mit freundlichen Grüßen,", "yourGradidoTeam": "dein Gradido-Team" }, "sendAccountActivation": { "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", - "emailRegistered": "Deine E-Mail-Adresse wurde soeben bei Gradido registriert.", + "emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.", "pleaseClickLink": "Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:", "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "subject": "Gradido: E-Mail Überprüfung" From b78229b646311ec991421261e4f7ea4763ac357a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 21 Nov 2022 12:22:25 +0100 Subject: [PATCH 04/41] Fix faulty email content tests --- backend/src/emails/sendEmailVariants.test.ts | 99 +++++++++++--------- 1 file changed, 54 insertions(+), 45 deletions(-) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 017efc270..698ba32d9 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -17,7 +17,8 @@ jest.mock('./sendEmailTranslated', () => { }) describe('sendEmailVariants', () => { - let result: Record | null + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let result: any describe('sendAccountActivationEmail', () => { beforeAll(async () => { @@ -27,7 +28,7 @@ describe('sendEmailVariants', () => { email: 'peter@lustig.de', language: 'en', activationLink: 'http://localhost/checkEmail/6627633878930542284', - timeDurationObject: { hours: 24, minutes: 0 }, + timeDurationObject: { hours: 23, minutes: 30 }, }) }) @@ -43,7 +44,7 @@ describe('sendEmailVariants', () => { lastName: 'Lustig', locale: 'en', activationLink: 'http://localhost/checkEmail/6627633878930542284', - timeDurationObject: { hours: 24, minutes: 0 }, + timeDurationObject: { hours: 23, minutes: 30 }, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, }, }) @@ -61,29 +62,32 @@ describe('sendEmailVariants', () => { from: 'Gradido (nicht antworten) ', attachments: [], subject: 'Gradido: Email Verification', - html: - expect.stringContaining('Gradido: Email Verification') && - expect.stringContaining('>Gradido: Email Verification') && - expect.stringContaining( - 'Your email address has just been registered with Gradido.', - ) && - expect.stringContaining( - 'Please click on this link to complete the registration and activate your Gradido account:', - ) && - expect.stringContaining( - 'http://localhost/checkEmail/6627633878930542284', - ) && - expect.stringContaining('or copy the link above into your browser window.') && - expect.stringContaining( - 'The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:', - ) && - expect.stringContaining( - `${CONFIG.EMAIL_LINK_FORGOTPASSWORD}`, - ) && - expect.stringContaining('Sincerely yours,
your Gradido team'), + html: expect.any(String), text: expect.stringContaining('GRADIDO: EMAIL VERIFICATION'), }), }) + expect(result.originalMessage.html).toContain('Gradido: Email Verification') + expect(result.originalMessage.html).toContain('>Gradido: Email Verification') + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'Your email address has just been registered with Gradido.', + ) + expect(result.originalMessage.html).toContain( + 'Please click on this link to complete the registration and activate your Gradido account:', + ) + expect(result.originalMessage.html).toContain( + 'http://localhost/checkEmail/6627633878930542284', + ) + expect(result.originalMessage.html).toContain( + 'or copy the link above into your browser window.', + ) + expect(result.originalMessage.html).toContain( + 'The link has a validity of 23 hours and 30 minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:', + ) + expect(result.originalMessage.html).toContain( + `${CONFIG.EMAIL_LINK_FORGOTPASSWORD}`, + ) + expect(result.originalMessage.html).toContain('Sincerely yours,
your Gradido team') }) }) }) @@ -126,31 +130,36 @@ describe('sendEmailVariants', () => { from: 'Gradido (nicht antworten) ', attachments: [], subject: 'Gradido: Try To Register Again With Your Email', - html: - expect.stringContaining( - 'Gradido: Try To Register Again With Your Email', - ) && - expect.stringContaining('>Gradido: Try To Register Again With Your Email') && - expect.stringContaining( - 'Your email address has just been used again to register an account with Gradido.', - ) && - expect.stringContaining( - 'However, an account already exists for your email address.', - ) && - expect.stringContaining( - 'Please click on the following link if you have forgotten your password:', - ) && - expect.stringContaining( - `${CONFIG.EMAIL_LINK_FORGOTPASSWORD}`, - ) && - expect.stringContaining('or copy the link above into your browser window.') && - expect.stringContaining( - 'If you are not the one who tried to register again, please contact our support:', - ) && - expect.stringContaining('Sincerely yours,
your Gradido team'), + html: expect.any(String), text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'), }), }) + expect(result.originalMessage.html).toContain( + 'Gradido: Try To Register Again With Your Email', + ) + expect(result.originalMessage.html).toContain( + '>Gradido: Try To Register Again With Your Email', + ) + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'Your email address has just been used again to register an account with Gradido.', + ) + expect(result.originalMessage.html).toContain( + 'However, an account already exists for your email address.', + ) + expect(result.originalMessage.html).toContain( + 'Please click on the following link if you have forgotten your password:', + ) + expect(result.originalMessage.html).toContain( + `${CONFIG.EMAIL_LINK_FORGOTPASSWORD}`, + ) + expect(result.originalMessage.html).toContain( + 'or copy the link above into your browser window.', + ) + expect(result.originalMessage.html).toContain( + 'If you are not the one who tried to register again, please contact our support:', + ) + expect(result.originalMessage.html).toContain('Sincerely yours,
your Gradido team') }) }) }) From 18a4408e8aca1c9bbbe39fedbcfd447b2cb26d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 21 Nov 2022 13:37:29 +0100 Subject: [PATCH 05/41] Adjust the tests for new translatable 'sendAccountActivationEmail' --- .../graphql/resolver/AdminResolver.test.ts | 5 ++-- backend/src/graphql/resolver/AdminResolver.ts | 9 ++++---- .../src/graphql/resolver/UserResolver.test.ts | 23 +++++++++---------- backend/src/graphql/resolver/UserResolver.ts | 2 +- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 503bab472..ea1bb848e 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -36,7 +36,7 @@ import { import { GraphQLError } from 'graphql' import { User } from '@entity/User' /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' +import { sendAccountActivationEmail } from '@/emails/sendEmailVariants' import Decimal from 'decimal.js-light' import { Contribution } from '@entity/Contribution' import { Transaction as DbTransaction } from '@entity/Transaction' @@ -47,9 +47,10 @@ import { EventProtocolType } from '@/event/EventProtocolType' import { logger } from '@test/testSetup' // mock account activation email to avoid console spam -jest.mock('@/mailer/sendAccountActivationEmail', () => { +jest.mock('@/emails/sendEmailVariants', () => { return { __esModule: true, + // TODO: test the call of … sendAccountActivationEmail: jest.fn(), } }) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 80c69a864..bd3ed20dc 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -39,8 +39,8 @@ import { Decay } from '@model/Decay' import Paginated from '@arg/Paginated' import TransactionLinkFilters from '@arg/TransactionLinkFilters' import { Order } from '@enum/Order' -import { findUserByEmail, activationLink, printTimeDuration } from './UserResolver' -import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' +import { findUserByEmail, activationLink, getTimeDurationObject } from './UserResolver' +import { sendAccountActivationEmail } from '@/emails/sendEmailVariants' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import CONFIG from '@/config' import { @@ -656,11 +656,12 @@ export class AdminResolver { // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendAccountActivationEmail({ - link: activationLink(emailContact.emailVerificationCode), firstName: user.firstName, lastName: user.lastName, email, - duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME), + language: user.language, + activationLink: activationLink(emailContact.emailVerificationCode), + timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), }) // In case EMails are disabled log the activation link for the user diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 6323abfde..fac4618bf 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -18,15 +18,16 @@ import { verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queri import { GraphQLError } from 'graphql' import { User } from '@entity/User' import CONFIG from '@/config' -import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' -import { sendAccountMultiRegistrationEmail } from '@/emails/sendEmailVariants' +import { + sendAccountActivationEmail, + sendAccountMultiRegistrationEmail, +} from '@/emails/sendEmailVariants' import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' import { printTimeDuration, activationLink } from './UserResolver' import { contributionLinkFactory } from '@/seeds/factory/contributionLink' import { transactionLinkFactory } from '@/seeds/factory/transactionLink' import { ContributionLink } from '@model/ContributionLink' import { TransactionLink } from '@entity/TransactionLink' - import { EventProtocolType } from '@/event/EventProtocolType' import { EventProtocol } from '@entity/EventProtocol' import { logger, i18n as localization } from '@test/testSetup' @@ -39,16 +40,10 @@ import { bobBaumeister } from '@/seeds/users/bob-baumeister' // import { klicktippSignIn } from '@/apis/KlicktippController' -jest.mock('@/mailer/sendAccountActivationEmail', () => { - return { - __esModule: true, - sendAccountActivationEmail: jest.fn(), - } -}) - jest.mock('@/emails/sendEmailVariants', () => { return { __esModule: true, + sendAccountActivationEmail: jest.fn(), sendAccountMultiRegistrationEmail: jest.fn(), } }) @@ -180,11 +175,15 @@ describe('UserResolver', () => { emailVerificationCode, ).replace(/{code}/g, '') expect(sendAccountActivationEmail).toBeCalledWith({ - link: activationLink, firstName: 'Peter', lastName: 'Lustig', email: 'peter@lustig.de', - duration: expect.any(String), + language: 'de', + activationLink, + timeDurationObject: expect.objectContaining({ + hours: expect.any(Number), + minutes: expect.any(Number), + }), }) }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 62636ebb7..23cf9eaeb 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -953,7 +953,7 @@ const canEmailResend = (updatedAt: Date): boolean => { return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME) } -const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => { +export const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => { if (time > 60) { return { hours: Math.floor(time / 60), From b8fdb59d73582ac5231a9dd8fe7cf1fcfbeb3661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 21 Nov 2022 13:38:18 +0100 Subject: [PATCH 06/41] Remove old email 'sendAccountActivationEmail' --- .../mailer/sendAccountActivationEmail.test.ts | 32 ------------------- .../src/mailer/sendAccountActivationEmail.ts | 17 ---------- backend/src/mailer/text/accountActivation.ts | 32 ------------------- .../mailer/text/accountMultiRegistration.ts | 25 --------------- 4 files changed, 106 deletions(-) delete mode 100644 backend/src/mailer/sendAccountActivationEmail.test.ts delete mode 100644 backend/src/mailer/sendAccountActivationEmail.ts delete mode 100644 backend/src/mailer/text/accountActivation.ts delete mode 100644 backend/src/mailer/text/accountMultiRegistration.ts diff --git a/backend/src/mailer/sendAccountActivationEmail.test.ts b/backend/src/mailer/sendAccountActivationEmail.test.ts deleted file mode 100644 index 08ddae166..000000000 --- a/backend/src/mailer/sendAccountActivationEmail.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { sendAccountActivationEmail } from './sendAccountActivationEmail' -import { sendEMail } from './sendEMail' - -jest.mock('./sendEMail', () => { - return { - __esModule: true, - sendEMail: jest.fn(), - } -}) - -describe('sendAccountActivationEmail', () => { - beforeEach(async () => { - await sendAccountActivationEmail({ - link: 'activationLink', - firstName: 'Peter', - lastName: 'Lustig', - email: 'peter@lustig.de', - duration: '23 hours and 30 minutes', - }) - }) - - it('calls sendEMail', () => { - expect(sendEMail).toBeCalledWith({ - to: `Peter Lustig `, - subject: 'Gradido: E-Mail Überprüfung', - text: - expect.stringContaining('Hallo Peter Lustig') && - expect.stringContaining('activationLink') && - expect.stringContaining('23 Stunden und 30 Minuten'), - }) - }) -}) diff --git a/backend/src/mailer/sendAccountActivationEmail.ts b/backend/src/mailer/sendAccountActivationEmail.ts deleted file mode 100644 index 335f80a82..000000000 --- a/backend/src/mailer/sendAccountActivationEmail.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { sendEMail } from './sendEMail' -import { accountActivation } from './text/accountActivation' -import CONFIG from '@/config' - -export const sendAccountActivationEmail = (data: { - link: string - firstName: string - lastName: string - email: string - duration: string -}): Promise => { - return sendEMail({ - to: `${data.firstName} ${data.lastName} <${data.email}>`, - subject: accountActivation.de.subject, - text: accountActivation.de.text({ ...data, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD }), - }) -} diff --git a/backend/src/mailer/text/accountActivation.ts b/backend/src/mailer/text/accountActivation.ts deleted file mode 100644 index 2755c4c0a..000000000 --- a/backend/src/mailer/text/accountActivation.ts +++ /dev/null @@ -1,32 +0,0 @@ -export const accountActivation = { - de: { - subject: 'Gradido: E-Mail Überprüfung', - text: (data: { - link: string - firstName: string - lastName: string - email: string - duration: string - resendLink: string - }): string => - `Hallo ${data.firstName} ${data.lastName}, - -Deine E-Mail-Adresse wurde soeben bei Gradido registriert. - -Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren: -${data.link} -oder kopiere den obigen Link in dein Browserfenster. - -Der Link hat eine Gültigkeit von ${data.duration - .replace('hours', 'Stunden') - .replace('minutes', 'Minuten') - .replace( - ' and ', - ' und ', - )}. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst: -${data.resendLink} - -Mit freundlichen Grüßen, -dein Gradido-Team`, - }, -} diff --git a/backend/src/mailer/text/accountMultiRegistration.ts b/backend/src/mailer/text/accountMultiRegistration.ts deleted file mode 100644 index c5b55bac5..000000000 --- a/backend/src/mailer/text/accountMultiRegistration.ts +++ /dev/null @@ -1,25 +0,0 @@ -export const accountMultiRegistration = { - de: { - subject: 'Gradido: Erneuter Registrierungsversuch mit deiner E-Mail', - text: (data: { - firstName: string - lastName: string - email: string - resendLink: string - }): string => - `Hallo ${data.firstName} ${data.lastName}, - -Deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren. -Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto. - -Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest: -${data.resendLink} -oder kopiere den obigen Link in dein Browserfenster. - -Wenn du nicht derjenige bist, der sich versucht hat erneut zu registrieren, wende dich bitte an unseren support: -https://gradido.net/de/contact/ - -Mit freundlichen Grüßen, -dein Gradido-Team`, - }, -} From 624cd6e4bc25d9ef3da04b933a622bbd2a2d028c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 22 Nov 2022 08:34:48 +0100 Subject: [PATCH 07/41] Fix spelling of error message in 'sendActivationEmail' --- backend/src/graphql/resolver/AdminResolver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index bd3ed20dc..047f9bc4e 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -650,8 +650,8 @@ export class AdminResolver { } const emailContact = user.emailContact if (emailContact.deletedAt) { - logger.error(`The emailContact: ${email} of htis User is deleted.`) - throw new Error(`The emailContact: ${email} of htis User is deleted.`) + logger.error(`The emailContact: ${email} of this User is deleted.`) + throw new Error(`The emailContact: ${email} of this User is deleted.`) } // eslint-disable-next-line @typescript-eslint/no-unused-vars From 0695ac282b89ade1217f88b2de2ba0d388838891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 22 Nov 2022 11:17:26 +0100 Subject: [PATCH 08/41] Adjust the fixed 'CONFIG.EMAIL_TEST_MODUS' problem in 'sendEmailTranslated.ts' --- backend/src/emails/sendEmailTranslated.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/emails/sendEmailTranslated.ts b/backend/src/emails/sendEmailTranslated.ts index fc1161b8a..39291a0ac 100644 --- a/backend/src/emails/sendEmailTranslated.ts +++ b/backend/src/emails/sendEmailTranslated.ts @@ -32,8 +32,7 @@ export const sendEmailTranslated = async (params: { logger.info(`Emails are disabled via config...`) return null } - // because 'CONFIG.EMAIL_TEST_MODUS' can be boolean 'true' or string '`false`' - if (CONFIG.EMAIL_TEST_MODUS === true) { + if (CONFIG.EMAIL_TEST_MODUS) { logger.info( `Testmodus=ON: change receiver from ${params.receiver.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`, ) From 53b29db25ec5388d733ca9f22ef8fd2ef9420372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 22 Nov 2022 11:24:13 +0100 Subject: [PATCH 09/41] =?UTF-8?q?Add=20README's=20for=20locales=20?= =?UTF-8?q?=E2=80=93=20especially=20for=20quotation=20marks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/locales/README.md | 3 +++ backend/src/locales/README.md | 3 +++ frontend/src/locales/README.md | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 admin/src/locales/README.md create mode 100644 backend/src/locales/README.md create mode 100644 frontend/src/locales/README.md diff --git a/admin/src/locales/README.md b/admin/src/locales/README.md new file mode 100644 index 000000000..5d6bf75b1 --- /dev/null +++ b/admin/src/locales/README.md @@ -0,0 +1,3 @@ +# Localizations + +Please see [frontend localization](/frontend/src/locales/README.md). diff --git a/backend/src/locales/README.md b/backend/src/locales/README.md new file mode 100644 index 000000000..5d6bf75b1 --- /dev/null +++ b/backend/src/locales/README.md @@ -0,0 +1,3 @@ +# Localizations + +Please see [frontend localization](/frontend/src/locales/README.md). diff --git a/frontend/src/locales/README.md b/frontend/src/locales/README.md new file mode 100644 index 000000000..2c03abbd4 --- /dev/null +++ b/frontend/src/locales/README.md @@ -0,0 +1,25 @@ +# Localizations + +## Quotation Marks + +The following characters are different from the programming quotation mark: + +`"` + +### English + +In English, we use these double-barreled quotation marks: + +“This is a sample sentence.” + +Please copy and paste … + +See + +### German + +In German, we use these double-barreled quotation marks: + +„Dies ist ein Beispielsatz.“ + +Please copy and paste … From 76fa42552931783b7bd819e787070f6fd3d427df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 22 Nov 2022 13:18:32 +0100 Subject: [PATCH 10/41] Refactor 'sendAddedContributionMessageEmail' email to HTML and translatable --- backend/src/emails/accountActivation/html.pug | 20 +++++++++++++ .../src/emails/accountActivation/subject.pug | 1 + .../emails/addedContributionMessage/html.pug | 17 +++++++++++ .../addedContributionMessage/subject.pug | 1 + .../src/emails/sendAccountActivation/html.pug | 20 ------------- .../emails/sendAccountActivation/subject.pug | 1 - backend/src/emails/sendEmailVariants.test.ts | 2 +- backend/src/emails/sendEmailVariants.ts | 28 ++++++++++++++++++- backend/src/graphql/resolver/AdminResolver.ts | 16 +++++------ backend/src/locales/de.json | 23 +++++++++------ backend/src/locales/en.json | 23 +++++++++------ 11 files changed, 105 insertions(+), 47 deletions(-) create mode 100644 backend/src/emails/accountActivation/html.pug create mode 100644 backend/src/emails/accountActivation/subject.pug create mode 100644 backend/src/emails/addedContributionMessage/html.pug create mode 100644 backend/src/emails/addedContributionMessage/subject.pug delete mode 100644 backend/src/emails/sendAccountActivation/html.pug delete mode 100644 backend/src/emails/sendAccountActivation/subject.pug diff --git a/backend/src/emails/accountActivation/html.pug b/backend/src/emails/accountActivation/html.pug new file mode 100644 index 000000000..9c631c960 --- /dev/null +++ b/backend/src/emails/accountActivation/html.pug @@ -0,0 +1,20 @@ +doctype html +html(lang=locale) + head + title= t('emails.accountActivation.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.accountActivation.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.accountActivation.emailRegistered') + p= t('emails.accountActivation.pleaseClickLink') + br + a(href=activationLink) #{activationLink} + br + span= t('emails.accountActivation.orCopyLink') + p= t('emails.accountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes }) + br + a(href=resendLink) #{resendLink} + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/accountActivation/subject.pug b/backend/src/emails/accountActivation/subject.pug new file mode 100644 index 000000000..378053bbf --- /dev/null +++ b/backend/src/emails/accountActivation/subject.pug @@ -0,0 +1 @@ += t('emails.accountActivation.subject') \ No newline at end of file diff --git a/backend/src/emails/addedContributionMessage/html.pug b/backend/src/emails/addedContributionMessage/html.pug new file mode 100644 index 000000000..020f36c33 --- /dev/null +++ b/backend/src/emails/addedContributionMessage/html.pug @@ -0,0 +1,17 @@ +doctype html +html(lang=locale) + head + title= t('emails.addedContributionMessage.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.addedContributionMessage.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.addedContributionMessage.commonGoodContributionMessage', { senderFirstName, senderLastName, contributionMemo }) + p= t('emails.addedContributionMessage.toSeeAndAnswerMessage') + p= t('emails.addedContributionMessage.linkToYourAccount') + span= " " + a(href=overviewURL) #{overviewURL} + p= t('emails.addedContributionMessage.pleaseDoNotReply') + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/addedContributionMessage/subject.pug b/backend/src/emails/addedContributionMessage/subject.pug new file mode 100644 index 000000000..8620725f8 --- /dev/null +++ b/backend/src/emails/addedContributionMessage/subject.pug @@ -0,0 +1 @@ += t('emails.addedContributionMessage.subject') \ No newline at end of file diff --git a/backend/src/emails/sendAccountActivation/html.pug b/backend/src/emails/sendAccountActivation/html.pug deleted file mode 100644 index 18bce36b5..000000000 --- a/backend/src/emails/sendAccountActivation/html.pug +++ /dev/null @@ -1,20 +0,0 @@ -doctype html -html(lang=locale) - head - title= t('emails.sendAccountActivation.subject') - body - h1(style='margin-bottom: 24px;')= t('emails.sendAccountActivation.subject') - #container.col - p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) - p= t('emails.sendAccountActivation.emailRegistered') - p= t('emails.sendAccountActivation.pleaseClickLink') - br - a(href=activationLink) #{activationLink} - br - span= t('emails.sendAccountActivation.orCopyLink') - p= t('emails.sendAccountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes }) - br - a(href=resendLink) #{resendLink} - p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') - br - span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/sendAccountActivation/subject.pug b/backend/src/emails/sendAccountActivation/subject.pug deleted file mode 100644 index 09586310f..000000000 --- a/backend/src/emails/sendAccountActivation/subject.pug +++ /dev/null @@ -1 +0,0 @@ -= t('emails.sendAccountActivation.subject') \ No newline at end of file diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 698ba32d9..4b8e184ae 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -38,7 +38,7 @@ describe('sendEmailVariants', () => { receiver: { to: 'Peter Lustig ', }, - template: 'sendAccountActivation', + template: 'accountActivation', locals: { firstName: 'Peter', lastName: 'Lustig', diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index c507f6ace..4d0cd6e5c 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -1,6 +1,32 @@ import CONFIG from '@/config' import { sendEmailTranslated } from './sendEmailTranslated' +export const sendAddedContributionMessageEmail = (data: { + firstName: string + lastName: string + email: string + language: string + senderFirstName: string + senderLastName: string + contributionMemo: string +}): Promise | null> => { + return sendEmailTranslated({ + receiver: { + to: `${data.firstName} ${data.lastName} <${data.email}>`, + }, + template: 'addedContributionMessage', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + senderFirstName: data.senderFirstName, + senderLastName: data.senderLastName, + contributionMemo: data.contributionMemo, + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) +} + export const sendAccountActivationEmail = (data: { firstName: string lastName: string @@ -11,7 +37,7 @@ export const sendAccountActivationEmail = (data: { }): Promise | null> => { return sendEmailTranslated({ receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, - template: 'sendAccountActivation', + template: 'accountActivation', locals: { firstName: data.firstName, lastName: data.lastName, diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 047f9bc4e..1c1e8968b 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -40,7 +40,10 @@ import Paginated from '@arg/Paginated' import TransactionLinkFilters from '@arg/TransactionLinkFilters' import { Order } from '@enum/Order' import { findUserByEmail, activationLink, getTimeDurationObject } from './UserResolver' -import { sendAccountActivationEmail } from '@/emails/sendEmailVariants' +import { + sendAddedContributionMessageEmail, + sendAccountActivationEmail, +} from '@/emails/sendEmailVariants' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import CONFIG from '@/config' import { @@ -65,7 +68,6 @@ import { ContributionMessageType } from '@enum/MessageType' import { ContributionMessage } from '@model/ContributionMessage' import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail' import { sendContributionRejectedEmail } from '@/mailer/sendContributionRejectedEmail' -import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributionMessageEmail' import { eventProtocol } from '@/event/EventProtocolEmitter' import { Event, @@ -896,15 +898,13 @@ export class AdminResolver { } await sendAddedContributionMessageEmail({ + firstName: contribution.user.firstName, + lastName: contribution.user.lastName, + email: contribution.user.emailContact.email, + language: contribution.user.language, senderFirstName: user.firstName, senderLastName: user.lastName, - recipientFirstName: contribution.user.firstName, - recipientLastName: contribution.user.lastName, - recipientEmail: contribution.user.emailContact.email, - senderEmail: user.emailContact.email, contributionMemo: contribution.memo, - message, - overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, }) await queryRunner.commitTransaction() } catch (e) { diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index 78d9d50b3..85bac73c8 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -1,5 +1,19 @@ { "emails": { + "addedContributionMessage": { + "commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.", + "linkToYourAccount": "Link zu deinem Konto:", + "pleaseDoNotReply": "Bitte antworte nicht auf diese E-Mail!", + "subject": "Gradido: Nachricht zu deinem Gemeinwohl-Beitrag", + "toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!" + }, + "accountActivation": { + "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", + "emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.", + "pleaseClickLink": "Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:", + "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", + "subject": "Gradido: E-Mail Überprüfung" + }, "accountMultiRegistration": { "emailExists": "Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto.", "emailReused": "deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.", @@ -10,15 +24,8 @@ }, "general": { "helloName": "Hallo {firstName} {lastName},", - "sincerelyYours": "Mit freundlichen Grüßen,", + "sincerelyYours": "Liebe Grüße", "yourGradidoTeam": "dein Gradido-Team" - }, - "sendAccountActivation": { - "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", - "emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.", - "pleaseClickLink": "Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:", - "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", - "subject": "Gradido: E-Mail Überprüfung" } } } \ No newline at end of file diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index e9f43e416..5207696da 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -1,5 +1,19 @@ { "emails": { + "addedContributionMessage": { + "commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.", + "linkToYourAccount": "Link to your account:", + "pleaseDoNotReply": "Please do not reply to this email!", + "subject": "Gradido: Message about your common good contribution", + "toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My Contributions to the Common Good” tab!" + }, + "accountActivation": { + "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", + "emailRegistered": "Your email address has just been registered with Gradido.", + "pleaseClickLink": "Please click on this link to complete the registration and activate your Gradido account:", + "orCopyLink": "or copy the link above into your browser window.", + "subject": "Gradido: Email Verification" + }, "accountMultiRegistration": { "emailExists": "However, an account already exists for your email address.", "emailReused": "Your email address has just been used again to register an account with Gradido.", @@ -10,15 +24,8 @@ }, "general": { "helloName": "Hello {firstName} {lastName}", - "sincerelyYours": "Sincerely yours,", + "sincerelyYours": "Kind regards,", "yourGradidoTeam": "your Gradido team" - }, - "sendAccountActivation": { - "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", - "emailRegistered": "Your email address has just been registered with Gradido.", - "pleaseClickLink": "Please click on this link to complete the registration and activate your Gradido account:", - "orCopyLink": "or copy the link above into your browser window.", - "subject": "Gradido: Email Verification" } } } \ No newline at end of file From fb015671698b28b5d1d4f425c3e0888557707d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 22 Nov 2022 14:13:01 +0100 Subject: [PATCH 11/41] Test 'sendAddedContributionMessageEmail' in 'sendEmailVariants.test.ts' --- backend/src/emails/sendEmailVariants.test.ts | 86 +++++++++++++++++++- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 4b8e184ae..38e901828 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -1,5 +1,9 @@ import CONFIG from '@/config' -import { sendAccountActivationEmail, sendAccountMultiRegistrationEmail } from './sendEmailVariants' +import { + sendAddedContributionMessageEmail, + sendAccountActivationEmail, + sendAccountMultiRegistrationEmail, +} from './sendEmailVariants' import { sendEmailTranslated } from './sendEmailTranslated' CONFIG.EMAIL = true @@ -20,6 +24,78 @@ describe('sendEmailVariants', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let result: any + describe('sendAddedContributionMessageEmail', () => { + beforeAll(async () => { + result = await sendAddedContributionMessageEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'addedContributionMessage', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) + }) + + it('has expected result', () => { + expect(result).toMatchObject({ + envelope: { + from: 'info@gradido.net', + to: ['peter@lustig.de'], + }, + message: expect.any(String), + originalMessage: expect.objectContaining({ + to: 'Peter Lustig ', + from: 'Gradido (nicht antworten) ', + attachments: [], + subject: 'Gradido: Message about your common good contribution', + html: expect.any(String), + text: expect.stringContaining('GRADIDO: MESSAGE ABOUT YOUR COMMON GOOD CONTRIBUTION'), + }), + }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain( + 'Gradido: Message about your common good contribution', + ) + expect(result.originalMessage.html).toContain( + '>Gradido: Message about your common good contribution', + ) + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'you have received a message from Bibi Bloxberg regarding your common good contribution “My contribution.”.', + ) + expect(result.originalMessage.html).toContain( + 'To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My Contributions to the Common Good” tab!', + ) + expect(result.originalMessage.html).toContain( + 'Link to your account: http://localhost/overview', + ) + expect(result.originalMessage.html).toContain('Please do not reply to this email!') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') + }) + }) + }) + describe('sendAccountActivationEmail', () => { beforeAll(async () => { result = await sendAccountActivationEmail({ @@ -66,6 +142,8 @@ describe('sendEmailVariants', () => { text: expect.stringContaining('GRADIDO: EMAIL VERIFICATION'), }), }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') expect(result.originalMessage.html).toContain('Gradido: Email Verification') expect(result.originalMessage.html).toContain('>Gradido: Email Verification') expect(result.originalMessage.html).toContain('Hello Peter Lustig') @@ -87,7 +165,7 @@ describe('sendEmailVariants', () => { expect(result.originalMessage.html).toContain( `${CONFIG.EMAIL_LINK_FORGOTPASSWORD}`, ) - expect(result.originalMessage.html).toContain('Sincerely yours,
your Gradido team') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') }) }) }) @@ -134,6 +212,8 @@ describe('sendEmailVariants', () => { text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'), }), }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') expect(result.originalMessage.html).toContain( 'Gradido: Try To Register Again With Your Email', ) @@ -159,7 +239,7 @@ describe('sendEmailVariants', () => { expect(result.originalMessage.html).toContain( 'If you are not the one who tried to register again, please contact our support:', ) - expect(result.originalMessage.html).toContain('Sincerely yours,
your Gradido team') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') }) }) }) From a1ec8dfa4f8b2a477b2ce65949f4e082478e9a7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 22 Nov 2022 14:16:10 +0100 Subject: [PATCH 12/41] Remove old untranslated email 'sendAddedContributionMessageEmail' --- .../sendAddedContributionMessageEmail.test.ts | 40 ------------------- .../sendAddedContributionMessageEmail.ts | 26 ------------ .../text/contributionMessageReceived.ts | 28 ------------- 3 files changed, 94 deletions(-) delete mode 100644 backend/src/mailer/sendAddedContributionMessageEmail.test.ts delete mode 100644 backend/src/mailer/sendAddedContributionMessageEmail.ts delete mode 100644 backend/src/mailer/text/contributionMessageReceived.ts diff --git a/backend/src/mailer/sendAddedContributionMessageEmail.test.ts b/backend/src/mailer/sendAddedContributionMessageEmail.test.ts deleted file mode 100644 index 9a2ec1aa1..000000000 --- a/backend/src/mailer/sendAddedContributionMessageEmail.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { sendAddedContributionMessageEmail } from './sendAddedContributionMessageEmail' -import { sendEMail } from './sendEMail' - -jest.mock('./sendEMail', () => { - return { - __esModule: true, - sendEMail: jest.fn(), - } -}) - -describe('sendAddedContributionMessageEmail', () => { - beforeEach(async () => { - await sendAddedContributionMessageEmail({ - senderFirstName: 'Peter', - senderLastName: 'Lustig', - recipientFirstName: 'Bibi', - recipientLastName: 'Bloxberg', - recipientEmail: 'bibi@bloxberg.de', - senderEmail: 'peter@lustig.de', - contributionMemo: 'Vielen herzlichen Dank für den neuen Hexenbesen!', - message: 'Was für ein Besen ist es geworden?', - overviewURL: 'http://localhost/overview', - }) - }) - - it('calls sendEMail', () => { - expect(sendEMail).toBeCalledWith({ - to: `Bibi Bloxberg `, - subject: 'Nachricht zu deinem Gemeinwohl-Beitrag', - text: - expect.stringContaining('Hallo Bibi Bloxberg') && - expect.stringContaining('Peter Lustig') && - expect.stringContaining( - 'du hast zu deinem Gemeinwohl-Beitrag "Vielen herzlichen Dank für den neuen Hexenbesen!" eine Nachricht von Peter Lustig erhalten.', - ) && - expect.stringContaining('Was für ein Besen ist es geworden?') && - expect.stringContaining('http://localhost/overview'), - }) - }) -}) diff --git a/backend/src/mailer/sendAddedContributionMessageEmail.ts b/backend/src/mailer/sendAddedContributionMessageEmail.ts deleted file mode 100644 index 14d5f6d31..000000000 --- a/backend/src/mailer/sendAddedContributionMessageEmail.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { backendLogger as logger } from '@/server/logger' -import { sendEMail } from './sendEMail' -import { contributionMessageReceived } from './text/contributionMessageReceived' - -export const sendAddedContributionMessageEmail = (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - recipientEmail: string - senderEmail: string - contributionMemo: string - message: string - overviewURL: string -}): Promise => { - logger.info( - `sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>, - subject=${contributionMessageReceived.de.subject}, - text=${contributionMessageReceived.de.text(data)}`, - ) - return sendEMail({ - to: `${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>`, - subject: contributionMessageReceived.de.subject, - text: contributionMessageReceived.de.text(data), - }) -} diff --git a/backend/src/mailer/text/contributionMessageReceived.ts b/backend/src/mailer/text/contributionMessageReceived.ts deleted file mode 100644 index 301ebef22..000000000 --- a/backend/src/mailer/text/contributionMessageReceived.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const contributionMessageReceived = { - de: { - subject: 'Nachricht zu deinem Gemeinwohl-Beitrag', - text: (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - recipientEmail: string - senderEmail: string - contributionMemo: string - message: string - overviewURL: string - }): string => - `Hallo ${data.recipientFirstName} ${data.recipientLastName}, - -du hast zu deinem Gemeinwohl-Beitrag "${data.contributionMemo}" eine Nachricht von ${data.senderFirstName} ${data.senderLastName} erhalten. - -Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü "Gemeinschaft" auf den Tab "Meine Beiträge zum Gemeinwohl"! - -Link zu deinem Konto: ${data.overviewURL} - -Bitte antworte nicht auf diese E-Mail! - -Liebe Grüße -dein Gradido-Team`, - }, -} From 98798d33228e35a0c93f4509345cc10b353ac89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 22 Nov 2022 17:24:36 +0100 Subject: [PATCH 13/41] Adjust test of 'sendAddedContributionMessageEmail' in 'ContributionMessageResolver.test.ts' --- .../graphql/resolver/AdminResolver.test.ts | 8 ++++--- .../ContributionMessageResolver.test.ts | 23 +++++++++++-------- .../src/graphql/resolver/UserResolver.test.ts | 10 +++++--- backend/test/testSetup.ts | 4 ++++ 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index ea1bb848e..2ff7b0437 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -3,6 +3,7 @@ import { objectValuesToArray } from '@/util/utilities' import { testEnvironment, resetToken, cleanDB, contributionDateFormatter } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' import { userFactory } from '@/seeds/factory/user' import { creationFactory } from '@/seeds/factory/creation' import { creations } from '@/seeds/creation/index' @@ -44,14 +45,15 @@ import { ContributionLink as DbContributionLink } from '@entity/ContributionLink import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail' import { EventProtocol } from '@entity/EventProtocol' import { EventProtocolType } from '@/event/EventProtocolType' -import { logger } from '@test/testSetup' // mock account activation email to avoid console spam jest.mock('@/emails/sendEmailVariants', () => { + const originalModule = jest.requireActual('@/emails/sendEmailVariants') return { __esModule: true, + ...originalModule, // TODO: test the call of … - sendAccountActivationEmail: jest.fn(), + sendAccountActivationEmail: jest.fn((a) => originalModule.sendAccountActivationEmail(a)), } }) @@ -67,7 +69,7 @@ let mutate: any, query: any, con: any let testEnv: any beforeAll(async () => { - testEnv = await testEnvironment() + testEnv = await testEnvironment(logger, localization) mutate = testEnv.mutate query = testEnv.query con = testEnv.con diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts index 612c2d20b..436830c2c 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { cleanDB, resetToken, testEnvironment } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' import { GraphQLError } from 'graphql' import { adminCreateContributionMessage, @@ -13,12 +14,16 @@ import { listContributionMessages } from '@/seeds/graphql/queries' import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' -import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributionMessageEmail' +import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants' -jest.mock('@/mailer/sendAddedContributionMessageEmail', () => { +jest.mock('@/emails/sendEmailVariants', () => { + const originalModule = jest.requireActual('@/emails/sendEmailVariants') return { __esModule: true, - sendAddedContributionMessageEmail: jest.fn(), + ...originalModule, + sendAddedContributionMessageEmail: jest.fn((a) => + originalModule.sendAddedContributionMessageEmail(a), + ), } }) @@ -27,7 +32,7 @@ let testEnv: any let result: any beforeAll(async () => { - testEnv = await testEnvironment() + testEnv = await testEnvironment(logger, localization) mutate = testEnv.mutate con = testEnv.con await cleanDB() @@ -162,15 +167,13 @@ describe('ContributionMessageResolver', () => { it('calls sendAddedContributionMessageEmail', async () => { expect(sendAddedContributionMessageEmail).toBeCalledWith({ + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + language: 'de', senderFirstName: 'Peter', senderLastName: 'Lustig', - recipientFirstName: 'Bibi', - recipientLastName: 'Bloxberg', - recipientEmail: 'bibi@bloxberg.de', - senderEmail: 'peter@lustig.de', contributionMemo: 'Test env contribution', - message: 'Admin Test', - overviewURL: 'http://localhost/overview', }) }) }) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index fac4618bf..1303b8aaf 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { @@ -30,7 +31,6 @@ import { ContributionLink } from '@model/ContributionLink' import { TransactionLink } from '@entity/TransactionLink' import { EventProtocolType } from '@/event/EventProtocolType' import { EventProtocol } from '@entity/EventProtocol' -import { logger, i18n as localization } from '@test/testSetup' import { validate as validateUUID, version as versionUUID } from 'uuid' import { peterLustig } from '@/seeds/users/peter-lustig' import { UserContact } from '@entity/UserContact' @@ -41,10 +41,14 @@ import { bobBaumeister } from '@/seeds/users/bob-baumeister' // import { klicktippSignIn } from '@/apis/KlicktippController' jest.mock('@/emails/sendEmailVariants', () => { + const originalModule = jest.requireActual('@/emails/sendEmailVariants') return { __esModule: true, - sendAccountActivationEmail: jest.fn(), - sendAccountMultiRegistrationEmail: jest.fn(), + ...originalModule, + sendAccountActivationEmail: jest.fn((a) => originalModule.sendAccountActivationEmail(a)), + sendAccountMultiRegistrationEmail: jest.fn((a) => + originalModule.sendAccountMultiRegistrationEmail(a), + ), } }) diff --git a/backend/test/testSetup.ts b/backend/test/testSetup.ts index 06779674d..300a9cbf3 100644 --- a/backend/test/testSetup.ts +++ b/backend/test/testSetup.ts @@ -1,6 +1,10 @@ +import CONFIG from '@/config' import { backendLogger as logger } from '@/server/logger' import { i18n } from '@/server/localization' +CONFIG.EMAIL = true +CONFIG.EMAIL_TEST_MODUS = false + jest.setTimeout(1000000) jest.mock('@/server/logger', () => { From c87ffd5b8a54d57ec9617961b1a8a9174c9d5714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 14:44:50 +0100 Subject: [PATCH 14/41] Refactor 'sendContributionConfirmedEmail' email to HTML and translatable --- .../emails/addedContributionMessage/html.pug | 4 +-- .../src/emails/contributionConfirmed/html.pug | 17 ++++++++++ .../emails/contributionConfirmed/subject.pug | 1 + backend/src/emails/sendEmailTranslated.ts | 3 +- backend/src/emails/sendEmailVariants.ts | 34 +++++++++++++++++++ backend/src/graphql/resolver/AdminResolver.ts | 10 +++--- backend/src/locales/de.json | 10 ++++-- backend/src/locales/en.json | 10 ++++-- 8 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 backend/src/emails/contributionConfirmed/html.pug create mode 100644 backend/src/emails/contributionConfirmed/subject.pug diff --git a/backend/src/emails/addedContributionMessage/html.pug b/backend/src/emails/addedContributionMessage/html.pug index 020f36c33..5e5d0975c 100644 --- a/backend/src/emails/addedContributionMessage/html.pug +++ b/backend/src/emails/addedContributionMessage/html.pug @@ -8,10 +8,10 @@ html(lang=locale) p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) p= t('emails.addedContributionMessage.commonGoodContributionMessage', { senderFirstName, senderLastName, contributionMemo }) p= t('emails.addedContributionMessage.toSeeAndAnswerMessage') - p= t('emails.addedContributionMessage.linkToYourAccount') + p= t('emails.general.linkToYourAccount') span= " " a(href=overviewURL) #{overviewURL} - p= t('emails.addedContributionMessage.pleaseDoNotReply') + p= t('emails.general.pleaseDoNotReply') p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') br span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/contributionConfirmed/html.pug b/backend/src/emails/contributionConfirmed/html.pug new file mode 100644 index 000000000..e60e6c700 --- /dev/null +++ b/backend/src/emails/contributionConfirmed/html.pug @@ -0,0 +1,17 @@ +doctype html +html(lang=locale) + head + title= t('emails.contributionConfirmed.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.contributionConfirmed.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.contributionConfirmed.commonGoodContributionConfirmed', { senderFirstName, senderLastName, contributionMemo }) + p= t('emails.contributionConfirmed.contributionAmount', { contributionAmount }) + p= t('emails.general.linkToYourAccount') + span= " " + a(href=overviewURL) #{overviewURL} + p= t('emails.general.pleaseDoNotReply') + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/contributionConfirmed/subject.pug b/backend/src/emails/contributionConfirmed/subject.pug new file mode 100644 index 000000000..7e74a77c6 --- /dev/null +++ b/backend/src/emails/contributionConfirmed/subject.pug @@ -0,0 +1 @@ += t('emails.contributionConfirmed.subject') \ No newline at end of file diff --git a/backend/src/emails/sendEmailTranslated.ts b/backend/src/emails/sendEmailTranslated.ts index 39291a0ac..9468e9f97 100644 --- a/backend/src/emails/sendEmailTranslated.ts +++ b/backend/src/emails/sendEmailTranslated.ts @@ -1,11 +1,10 @@ +import CONFIG from '@/config' import { backendLogger as logger } from '@/server/logger' import path from 'path' import { createTransport } from 'nodemailer' import Email from 'email-templates' import i18n from 'i18n' -import CONFIG from '@/config' - export const sendEmailTranslated = async (params: { receiver: { to: string diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index 4d0cd6e5c..8d1dcf8de 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -1,3 +1,5 @@ +import i18n from 'i18n' +import Decimal from 'decimal.js-light' import CONFIG from '@/config' import { sendEmailTranslated } from './sendEmailTranslated' @@ -66,3 +68,35 @@ export const sendAccountMultiRegistrationEmail = (data: { }, }) } + +export const sendContributionConfirmedEmail = (data: { + firstName: string + lastName: string + email: string + language: string + senderFirstName: string + senderLastName: string + contributionMemo: string + contributionAmount: Decimal +}): Promise | null> => { + const rememberLocaleToRestore = i18n.getLocale() + i18n.setLocale(data.language) + const contributionAmount = data.contributionAmount + .toFixed(2) + .replace('.', i18n.__('emails.general.decimalSeparator')) + i18n.setLocale(rememberLocaleToRestore) + return sendEmailTranslated({ + receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, + template: 'contributionConfirmed', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + senderFirstName: data.senderFirstName, + senderLastName: data.senderLastName, + contributionMemo: data.contributionMemo, + contributionAmount, + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) +} diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 1c1e8968b..8c4f2e00b 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -43,6 +43,7 @@ import { findUserByEmail, activationLink, getTimeDurationObject } from './UserRe import { sendAddedContributionMessageEmail, sendAccountActivationEmail, + sendContributionConfirmedEmail, } from '@/emails/sendEmailVariants' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import CONFIG from '@/config' @@ -66,7 +67,6 @@ import { ContributionMessage as DbContributionMessage } from '@entity/Contributi import ContributionMessageArgs from '@arg/ContributionMessageArgs' import { ContributionMessageType } from '@enum/MessageType' import { ContributionMessage } from '@model/ContributionMessage' -import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail' import { sendContributionRejectedEmail } from '@/mailer/sendContributionRejectedEmail' import { eventProtocol } from '@/event/EventProtocolEmitter' import { @@ -584,14 +584,14 @@ export class AdminResolver { await queryRunner.commitTransaction() logger.info('creation commited successfuly.') sendContributionConfirmedEmail({ + firstName: user.firstName, + lastName: user.lastName, + email: user.emailContact.email, + language: user.language, senderFirstName: moderatorUser.firstName, senderLastName: moderatorUser.lastName, - recipientFirstName: user.firstName, - recipientLastName: user.lastName, - recipientEmail: user.emailContact.email, contributionMemo: contribution.memo, contributionAmount: contribution.amount, - overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, }) } catch (e) { await queryRunner.rollbackTransaction() diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index 85bac73c8..cca40d630 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -2,8 +2,6 @@ "emails": { "addedContributionMessage": { "commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.", - "linkToYourAccount": "Link zu deinem Konto:", - "pleaseDoNotReply": "Bitte antworte nicht auf diese E-Mail!", "subject": "Gradido: Nachricht zu deinem Gemeinwohl-Beitrag", "toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!" }, @@ -22,8 +20,16 @@ "onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail" }, + "contributionConfirmed": { + "commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.", + "contributionAmount": "Betrag: {contributionAmount} GDD", + "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt" + }, "general": { + "decimalSeparator": ",", "helloName": "Hallo {firstName} {lastName},", + "linkToYourAccount": "Link zu deinem Konto:", + "pleaseDoNotReply": "Bitte antworte nicht auf diese E-Mail!", "sincerelyYours": "Liebe Grüße", "yourGradidoTeam": "dein Gradido-Team" } diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index 5207696da..09cf61a9e 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -2,8 +2,6 @@ "emails": { "addedContributionMessage": { "commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.", - "linkToYourAccount": "Link to your account:", - "pleaseDoNotReply": "Please do not reply to this email!", "subject": "Gradido: Message about your common good contribution", "toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My Contributions to the Common Good” tab!" }, @@ -22,8 +20,16 @@ "onForgottenPasswordCopyLink": "or copy the link above into your browser window.", "subject": "Gradido: Try To Register Again With Your Email" }, + "contributionConfirmed": { + "commonGoodContributionConfirmed": "your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.", + "contributionAmount": "Amount: {contributionAmount} GDD", + "subject": "Gradido: Your common good contribution was confirmed" + }, "general": { + "decimalSeparator": ".", "helloName": "Hello {firstName} {lastName}", + "linkToYourAccount": "Link to your account:", + "pleaseDoNotReply": "Please do not reply to this email!", "sincerelyYours": "Kind regards,", "yourGradidoTeam": "your Gradido team" } From 727e5ca1f68d85f6f3d2a962fd9bed14bf1a6e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 14:45:33 +0100 Subject: [PATCH 15/41] Test 'sendContributionConfirmedEmail' --- backend/src/emails/sendEmailVariants.test.ts | 96 ++++++++++++++++++- .../graphql/resolver/AdminResolver.test.ts | 41 ++++---- 2 files changed, 109 insertions(+), 28 deletions(-) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 38e901828..1d562ebe9 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -1,16 +1,30 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import Decimal from 'decimal.js-light' +import { testEnvironment } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' import CONFIG from '@/config' import { sendAddedContributionMessageEmail, sendAccountActivationEmail, sendAccountMultiRegistrationEmail, + sendContributionConfirmedEmail, } from './sendEmailVariants' import { sendEmailTranslated } from './sendEmailTranslated' -CONFIG.EMAIL = true -CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL' -CONFIG.EMAIL_SMTP_PORT = '1234' -CONFIG.EMAIL_USERNAME = 'user' -CONFIG.EMAIL_PASSWORD = 'pwd' +let con: any +let testEnv: any + +beforeAll(async () => { + testEnv = await testEnvironment(logger, localization) + con = testEnv.con + // await cleanDB() +}) + +afterAll(async () => { + // await cleanDB() + await con.close() +}) jest.mock('./sendEmailTranslated', () => { const originalModule = jest.requireActual('./sendEmailTranslated') @@ -243,4 +257,76 @@ describe('sendEmailVariants', () => { }) }) }) + + describe('sendContributionConfirmedEmail', () => { + beforeAll(async () => { + result = await sendContributionConfirmedEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + contributionAmount: new Decimal(23.54), + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'contributionConfirmed', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + contributionAmount: '23.54', + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) + }) + + it('has expected result', () => { + expect(result).toMatchObject({ + envelope: { + from: 'info@gradido.net', + to: ['peter@lustig.de'], + }, + message: expect.any(String), + originalMessage: expect.objectContaining({ + to: 'Peter Lustig ', + from: 'Gradido (nicht antworten) ', + attachments: [], + subject: 'Gradido: Your common good contribution was confirmed', + html: expect.any(String), + text: expect.stringContaining('GRADIDO: YOUR COMMON GOOD CONTRIBUTION WAS CONFIRMED'), + }), + }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain( + 'Gradido: Your common good contribution was confirmed', + ) + expect(result.originalMessage.html).toContain( + '>Gradido: Your common good contribution was confirmed', + ) + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'your public good contribution “My contribution.” has just been confirmed by Bibi Bloxberg and credited to your Gradido account.', + ) + expect(result.originalMessage.html).toContain('Amount: 23.54 GDD') + expect(result.originalMessage.html).toContain( + 'Link to your account: http://localhost/overview', + ) + expect(result.originalMessage.html).toContain('Please do not reply to this email!') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') + }) + }) + }) }) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 2ff7b0437..d5b4cade5 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -36,13 +36,14 @@ import { } from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' import { User } from '@entity/User' -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -import { sendAccountActivationEmail } from '@/emails/sendEmailVariants' +import { + // sendAccountActivationEmail, + sendContributionConfirmedEmail, +} from '@/emails/sendEmailVariants' import Decimal from 'decimal.js-light' import { Contribution } from '@entity/Contribution' import { Transaction as DbTransaction } from '@entity/Transaction' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' -import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail' import { EventProtocol } from '@entity/EventProtocol' import { EventProtocolType } from '@/event/EventProtocolType' @@ -53,15 +54,10 @@ jest.mock('@/emails/sendEmailVariants', () => { __esModule: true, ...originalModule, // TODO: test the call of … - sendAccountActivationEmail: jest.fn((a) => originalModule.sendAccountActivationEmail(a)), - } -}) - -// mock account activation email to avoid console spam -jest.mock('@/mailer/sendContributionConfirmedEmail', () => { - return { - __esModule: true, - sendContributionConfirmedEmail: jest.fn(), + // sendAccountActivationEmail: jest.fn((a) => originalModule.sendAccountActivationEmail(a)), + sendContributionConfirmedEmail: jest.fn((a) => + originalModule.sendContributionConfirmedEmail(a), + ), } }) @@ -1718,17 +1714,16 @@ describe('AdminResolver', () => { }) it('calls sendContributionConfirmedEmail', async () => { - expect(sendContributionConfirmedEmail).toBeCalledWith( - expect.objectContaining({ - contributionMemo: 'Herzlich Willkommen bei Gradido liebe Bibi!', - overviewURL: 'http://localhost/overview', - recipientEmail: 'bibi@bloxberg.de', - recipientFirstName: 'Bibi', - recipientLastName: 'Bloxberg', - senderFirstName: 'Peter', - senderLastName: 'Lustig', - }), - ) + expect(sendContributionConfirmedEmail).toBeCalledWith({ + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + language: 'de', + senderFirstName: 'Peter', + senderLastName: 'Lustig', + contributionMemo: 'Herzlich Willkommen bei Gradido liebe Bibi!', + contributionAmount: expect.decimalEqual(450), + }) }) it('stores the send confirmation email event in the database', async () => { From dd33254acbc59f579e47eb8fe9967036857dd682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 14:56:09 +0100 Subject: [PATCH 16/41] Remove old untranslated email 'sendContributionConfirmedEmail' --- .../sendContributionConfirmedEmail.test.ts | 39 ------------------- .../mailer/sendContributionConfirmedEmail.ts | 26 ------------- .../src/mailer/text/contributionConfirmed.ts | 30 -------------- 3 files changed, 95 deletions(-) delete mode 100644 backend/src/mailer/sendContributionConfirmedEmail.test.ts delete mode 100644 backend/src/mailer/sendContributionConfirmedEmail.ts delete mode 100644 backend/src/mailer/text/contributionConfirmed.ts diff --git a/backend/src/mailer/sendContributionConfirmedEmail.test.ts b/backend/src/mailer/sendContributionConfirmedEmail.test.ts deleted file mode 100644 index bd89afa69..000000000 --- a/backend/src/mailer/sendContributionConfirmedEmail.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import Decimal from 'decimal.js-light' -import { sendContributionConfirmedEmail } from './sendContributionConfirmedEmail' -import { sendEMail } from './sendEMail' - -jest.mock('./sendEMail', () => { - return { - __esModule: true, - sendEMail: jest.fn(), - } -}) - -describe('sendContributionConfirmedEmail', () => { - beforeEach(async () => { - await sendContributionConfirmedEmail({ - senderFirstName: 'Peter', - senderLastName: 'Lustig', - recipientFirstName: 'Bibi', - recipientLastName: 'Bloxberg', - recipientEmail: 'bibi@bloxberg.de', - contributionMemo: 'Vielen herzlichen Dank für den neuen Hexenbesen!', - contributionAmount: new Decimal(200.0), - overviewURL: 'http://localhost/overview', - }) - }) - - it('calls sendEMail', () => { - expect(sendEMail).toBeCalledWith({ - to: 'Bibi Bloxberg ', - subject: 'Dein Gemeinwohl-Beitrag wurde bestätigt', - text: - expect.stringContaining('Hallo Bibi Bloxberg') && - expect.stringContaining( - 'dein Gemeinwohl-Beitrag "Vielen herzlichen Dank für den neuen Hexenbesen!" wurde soeben von Peter Lustig bestätigt und in deinem Gradido-Konto gutgeschrieben.', - ) && - expect.stringContaining('Betrag: 200,00 GDD') && - expect.stringContaining('Link zu deinem Konto: http://localhost/overview'), - }) - }) -}) diff --git a/backend/src/mailer/sendContributionConfirmedEmail.ts b/backend/src/mailer/sendContributionConfirmedEmail.ts deleted file mode 100644 index 439d240eb..000000000 --- a/backend/src/mailer/sendContributionConfirmedEmail.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { backendLogger as logger } from '@/server/logger' -import Decimal from 'decimal.js-light' -import { sendEMail } from './sendEMail' -import { contributionConfirmed } from './text/contributionConfirmed' - -export const sendContributionConfirmedEmail = (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - recipientEmail: string - contributionMemo: string - contributionAmount: Decimal - overviewURL: string -}): Promise => { - logger.info( - `sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>, - subject=${contributionConfirmed.de.subject}, - text=${contributionConfirmed.de.text(data)}`, - ) - return sendEMail({ - to: `${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>`, - subject: contributionConfirmed.de.subject, - text: contributionConfirmed.de.text(data), - }) -} diff --git a/backend/src/mailer/text/contributionConfirmed.ts b/backend/src/mailer/text/contributionConfirmed.ts deleted file mode 100644 index 106c3a4c5..000000000 --- a/backend/src/mailer/text/contributionConfirmed.ts +++ /dev/null @@ -1,30 +0,0 @@ -import Decimal from 'decimal.js-light' - -export const contributionConfirmed = { - de: { - subject: 'Dein Gemeinwohl-Beitrag wurde bestätigt', - text: (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - contributionMemo: string - contributionAmount: Decimal - overviewURL: string - }): string => - `Hallo ${data.recipientFirstName} ${data.recipientLastName}, - -dein Gemeinwohl-Beitrag "${data.contributionMemo}" wurde soeben von ${data.senderFirstName} ${ - data.senderLastName - } bestätigt und in deinem Gradido-Konto gutgeschrieben. - -Betrag: ${data.contributionAmount.toFixed(2).replace('.', ',')} GDD - -Link zu deinem Konto: ${data.overviewURL} - -Bitte antworte nicht auf diese E-Mail! - -Liebe Grüße -dein Gradido-Team`, - }, -} From 72213988ac0b91b3d433305273cbbc590ed88d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 16:10:29 +0100 Subject: [PATCH 17/41] Refactor 'sendContributionRejectedEmail' email to HTML and translatable --- .../src/emails/contributionRejected/html.pug | 17 +++++++++++++ .../emails/contributionRejected/subject.pug | 1 + backend/src/emails/sendEmailVariants.ts | 24 +++++++++++++++++++ backend/src/graphql/resolver/AdminResolver.ts | 11 ++++----- backend/src/locales/de.json | 5 ++++ backend/src/locales/en.json | 7 +++++- 6 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 backend/src/emails/contributionRejected/html.pug create mode 100644 backend/src/emails/contributionRejected/subject.pug diff --git a/backend/src/emails/contributionRejected/html.pug b/backend/src/emails/contributionRejected/html.pug new file mode 100644 index 000000000..07c014f92 --- /dev/null +++ b/backend/src/emails/contributionRejected/html.pug @@ -0,0 +1,17 @@ +doctype html +html(lang=locale) + head + title= t('emails.contributionRejected.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.contributionRejected.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.contributionRejected.commonGoodContributionRejected', { senderFirstName, senderLastName, contributionMemo }) + p= t('emails.contributionRejected.toSeeContributionsAndMessages') + p= t('emails.general.linkToYourAccount') + span= " " + a(href=overviewURL) #{overviewURL} + p= t('emails.general.pleaseDoNotReply') + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/contributionRejected/subject.pug b/backend/src/emails/contributionRejected/subject.pug new file mode 100644 index 000000000..cdaae4157 --- /dev/null +++ b/backend/src/emails/contributionRejected/subject.pug @@ -0,0 +1 @@ += t('emails.contributionRejected.subject') \ No newline at end of file diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index 8d1dcf8de..1779cc297 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -100,3 +100,27 @@ export const sendContributionConfirmedEmail = (data: { }, }) } + +export const sendContributionRejectedEmail = (data: { + firstName: string + lastName: string + email: string + language: string + senderFirstName: string + senderLastName: string + contributionMemo: string +}): Promise | null> => { + return sendEmailTranslated({ + receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, + template: 'contributionRejected', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + senderFirstName: data.senderFirstName, + senderLastName: data.senderLastName, + contributionMemo: data.contributionMemo, + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) +} diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 8c4f2e00b..993fe8c3b 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -44,6 +44,7 @@ import { sendAddedContributionMessageEmail, sendAccountActivationEmail, sendContributionConfirmedEmail, + sendContributionRejectedEmail, } from '@/emails/sendEmailVariants' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import CONFIG from '@/config' @@ -67,7 +68,6 @@ import { ContributionMessage as DbContributionMessage } from '@entity/Contributi import ContributionMessageArgs from '@arg/ContributionMessageArgs' import { ContributionMessageType } from '@enum/MessageType' import { ContributionMessage } from '@model/ContributionMessage' -import { sendContributionRejectedEmail } from '@/mailer/sendContributionRejectedEmail' import { eventProtocol } from '@/event/EventProtocolEmitter' import { Event, @@ -489,14 +489,13 @@ export class AdminResolver { event.setEventAdminContributionDelete(eventAdminContributionDelete), ) sendContributionRejectedEmail({ + firstName: user.firstName, + lastName: user.lastName, + email: user.emailContact.email, + language: user.language, senderFirstName: moderator.firstName, senderLastName: moderator.lastName, - recipientEmail: user.emailContact.email, - recipientFirstName: user.firstName, - recipientLastName: user.lastName, contributionMemo: contribution.memo, - contributionAmount: contribution.amount, - overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, }) return !!res diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index cca40d630..277728439 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -25,6 +25,11 @@ "contributionAmount": "Betrag: {contributionAmount} GDD", "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt" }, + "contributionRejected": { + "commonGoodContributionRejected": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} abgelehnt.", + "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde abgelehnt", + "toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!" + }, "general": { "decimalSeparator": ",", "helloName": "Hallo {firstName} {lastName},", diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index 09cf61a9e..d4292b05f 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -3,7 +3,7 @@ "addedContributionMessage": { "commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.", "subject": "Gradido: Message about your common good contribution", - "toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My Contributions to the Common Good” tab!" + "toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!" }, "accountActivation": { "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", @@ -25,6 +25,11 @@ "contributionAmount": "Amount: {contributionAmount} GDD", "subject": "Gradido: Your common good contribution was confirmed" }, + "contributionRejected": { + "commonGoodContributionRejected": "your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.", + "subject": "Gradido: Your common good contribution was rejected", + "toSeeContributionsAndMessages": "To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!" + }, "general": { "decimalSeparator": ".", "helloName": "Hello {firstName} {lastName}", From ae65af9df801b56bdc67b8a95544d2b9e118d2c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 16:10:41 +0100 Subject: [PATCH 18/41] Test 'sendContributionRejectedEmail' --- backend/src/emails/sendEmailVariants.test.ts | 75 ++++++++++++++++++- .../graphql/resolver/AdminResolver.test.ts | 3 + 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 1d562ebe9..666fea994 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -9,6 +9,7 @@ import { sendAccountActivationEmail, sendAccountMultiRegistrationEmail, sendContributionConfirmedEmail, + sendContributionRejectedEmail, } from './sendEmailVariants' import { sendEmailTranslated } from './sendEmailTranslated' @@ -99,7 +100,7 @@ describe('sendEmailVariants', () => { 'you have received a message from Bibi Bloxberg regarding your common good contribution “My contribution.”.', ) expect(result.originalMessage.html).toContain( - 'To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My Contributions to the Common Good” tab!', + 'To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!', ) expect(result.originalMessage.html).toContain( 'Link to your account: http://localhost/overview', @@ -329,4 +330,76 @@ describe('sendEmailVariants', () => { }) }) }) + + describe('sendContributionRejectedEmail', () => { + beforeAll(async () => { + result = await sendContributionRejectedEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'contributionRejected', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) + }) + + it('has expected result', () => { + expect(result).toMatchObject({ + envelope: { + from: 'info@gradido.net', + to: ['peter@lustig.de'], + }, + message: expect.any(String), + originalMessage: expect.objectContaining({ + to: 'Peter Lustig ', + from: 'Gradido (nicht antworten) ', + attachments: [], + subject: 'Gradido: Your common good contribution was rejected', + html: expect.any(String), + text: expect.stringContaining('GRADIDO: YOUR COMMON GOOD CONTRIBUTION WAS REJECTED'), + }), + }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain( + 'Gradido: Your common good contribution was rejected', + ) + expect(result.originalMessage.html).toContain( + '>Gradido: Your common good contribution was rejected', + ) + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'your public good contribution “My contribution.” was rejected by Bibi Bloxberg.', + ) + expect(result.originalMessage.html).toContain( + 'To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!', + ) + expect(result.originalMessage.html).toContain( + 'Link to your account: http://localhost/overview', + ) + expect(result.originalMessage.html).toContain('Please do not reply to this email!') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') + }) + }) + }) }) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index d5b4cade5..5c9e3250e 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -39,6 +39,7 @@ import { User } from '@entity/User' import { // sendAccountActivationEmail, sendContributionConfirmedEmail, + // sendContributionRejectedEmail, } from '@/emails/sendEmailVariants' import Decimal from 'decimal.js-light' import { Contribution } from '@entity/Contribution' @@ -58,6 +59,8 @@ jest.mock('@/emails/sendEmailVariants', () => { sendContributionConfirmedEmail: jest.fn((a) => originalModule.sendContributionConfirmedEmail(a), ), + // TODO: test the call of … + // sendContributionRejectedEmail: jest.fn((a) => originalModule.sendContributionRejectedEmail(a)), } }) From 3b030e461270b1e2f48f534b9c7020207191a115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 16:12:48 +0100 Subject: [PATCH 19/41] Remove old untranslated email 'sendContributionRejectedEmail' --- .../sendContributionRejectedEmail.test.ts | 38 ------------------- .../mailer/sendContributionRejectedEmail.ts | 26 ------------- .../src/mailer/text/contributionRejected.ts | 28 -------------- 3 files changed, 92 deletions(-) delete mode 100644 backend/src/mailer/sendContributionRejectedEmail.test.ts delete mode 100644 backend/src/mailer/sendContributionRejectedEmail.ts delete mode 100644 backend/src/mailer/text/contributionRejected.ts diff --git a/backend/src/mailer/sendContributionRejectedEmail.test.ts b/backend/src/mailer/sendContributionRejectedEmail.test.ts deleted file mode 100644 index be41ff15f..000000000 --- a/backend/src/mailer/sendContributionRejectedEmail.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Decimal from 'decimal.js-light' -import { sendContributionRejectedEmail } from './sendContributionRejectedEmail' -import { sendEMail } from './sendEMail' - -jest.mock('./sendEMail', () => { - return { - __esModule: true, - sendEMail: jest.fn(), - } -}) - -describe('sendContributionConfirmedEmail', () => { - beforeEach(async () => { - await sendContributionRejectedEmail({ - senderFirstName: 'Peter', - senderLastName: 'Lustig', - recipientFirstName: 'Bibi', - recipientLastName: 'Bloxberg', - recipientEmail: 'bibi@bloxberg.de', - contributionMemo: 'Vielen herzlichen Dank für den neuen Hexenbesen!', - contributionAmount: new Decimal(200.0), - overviewURL: 'http://localhost/overview', - }) - }) - - it('calls sendEMail', () => { - expect(sendEMail).toBeCalledWith({ - to: 'Bibi Bloxberg ', - subject: 'Dein Gemeinwohl-Beitrag wurde abgelehnt', - text: - expect.stringContaining('Hallo Bibi Bloxberg') && - expect.stringContaining( - 'dein Gemeinwohl-Beitrag "Vielen herzlichen Dank für den neuen Hexenbesen!" wurde von Peter Lustig abgelehnt.', - ) && - expect.stringContaining('Link zu deinem Konto: http://localhost/overview'), - }) - }) -}) diff --git a/backend/src/mailer/sendContributionRejectedEmail.ts b/backend/src/mailer/sendContributionRejectedEmail.ts deleted file mode 100644 index 9edb5ba2a..000000000 --- a/backend/src/mailer/sendContributionRejectedEmail.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { backendLogger as logger } from '@/server/logger' -import Decimal from 'decimal.js-light' -import { sendEMail } from './sendEMail' -import { contributionRejected } from './text/contributionRejected' - -export const sendContributionRejectedEmail = (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - recipientEmail: string - contributionMemo: string - contributionAmount: Decimal - overviewURL: string -}): Promise => { - logger.info( - `sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>, - subject=${contributionRejected.de.subject}, - text=${contributionRejected.de.text(data)}`, - ) - return sendEMail({ - to: `${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>`, - subject: contributionRejected.de.subject, - text: contributionRejected.de.text(data), - }) -} diff --git a/backend/src/mailer/text/contributionRejected.ts b/backend/src/mailer/text/contributionRejected.ts deleted file mode 100644 index ff52c7b5a..000000000 --- a/backend/src/mailer/text/contributionRejected.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Decimal from 'decimal.js-light' - -export const contributionRejected = { - de: { - subject: 'Dein Gemeinwohl-Beitrag wurde abgelehnt', - text: (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - contributionMemo: string - contributionAmount: Decimal - overviewURL: string - }): string => - `Hallo ${data.recipientFirstName} ${data.recipientLastName}, - -dein Gemeinwohl-Beitrag "${data.contributionMemo}" wurde von ${data.senderFirstName} ${data.senderLastName} abgelehnt. - -Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü "Gemeinschaft" auf den Tab "Meine Beiträge zum Gemeinwohl"! - -Link zu deinem Konto: ${data.overviewURL} - -Bitte antworte nicht auf diese E-Mail! - -Liebe Grüße -dein Gradido-Team`, - }, -} From 44f2e6b06a03a049415f61900bb9b01b6eb7b225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 20:15:55 +0100 Subject: [PATCH 20/41] Move 'getTimeDurationObject' and 'printTimeDuration' into a separate file --- backend/src/graphql/resolver/AdminResolver.ts | 3 ++- .../src/graphql/resolver/UserResolver.test.ts | 3 ++- backend/src/graphql/resolver/UserResolver.ts | 18 +----------------- backend/src/util/time.ts | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 19 deletions(-) create mode 100644 backend/src/util/time.ts diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 993fe8c3b..574377dce 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -39,7 +39,8 @@ import { Decay } from '@model/Decay' import Paginated from '@arg/Paginated' import TransactionLinkFilters from '@arg/TransactionLinkFilters' import { Order } from '@enum/Order' -import { findUserByEmail, activationLink, getTimeDurationObject } from './UserResolver' +import { getTimeDurationObject } from '@/util/time' +import { findUserByEmail, activationLink } from './UserResolver' import { sendAddedContributionMessageEmail, sendAccountActivationEmail, diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index dac45c289..2be2361e9 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -3,6 +3,7 @@ import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers' import { logger, i18n as localization } from '@test/testSetup' +import { printTimeDuration } from '@/util/time' import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { @@ -24,7 +25,7 @@ import { sendAccountMultiRegistrationEmail, } from '@/emails/sendEmailVariants' import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' -import { printTimeDuration, activationLink } from './UserResolver' +import { activationLink } from './UserResolver' import { contributionLinkFactory } from '@/seeds/factory/contributionLink' import { transactionLinkFactory } from '@/seeds/factory/transactionLink' import { ContributionLink } from '@model/ContributionLink' diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index ddfa94eab..4ec4af166 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -9,6 +9,7 @@ import { User } from '@model/User' import { User as DbUser } from '@entity/User' import { UserContact as DbUserContact } from '@entity/UserContact' import { communityDbUser } from '@/util/communityUser' +import { getTimeDurationObject, printTimeDuration } from '@/util/time' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' import { ContributionLink as dbContributionLink } from '@entity/ContributionLink' import { encode } from '@/auth/JWT' @@ -916,20 +917,3 @@ const canResendOptIn = (optIn: LoginEmailOptIn): boolean => { const canEmailResend = (updatedAt: Date): boolean => { return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME) } - -export const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => { - if (time > 60) { - return { - hours: Math.floor(time / 60), - minutes: time % 60, - } - } - return { minutes: time } -} - -export const printTimeDuration = (duration: number): string => { - const time = getTimeDurationObject(duration) - const result = time.minutes > 0 ? `${time.minutes} minutes` : '' - if (time.hours) return `${time.hours} hours` + (result !== '' ? ` and ${result}` : '') - return result -} diff --git a/backend/src/util/time.ts b/backend/src/util/time.ts new file mode 100644 index 000000000..d429c8d6b --- /dev/null +++ b/backend/src/util/time.ts @@ -0,0 +1,16 @@ +export const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => { + if (time > 60) { + return { + hours: Math.floor(time / 60), + minutes: time % 60, + } + } + return { minutes: time } +} + +export const printTimeDuration = (duration: number): string => { + const time = getTimeDurationObject(duration) + const result = time.minutes > 0 ? `${time.minutes} minutes` : '' + if (time.hours) return `${time.hours} hours` + (result !== '' ? ` and ${result}` : '') + return result +} From a652654be76a49de237e5c35fd8d942c1bd98945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 22:09:36 +0100 Subject: [PATCH 21/41] Refactor 'sendResetPasswordEmail' email to HTML and translatable --- backend/src/emails/accountActivation/html.pug | 2 +- backend/src/emails/resetPassword/html.pug | 20 +++++++++++++++++ backend/src/emails/resetPassword/subject.pug | 1 + backend/src/emails/sendEmailVariants.ts | 22 +++++++++++++++++++ backend/src/graphql/resolver/UserResolver.ts | 9 ++++---- backend/src/locales/de.json | 8 ++++++- backend/src/locales/en.json | 12 +++++++--- 7 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 backend/src/emails/resetPassword/html.pug create mode 100644 backend/src/emails/resetPassword/subject.pug diff --git a/backend/src/emails/accountActivation/html.pug b/backend/src/emails/accountActivation/html.pug index 9c631c960..f283e941e 100644 --- a/backend/src/emails/accountActivation/html.pug +++ b/backend/src/emails/accountActivation/html.pug @@ -11,7 +11,7 @@ html(lang=locale) br a(href=activationLink) #{activationLink} br - span= t('emails.accountActivation.orCopyLink') + span= t('emails.general.orCopyLink') p= t('emails.accountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes }) br a(href=resendLink) #{resendLink} diff --git a/backend/src/emails/resetPassword/html.pug b/backend/src/emails/resetPassword/html.pug new file mode 100644 index 000000000..a3ced9a75 --- /dev/null +++ b/backend/src/emails/resetPassword/html.pug @@ -0,0 +1,20 @@ +doctype html +html(lang=locale) + head + title= t('emails.resetPassword.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.resetPassword.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.resetPassword.youOrSomeoneResetPassword') + p= t('emails.resetPassword.pleaseClickLink') + br + a(href=resetLink) #{resetLink} + br + span= t('emails.general.orCopyLink') + p= t('emails.resetPassword.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes }) + br + a(href=resendLink) #{resendLink} + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/resetPassword/subject.pug b/backend/src/emails/resetPassword/subject.pug new file mode 100644 index 000000000..3d2b1f00f --- /dev/null +++ b/backend/src/emails/resetPassword/subject.pug @@ -0,0 +1 @@ += t('emails.resetPassword.subject') \ No newline at end of file diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index 1779cc297..e8f208eb0 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -124,3 +124,25 @@ export const sendContributionRejectedEmail = (data: { }, }) } + +export const sendResetPasswordEmail = (data: { + firstName: string + lastName: string + email: string + language: string + resetLink: string + timeDurationObject: Record +}): Promise | null> => { + return sendEmailTranslated({ + receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, + template: 'resetPassword', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + resetLink: data.resetLink, + timeDurationObject: data.timeDurationObject, + resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, + }, + }) +} diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 4ec4af166..6bdddb7b7 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -18,10 +18,10 @@ import UnsecureLoginArgs from '@arg/UnsecureLoginArgs' import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs' import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware' import { OptInType } from '@enum/OptInType' -import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail' import { sendAccountActivationEmail, sendAccountMultiRegistrationEmail, + sendResetPasswordEmail, } from '@/emails/sendEmailVariants' import { klicktippSignIn } from '@/apis/KlicktippController' import { RIGHTS } from '@/auth/RIGHTS' @@ -574,12 +574,13 @@ export class UserResolver { // optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD) logger.info(`optInCode for ${email}=${dbUserContact}`) // eslint-disable-next-line @typescript-eslint/no-unused-vars - const emailSent = await sendResetPasswordEmailMailer({ - link: activationLink(dbUserContact.emailVerificationCode), + const emailSent = await sendResetPasswordEmail({ firstName: user.firstName, lastName: user.lastName, email, - duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME), + language: user.language, + resetLink: activationLink(dbUserContact.emailVerificationCode), + timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), }) /* uncomment this, when you need the activation link on the console */ diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index 277728439..fd9b74662 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -9,7 +9,6 @@ "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", "emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.", "pleaseClickLink": "Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:", - "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "subject": "Gradido: E-Mail Überprüfung" }, "accountMultiRegistration": { @@ -30,10 +29,17 @@ "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde abgelehnt", "toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!" }, + "resetPassword": { + "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", + "pleaseClickLink": "Wenn du es warst, klicke bitte auf den Link:", + "subject": "Gradido: Passwort zurücksetzen", + "youOrSomeoneResetPassword": "du, oder jemand anderes, hast für dieses Konto ein Zurücksetzen des Passworts angefordert." + }, "general": { "decimalSeparator": ",", "helloName": "Hallo {firstName} {lastName},", "linkToYourAccount": "Link zu deinem Konto:", + "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "pleaseDoNotReply": "Bitte antworte nicht auf diese E-Mail!", "sincerelyYours": "Liebe Grüße", "yourGradidoTeam": "dein Gradido-Team" diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index d4292b05f..c688102c6 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -9,7 +9,6 @@ "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", "emailRegistered": "Your email address has just been registered with Gradido.", "pleaseClickLink": "Please click on this link to complete the registration and activate your Gradido account:", - "orCopyLink": "or copy the link above into your browser window.", "subject": "Gradido: Email Verification" }, "accountMultiRegistration": { @@ -21,19 +20,26 @@ "subject": "Gradido: Try To Register Again With Your Email" }, "contributionConfirmed": { - "commonGoodContributionConfirmed": "your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.", + "commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.", "contributionAmount": "Amount: {contributionAmount} GDD", "subject": "Gradido: Your common good contribution was confirmed" }, "contributionRejected": { - "commonGoodContributionRejected": "your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.", + "commonGoodContributionRejected": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.", "subject": "Gradido: Your common good contribution was rejected", "toSeeContributionsAndMessages": "To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!" }, + "resetPassword": { + "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", + "pleaseClickLink": "If it was you, please click on the link:", + "subject": "Gradido: Reset password", + "youOrSomeoneResetPassword": "You, or someone else, requested a password reset for this account." + }, "general": { "decimalSeparator": ".", "helloName": "Hello {firstName} {lastName}", "linkToYourAccount": "Link to your account:", + "orCopyLink": "or copy the link above into your browser window.", "pleaseDoNotReply": "Please do not reply to this email!", "sincerelyYours": "Kind regards,", "yourGradidoTeam": "your Gradido team" From 2d634b4111cc789e80cfd0bf1546887c03b1b9f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 22:10:07 +0100 Subject: [PATCH 22/41] Test 'sendResetPasswordEmail' --- backend/src/emails/sendEmailVariants.test.ts | 77 ++++++++++++++++++- .../src/graphql/resolver/UserResolver.test.ts | 18 ++--- 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 666fea994..dd5bb68e9 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -10,6 +10,7 @@ import { sendAccountMultiRegistrationEmail, sendContributionConfirmedEmail, sendContributionRejectedEmail, + sendResetPasswordEmail, } from './sendEmailVariants' import { sendEmailTranslated } from './sendEmailTranslated' @@ -319,7 +320,7 @@ describe('sendEmailVariants', () => { ) expect(result.originalMessage.html).toContain('Hello Peter Lustig') expect(result.originalMessage.html).toContain( - 'your public good contribution “My contribution.” has just been confirmed by Bibi Bloxberg and credited to your Gradido account.', + 'Your public good contribution “My contribution.” has just been confirmed by Bibi Bloxberg and credited to your Gradido account.', ) expect(result.originalMessage.html).toContain('Amount: 23.54 GDD') expect(result.originalMessage.html).toContain( @@ -389,7 +390,7 @@ describe('sendEmailVariants', () => { ) expect(result.originalMessage.html).toContain('Hello Peter Lustig') expect(result.originalMessage.html).toContain( - 'your public good contribution “My contribution.” was rejected by Bibi Bloxberg.', + 'Your public good contribution “My contribution.” was rejected by Bibi Bloxberg.', ) expect(result.originalMessage.html).toContain( 'To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!', @@ -402,4 +403,76 @@ describe('sendEmailVariants', () => { }) }) }) + + describe('sendResetPasswordEmail', () => { + beforeAll(async () => { + result = await sendResetPasswordEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + resetLink: 'http://localhost/reset-password/3762660021544901417', + timeDurationObject: { hours: 23, minutes: 30 }, + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'resetPassword', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + resetLink: 'http://localhost/reset-password/3762660021544901417', + timeDurationObject: { hours: 23, minutes: 30 }, + resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, + }, + }) + }) + + it('has expected result', () => { + expect(result).toMatchObject({ + envelope: { + from: 'info@gradido.net', + to: ['peter@lustig.de'], + }, + message: expect.any(String), + originalMessage: expect.objectContaining({ + to: 'Peter Lustig ', + from: 'Gradido (nicht antworten) ', + attachments: [], + subject: 'Gradido: Reset password', + html: expect.any(String), + text: expect.stringContaining('GRADIDO: RESET PASSWORD'), + }), + }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('Gradido: Reset password') + expect(result.originalMessage.html).toContain('>Gradido: Reset password') + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'You, or someone else, requested a password reset for this account.', + ) + expect(result.originalMessage.html).toContain('If it was you, please click on the link:') + expect(result.originalMessage.html).toContain( + 'http://localhost/reset-password/3762660021544901417', + ) + expect(result.originalMessage.html).toContain( + 'or copy the link above into your browser window.', + ) + expect(result.originalMessage.html).toContain( + 'The link has a validity of 23 hours and 30 minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:', + ) + expect(result.originalMessage.html).toContain( + `${CONFIG.EMAIL_LINK_FORGOTPASSWORD}`, + ) + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') + }) + }) + }) }) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 2be2361e9..ee88e58cc 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -23,8 +23,8 @@ import CONFIG from '@/config' import { sendAccountActivationEmail, sendAccountMultiRegistrationEmail, + sendResetPasswordEmail, } from '@/emails/sendEmailVariants' -import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' import { activationLink } from './UserResolver' import { contributionLinkFactory } from '@/seeds/factory/contributionLink' import { transactionLinkFactory } from '@/seeds/factory/transactionLink' @@ -53,13 +53,7 @@ jest.mock('@/emails/sendEmailVariants', () => { sendAccountMultiRegistrationEmail: jest.fn((a) => originalModule.sendAccountMultiRegistrationEmail(a), ), - } -}) - -jest.mock('@/mailer/sendResetPasswordEmail', () => { - return { - __esModule: true, - sendResetPasswordEmail: jest.fn(), + sendResetPasswordEmail: jest.fn((a) => originalModule.sendResetPasswordEmail(a)), } }) @@ -857,11 +851,15 @@ describe('UserResolver', () => { it('sends reset password email', () => { expect(sendResetPasswordEmail).toBeCalledWith({ - link: activationLink(emailContact.emailVerificationCode), firstName: 'Bibi', lastName: 'Bloxberg', email: 'bibi@bloxberg.de', - duration: expect.any(String), + language: 'de', + resetLink: activationLink(emailContact.emailVerificationCode), + timeDurationObject: expect.objectContaining({ + hours: expect.any(Number), + minutes: expect.any(Number), + }), }) }) From b68f550b6cc8952e5c91d6de2e8cd82fee8fec1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 22:16:18 +0100 Subject: [PATCH 23/41] Remove old untranslated email 'sendResetPasswordEmail' --- .../src/mailer/sendResetPasswordEmail.test.ts | 32 ------------------- backend/src/mailer/sendResetPasswordEmail.ts | 17 ---------- backend/src/mailer/text/resetPassword.ts | 30 ----------------- 3 files changed, 79 deletions(-) delete mode 100644 backend/src/mailer/sendResetPasswordEmail.test.ts delete mode 100644 backend/src/mailer/sendResetPasswordEmail.ts delete mode 100644 backend/src/mailer/text/resetPassword.ts diff --git a/backend/src/mailer/sendResetPasswordEmail.test.ts b/backend/src/mailer/sendResetPasswordEmail.test.ts deleted file mode 100644 index 94f69cf8b..000000000 --- a/backend/src/mailer/sendResetPasswordEmail.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { sendResetPasswordEmail } from './sendResetPasswordEmail' -import { sendEMail } from './sendEMail' - -jest.mock('./sendEMail', () => { - return { - __esModule: true, - sendEMail: jest.fn(), - } -}) - -describe('sendResetPasswordEmail', () => { - beforeEach(async () => { - await sendResetPasswordEmail({ - link: 'resetLink', - firstName: 'Peter', - lastName: 'Lustig', - email: 'peter@lustig.de', - duration: '23 hours and 30 minutes', - }) - }) - - it('calls sendEMail', () => { - expect(sendEMail).toBeCalledWith({ - to: `Peter Lustig `, - subject: 'Gradido: Passwort zurücksetzen', - text: - expect.stringContaining('Hallo Peter Lustig') && - expect.stringContaining('resetLink') && - expect.stringContaining('23 Stunden und 30 Minuten'), - }) - }) -}) diff --git a/backend/src/mailer/sendResetPasswordEmail.ts b/backend/src/mailer/sendResetPasswordEmail.ts deleted file mode 100644 index d9770f940..000000000 --- a/backend/src/mailer/sendResetPasswordEmail.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { sendEMail } from './sendEMail' -import { resetPassword } from './text/resetPassword' -import CONFIG from '@/config' - -export const sendResetPasswordEmail = (data: { - link: string - firstName: string - lastName: string - email: string - duration: string -}): Promise => { - return sendEMail({ - to: `${data.firstName} ${data.lastName} <${data.email}>`, - subject: resetPassword.de.subject, - text: resetPassword.de.text({ ...data, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD }), - }) -} diff --git a/backend/src/mailer/text/resetPassword.ts b/backend/src/mailer/text/resetPassword.ts deleted file mode 100644 index ff660f76e..000000000 --- a/backend/src/mailer/text/resetPassword.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const resetPassword = { - de: { - subject: 'Gradido: Passwort zurücksetzen', - text: (data: { - link: string - firstName: string - lastName: string - email: string - duration: string - resendLink: string - }): string => - `Hallo ${data.firstName} ${data.lastName}, - -Du oder jemand anderes hat für dieses Konto ein Zurücksetzen des Passworts angefordert. -Wenn du es warst, klicke bitte auf den Link: ${data.link} -oder kopiere den obigen Link in Dein Browserfenster. - -Der Link hat eine Gültigkeit von ${data.duration - .replace('hours', 'Stunden') - .replace('minutes', 'Minuten') - .replace( - ' and ', - ' und ', - )}. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst: -${data.resendLink} - -Mit freundlichen Grüßen, -dein Gradido-Team`, - }, -} From 4b097f6f66ddd35a47fbe76ef750365843377269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Sat, 26 Nov 2022 12:59:06 +0100 Subject: [PATCH 24/41] Add line ends to all 'subject.pug' files --- backend/src/emails/accountActivation/subject.pug | 2 +- backend/src/emails/accountMultiRegistration/subject.pug | 2 +- backend/src/emails/addedContributionMessage/subject.pug | 2 +- backend/src/emails/contributionConfirmed/subject.pug | 2 +- backend/src/emails/contributionRejected/subject.pug | 2 +- backend/src/emails/resetPassword/subject.pug | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/emails/accountActivation/subject.pug b/backend/src/emails/accountActivation/subject.pug index 378053bbf..81749a38e 100644 --- a/backend/src/emails/accountActivation/subject.pug +++ b/backend/src/emails/accountActivation/subject.pug @@ -1 +1 @@ -= t('emails.accountActivation.subject') \ No newline at end of file += t('emails.accountActivation.subject') diff --git a/backend/src/emails/accountMultiRegistration/subject.pug b/backend/src/emails/accountMultiRegistration/subject.pug index 322f07c78..fb130f0e4 100644 --- a/backend/src/emails/accountMultiRegistration/subject.pug +++ b/backend/src/emails/accountMultiRegistration/subject.pug @@ -1 +1 @@ -= t('emails.accountMultiRegistration.subject') \ No newline at end of file += t('emails.accountMultiRegistration.subject') diff --git a/backend/src/emails/addedContributionMessage/subject.pug b/backend/src/emails/addedContributionMessage/subject.pug index 8620725f8..4ac85fa23 100644 --- a/backend/src/emails/addedContributionMessage/subject.pug +++ b/backend/src/emails/addedContributionMessage/subject.pug @@ -1 +1 @@ -= t('emails.addedContributionMessage.subject') \ No newline at end of file += t('emails.addedContributionMessage.subject') diff --git a/backend/src/emails/contributionConfirmed/subject.pug b/backend/src/emails/contributionConfirmed/subject.pug index 7e74a77c6..c5bd41421 100644 --- a/backend/src/emails/contributionConfirmed/subject.pug +++ b/backend/src/emails/contributionConfirmed/subject.pug @@ -1 +1 @@ -= t('emails.contributionConfirmed.subject') \ No newline at end of file += t('emails.contributionConfirmed.subject') diff --git a/backend/src/emails/contributionRejected/subject.pug b/backend/src/emails/contributionRejected/subject.pug index cdaae4157..40a7622b8 100644 --- a/backend/src/emails/contributionRejected/subject.pug +++ b/backend/src/emails/contributionRejected/subject.pug @@ -1 +1 @@ -= t('emails.contributionRejected.subject') \ No newline at end of file += t('emails.contributionRejected.subject') diff --git a/backend/src/emails/resetPassword/subject.pug b/backend/src/emails/resetPassword/subject.pug index 3d2b1f00f..21f277316 100644 --- a/backend/src/emails/resetPassword/subject.pug +++ b/backend/src/emails/resetPassword/subject.pug @@ -1 +1 @@ -= t('emails.resetPassword.subject') \ No newline at end of file += t('emails.resetPassword.subject') From 224613f6d44b53090b6e2de464568eba9c7548e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Sat, 26 Nov 2022 15:42:44 +0100 Subject: [PATCH 25/41] Refactor 'sendTransactionLinkRedeemedEmail' email to HTML and translatable --- .../src/emails/contributionConfirmed/html.pug | 2 +- backend/src/emails/sendEmailVariants.ts | 38 +++++++++++++++---- .../emails/transactionLinkRedeemed/html.pug | 19 ++++++++++ .../transactionLinkRedeemed/subject.pug | 1 + .../graphql/resolver/TransactionResolver.ts | 14 +++---- backend/src/locales/de.json | 26 ++++++++----- backend/src/locales/en.json | 26 ++++++++----- backend/src/util/utilities.ts | 9 +++++ 8 files changed, 101 insertions(+), 34 deletions(-) create mode 100644 backend/src/emails/transactionLinkRedeemed/html.pug create mode 100644 backend/src/emails/transactionLinkRedeemed/subject.pug diff --git a/backend/src/emails/contributionConfirmed/html.pug b/backend/src/emails/contributionConfirmed/html.pug index e60e6c700..32626b147 100644 --- a/backend/src/emails/contributionConfirmed/html.pug +++ b/backend/src/emails/contributionConfirmed/html.pug @@ -7,7 +7,7 @@ html(lang=locale) #container.col p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) p= t('emails.contributionConfirmed.commonGoodContributionConfirmed', { senderFirstName, senderLastName, contributionMemo }) - p= t('emails.contributionConfirmed.contributionAmount', { contributionAmount }) + p= t('emails.general.amountGDD', { amountGDD: contributionAmount }) p= t('emails.general.linkToYourAccount') span= " " a(href=overviewURL) #{overviewURL} diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index e8f208eb0..953ac4af5 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -1,6 +1,6 @@ -import i18n from 'i18n' import Decimal from 'decimal.js-light' import CONFIG from '@/config' +import { decimalSeparatorByLanguage } from '@/util/utilities' import { sendEmailTranslated } from './sendEmailTranslated' export const sendAddedContributionMessageEmail = (data: { @@ -79,12 +79,6 @@ export const sendContributionConfirmedEmail = (data: { contributionMemo: string contributionAmount: Decimal }): Promise | null> => { - const rememberLocaleToRestore = i18n.getLocale() - i18n.setLocale(data.language) - const contributionAmount = data.contributionAmount - .toFixed(2) - .replace('.', i18n.__('emails.general.decimalSeparator')) - i18n.setLocale(rememberLocaleToRestore) return sendEmailTranslated({ receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, template: 'contributionConfirmed', @@ -95,7 +89,7 @@ export const sendContributionConfirmedEmail = (data: { senderFirstName: data.senderFirstName, senderLastName: data.senderLastName, contributionMemo: data.contributionMemo, - contributionAmount, + contributionAmount: decimalSeparatorByLanguage(data.contributionAmount, data.language), overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, }, }) @@ -146,3 +140,31 @@ export const sendResetPasswordEmail = (data: { }, }) } + +export const sendTransactionLinkRedeemedEmail = (data: { + firstName: string + lastName: string + email: string + language: string + senderFirstName: string + senderLastName: string + senderEmail: string + transactionMemo: string + transactionAmount: Decimal +}): Promise | null> => { + return sendEmailTranslated({ + receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, + template: 'transactionLinkRedeemed', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + senderFirstName: data.senderFirstName, + senderLastName: data.senderLastName, + senderEmail: data.senderEmail, + transactionMemo: data.transactionMemo, + transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language), + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) +} diff --git a/backend/src/emails/transactionLinkRedeemed/html.pug b/backend/src/emails/transactionLinkRedeemed/html.pug new file mode 100644 index 000000000..f15a278c9 --- /dev/null +++ b/backend/src/emails/transactionLinkRedeemed/html.pug @@ -0,0 +1,19 @@ +doctype html +html(lang=locale) + head + title= t('emails.transactionLinkRedeemed.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.transactionLinkRedeemed.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.transactionLinkRedeemed.hasRedeemedYourLink', { senderFirstName, senderLastName, senderEmail }) + p= t('emails.general.amountGDD', { amountGDD: transactionAmount }) + br + span= t('emails.transactionLinkRedeemed.memo', { transactionMemo }) + p= t('emails.transactionLinkRedeemed.detailsYouFindOnLinkToYourAccount') + span= " " + a(href=overviewURL) #{overviewURL} + p= t('emails.general.pleaseDoNotReply') + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/transactionLinkRedeemed/subject.pug b/backend/src/emails/transactionLinkRedeemed/subject.pug new file mode 100644 index 000000000..6f4f74f04 --- /dev/null +++ b/backend/src/emails/transactionLinkRedeemed/subject.pug @@ -0,0 +1 @@ += t('emails.transactionLinkRedeemed.subject') diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index f0fb2f452..d83d99132 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -36,7 +36,7 @@ import Decimal from 'decimal.js-light' import { BalanceResolver } from './BalanceResolver' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { findUserByEmail } from './UserResolver' -import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed' +import { sendTransactionLinkRedeemedEmail } from '@/emails/sendEmailVariants' import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event' import { eventProtocol } from '@/event/EventProtocolEmitter' import { Decay } from '../model/Decay' @@ -182,15 +182,15 @@ export const executeTransaction = async ( }) if (transactionLink) { await sendTransactionLinkRedeemedEmail({ + firstName: sender.firstName, + lastName: sender.lastName, + email: sender.emailContact.email, + language: sender.language, senderFirstName: recipient.firstName, senderLastName: recipient.lastName, - recipientFirstName: sender.firstName, - recipientLastName: sender.lastName, - email: sender.emailContact.email, senderEmail: recipient.emailContact.email, - amount, - memo, - overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + transactionAmount: amount, + transactionMemo: memo, }) } logger.info(`finished executeTransaction successfully`) diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index fd9b74662..de1e657fe 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -21,7 +21,6 @@ }, "contributionConfirmed": { "commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.", - "contributionAmount": "Betrag: {contributionAmount} GDD", "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt" }, "contributionRejected": { @@ -29,20 +28,29 @@ "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde abgelehnt", "toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!" }, - "resetPassword": { - "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", - "pleaseClickLink": "Wenn du es warst, klicke bitte auf den Link:", - "subject": "Gradido: Passwort zurücksetzen", - "youOrSomeoneResetPassword": "du, oder jemand anderes, hast für dieses Konto ein Zurücksetzen des Passworts angefordert." - }, "general": { - "decimalSeparator": ",", + "amountGDD": "Betrag: {amountGDD} GDD", "helloName": "Hallo {firstName} {lastName},", "linkToYourAccount": "Link zu deinem Konto:", "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "pleaseDoNotReply": "Bitte antworte nicht auf diese E-Mail!", "sincerelyYours": "Liebe Grüße", "yourGradidoTeam": "dein Gradido-Team" + }, + "resetPassword": { + "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", + "pleaseClickLink": "Wenn du es warst, klicke bitte auf den Link:", + "subject": "Gradido: Passwort zurücksetzen", + "youOrSomeoneResetPassword": "du, oder jemand anderes, hast für dieses Konto ein Zurücksetzen des Passworts angefordert." + }, + "transactionLinkRedeemed": { + "detailsYouFindOnLinkToYourAccount": "Details zur Transaktion findest du in deinem Gradido-Konto:", + "hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) hat soeben deinen Link eingelöst.", + "memo": "Memo: {transactionMemo}", + "subject": "Gradido: Dein Gradido-Link wurde eingelöst" } + }, + "general": { + "decimalSeparator": "," } -} \ No newline at end of file +} diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index c688102c6..34cf2512f 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -21,7 +21,6 @@ }, "contributionConfirmed": { "commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.", - "contributionAmount": "Amount: {contributionAmount} GDD", "subject": "Gradido: Your common good contribution was confirmed" }, "contributionRejected": { @@ -29,20 +28,29 @@ "subject": "Gradido: Your common good contribution was rejected", "toSeeContributionsAndMessages": "To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!" }, - "resetPassword": { - "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", - "pleaseClickLink": "If it was you, please click on the link:", - "subject": "Gradido: Reset password", - "youOrSomeoneResetPassword": "You, or someone else, requested a password reset for this account." - }, "general": { - "decimalSeparator": ".", + "amountGDD": "Amount: {amountGDD} GDD", "helloName": "Hello {firstName} {lastName}", "linkToYourAccount": "Link to your account:", "orCopyLink": "or copy the link above into your browser window.", "pleaseDoNotReply": "Please do not reply to this email!", "sincerelyYours": "Kind regards,", "yourGradidoTeam": "your Gradido team" + }, + "resetPassword": { + "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", + "pleaseClickLink": "If it was you, please click on the link:", + "subject": "Gradido: Reset password", + "youOrSomeoneResetPassword": "You, or someone else, requested a password reset for this account." + }, + "transactionLinkRedeemed": { + "detailsYouFindOnLinkToYourAccount": "You can find transaction details in your Gradido account:", + "hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) has just redeemed your link.", + "memo": "Memo: {transactionMemo}", + "subject": "Gradido: Your Gradido link has been redeemed" } + }, + "general": { + "decimalSeparator": "." } -} \ No newline at end of file +} diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index 65214ebb5..f24def721 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -1,4 +1,5 @@ import Decimal from 'decimal.js-light' +import i18n from 'i18n' export const objectValuesToArray = (obj: { [x: string]: string }): Array => { return Object.keys(obj).map(function (key) { @@ -15,3 +16,11 @@ export const decimalAddition = (a: Decimal, b: Decimal): Decimal => { export const decimalSubtraction = (a: Decimal, b: Decimal): Decimal => { return a.minus(b.toString()) } + +export const decimalSeparatorByLanguage = (a: Decimal, language: string): string => { + const rememberLocaleToRestore = i18n.getLocale() + i18n.setLocale(language) + const result = a.toFixed(2).replace('.', i18n.__('general.decimalSeparator')) + i18n.setLocale(rememberLocaleToRestore) + return result +} From 699daa8eec5c1851ab07ef7b6e890b370f88be6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Sat, 26 Nov 2022 15:43:44 +0100 Subject: [PATCH 26/41] Test 'sendTransactionLinkRedeemedEmail' --- backend/src/emails/sendEmailVariants.test.ts | 82 +++++++++++++++++++- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index dd5bb68e9..858b6426b 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -11,6 +11,7 @@ import { sendContributionConfirmedEmail, sendContributionRejectedEmail, sendResetPasswordEmail, + sendTransactionLinkRedeemedEmail, } from './sendEmailVariants' import { sendEmailTranslated } from './sendEmailTranslated' @@ -104,7 +105,7 @@ describe('sendEmailVariants', () => { 'To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!', ) expect(result.originalMessage.html).toContain( - 'Link to your account: http://localhost/overview', + `Link to your account: ${CONFIG.EMAIL_LINK_OVERVIEW}`, ) expect(result.originalMessage.html).toContain('Please do not reply to this email!') expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') @@ -324,7 +325,7 @@ describe('sendEmailVariants', () => { ) expect(result.originalMessage.html).toContain('Amount: 23.54 GDD') expect(result.originalMessage.html).toContain( - 'Link to your account: http://localhost/overview', + `Link to your account: ${CONFIG.EMAIL_LINK_OVERVIEW}`, ) expect(result.originalMessage.html).toContain('Please do not reply to this email!') expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') @@ -396,7 +397,7 @@ describe('sendEmailVariants', () => { 'To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!', ) expect(result.originalMessage.html).toContain( - 'Link to your account: http://localhost/overview', + `Link to your account: ${CONFIG.EMAIL_LINK_OVERVIEW}`, ) expect(result.originalMessage.html).toContain('Please do not reply to this email!') expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') @@ -475,4 +476,79 @@ describe('sendEmailVariants', () => { }) }) }) + + describe('sendTransactionLinkRedeemedEmail', () => { + beforeAll(async () => { + result = await sendTransactionLinkRedeemedEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + senderEmail: 'bibi@bloxberg.de', + transactionMemo: 'You deserve it! 🙏🏼', + transactionAmount: new Decimal(17.65), + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'transactionLinkRedeemed', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + senderEmail: 'bibi@bloxberg.de', + transactionMemo: 'You deserve it! 🙏🏼', + transactionAmount: '17.65', + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) + }) + + it('has expected result', () => { + expect(result).toMatchObject({ + envelope: { + from: 'info@gradido.net', + to: ['peter@lustig.de'], + }, + message: expect.any(String), + originalMessage: expect.objectContaining({ + to: 'Peter Lustig ', + from: 'Gradido (nicht antworten) ', + attachments: [], + subject: 'Gradido: Your Gradido link has been redeemed', + html: expect.any(String), + text: expect.stringContaining('GRADIDO: YOUR GRADIDO LINK HAS BEEN REDEEMED'), + }), + }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain( + 'Gradido: Your Gradido link has been redeemed', + ) + expect(result.originalMessage.html).toContain( + '>Gradido: Your Gradido link has been redeemed', + ) + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'Bibi Bloxberg (bibi@bloxberg.de) has just redeemed your link.', + ) + expect(result.originalMessage.html).toContain('Amount: 17.65 GDD') + expect(result.originalMessage.html).toContain('Memo: You deserve it! 🙏🏼') + expect(result.originalMessage.html).toContain( + `You can find transaction details in your Gradido account: ${CONFIG.EMAIL_LINK_OVERVIEW}`, + ) + expect(result.originalMessage.html).toContain('Please do not reply to this email!') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') + }) + }) + }) }) From 2b65c6b2613bab1d4576b07fb73e8c0cdac8c653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Sat, 26 Nov 2022 15:45:41 +0100 Subject: [PATCH 27/41] Remove old untranslated email 'sendTransactionLinkRedeemedEmail' --- .../sendTransactionLinkRedeemed.test.ts | 44 ------------------- .../src/mailer/sendTransactionLinkRedeemed.ts | 28 ------------ .../mailer/text/transactionLinkRedeemed.ts | 33 -------------- 3 files changed, 105 deletions(-) delete mode 100644 backend/src/mailer/sendTransactionLinkRedeemed.test.ts delete mode 100644 backend/src/mailer/sendTransactionLinkRedeemed.ts delete mode 100644 backend/src/mailer/text/transactionLinkRedeemed.ts diff --git a/backend/src/mailer/sendTransactionLinkRedeemed.test.ts b/backend/src/mailer/sendTransactionLinkRedeemed.test.ts deleted file mode 100644 index b56ff40a1..000000000 --- a/backend/src/mailer/sendTransactionLinkRedeemed.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { sendEMail } from './sendEMail' -import Decimal from 'decimal.js-light' -import { sendTransactionLinkRedeemedEmail } from './sendTransactionLinkRedeemed' - -jest.mock('./sendEMail', () => { - return { - __esModule: true, - sendEMail: jest.fn(), - } -}) - -describe('sendTransactionLinkRedeemedEmail', () => { - beforeEach(async () => { - await sendTransactionLinkRedeemedEmail({ - email: 'bibi@bloxberg.de', - senderFirstName: 'Peter', - senderLastName: 'Lustig', - recipientFirstName: 'Bibi', - recipientLastName: 'Bloxberg', - senderEmail: 'peter@lustig.de', - amount: new Decimal(42.0), - memo: 'Vielen Dank dass Du dabei bist', - overviewURL: 'http://localhost/overview', - }) - }) - - it('calls sendEMail', () => { - expect(sendEMail).toBeCalledWith({ - to: `Bibi Bloxberg `, - subject: 'Gradido-Link wurde eingelöst', - text: - expect.stringContaining('Hallo Bibi Bloxberg') && - expect.stringContaining( - 'Peter Lustig (peter@lustig.de) hat soeben deinen Link eingelöst.', - ) && - expect.stringContaining('Betrag: 42,00 GDD,') && - expect.stringContaining('Memo: Vielen Dank dass Du dabei bist') && - expect.stringContaining( - 'Details zur Transaktion findest du in deinem Gradido-Konto: http://localhost/overview', - ) && - expect.stringContaining('Bitte antworte nicht auf diese E-Mail!'), - }) - }) -}) diff --git a/backend/src/mailer/sendTransactionLinkRedeemed.ts b/backend/src/mailer/sendTransactionLinkRedeemed.ts deleted file mode 100644 index a78f3b3c9..000000000 --- a/backend/src/mailer/sendTransactionLinkRedeemed.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { backendLogger as logger } from '@/server/logger' -import Decimal from 'decimal.js-light' -import { sendEMail } from './sendEMail' -import { transactionLinkRedeemed } from './text/transactionLinkRedeemed' - -export const sendTransactionLinkRedeemedEmail = (data: { - email: string - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - senderEmail: string - amount: Decimal - memo: string - overviewURL: string -}): Promise => { - logger.info( - `sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName}, - <${data.email}>, - subject=${transactionLinkRedeemed.de.subject}, - text=${transactionLinkRedeemed.de.text(data)}`, - ) - return sendEMail({ - to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`, - subject: transactionLinkRedeemed.de.subject, - text: transactionLinkRedeemed.de.text(data), - }) -} diff --git a/backend/src/mailer/text/transactionLinkRedeemed.ts b/backend/src/mailer/text/transactionLinkRedeemed.ts deleted file mode 100644 index a63e5d275..000000000 --- a/backend/src/mailer/text/transactionLinkRedeemed.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Decimal from 'decimal.js-light' - -export const transactionLinkRedeemed = { - de: { - subject: 'Gradido-Link wurde eingelöst', - text: (data: { - email: string - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - senderEmail: string - amount: Decimal - memo: string - overviewURL: string - }): string => - `Hallo ${data.recipientFirstName} ${data.recipientLastName}, - -${data.senderFirstName} ${data.senderLastName} (${ - data.senderEmail - }) hat soeben deinen Link eingelöst. - -Betrag: ${data.amount.toFixed(2).replace('.', ',')} GDD, -Memo: ${data.memo} - -Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL} - -Bitte antworte nicht auf diese E-Mail! - -Liebe Grüße -dein Gradido-Team`, - }, -} From 0cccdb56de511b9d16ffc0c3f44ad43e03d3071a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 28 Nov 2022 09:38:19 +0100 Subject: [PATCH 28/41] Test that 'sendEmailTranslated' on 'CONFIG.EMAIL_TEST_MODUS = true' replaces the receiver with by the fake test mail accaount --- .../src/emails/sendEmailTranslated.test.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/backend/src/emails/sendEmailTranslated.test.ts b/backend/src/emails/sendEmailTranslated.test.ts index 28327f779..72c4c508c 100644 --- a/backend/src/emails/sendEmailTranslated.test.ts +++ b/backend/src/emails/sendEmailTranslated.test.ts @@ -107,4 +107,41 @@ describe('sendEmailTranslated', () => { expect(i18n.__).toBeCalled() }) }) + + describe('with email EMAIL_TEST_MODUS true', () => { + beforeEach(async () => { + jest.clearAllMocks() + CONFIG.EMAIL = true + CONFIG.EMAIL_TEST_MODUS = true + result = await sendEmailTranslated({ + receiver: { + to: 'receiver@mail.org', + cc: 'support@gradido.net', + }, + template: 'accountMultiRegistration', + locals: { + locale: 'en', + }, + }) + }) + + it('call of "sendEmailTranslated" with faked "to"', () => { + expect(result).toMatchObject({ + envelope: { + from: CONFIG.EMAIL_SENDER, + to: [CONFIG.EMAIL_TEST_RECEIVER, 'support@gradido.net'], + }, + message: expect.any(String), + originalMessage: expect.objectContaining({ + to: CONFIG.EMAIL_TEST_RECEIVER, + cc: 'support@gradido.net', + from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, + attachments: [], + subject: 'Gradido: Try To Register Again With Your Email', + html: expect.stringContaining('Gradido: Try To Register Again With Your Email'), + text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'), + }), + }) + }) + }) }) From 3903e29e160d48e1adb02dc1cee45000e8d1b346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 28 Nov 2022 10:54:10 +0100 Subject: [PATCH 29/41] Refactor 'sendTransactionReceivedEmail' email to HTML and translatable --- backend/src/emails/sendEmailVariants.ts | 26 +++++++++++++++++++ .../emails/transactionLinkRedeemed/html.pug | 2 +- .../src/emails/transactionReceived/html.pug | 16 ++++++++++++ .../emails/transactionReceived/subject.pug | 1 + .../graphql/resolver/TransactionResolver.ts | 20 +++++++------- backend/src/locales/de.json | 6 ++++- backend/src/locales/en.json | 6 ++++- 7 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 backend/src/emails/transactionReceived/html.pug create mode 100644 backend/src/emails/transactionReceived/subject.pug diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index 953ac4af5..356f95e39 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -168,3 +168,29 @@ export const sendTransactionLinkRedeemedEmail = (data: { }, }) } + +export const sendTransactionReceivedEmail = (data: { + firstName: string + lastName: string + email: string + language: string + senderFirstName: string + senderLastName: string + senderEmail: string + transactionAmount: Decimal +}): Promise | null> => { + return sendEmailTranslated({ + receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, + template: 'transactionReceived', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + senderFirstName: data.senderFirstName, + senderLastName: data.senderLastName, + senderEmail: data.senderEmail, + transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language), + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) +} diff --git a/backend/src/emails/transactionLinkRedeemed/html.pug b/backend/src/emails/transactionLinkRedeemed/html.pug index f15a278c9..321d070b4 100644 --- a/backend/src/emails/transactionLinkRedeemed/html.pug +++ b/backend/src/emails/transactionLinkRedeemed/html.pug @@ -10,7 +10,7 @@ html(lang=locale) p= t('emails.general.amountGDD', { amountGDD: transactionAmount }) br span= t('emails.transactionLinkRedeemed.memo', { transactionMemo }) - p= t('emails.transactionLinkRedeemed.detailsYouFindOnLinkToYourAccount') + p= t('emails.general.detailsYouFindOnLinkToYourAccount') span= " " a(href=overviewURL) #{overviewURL} p= t('emails.general.pleaseDoNotReply') diff --git a/backend/src/emails/transactionReceived/html.pug b/backend/src/emails/transactionReceived/html.pug new file mode 100644 index 000000000..eaf57f975 --- /dev/null +++ b/backend/src/emails/transactionReceived/html.pug @@ -0,0 +1,16 @@ +doctype html +html(lang=locale) + head + title= t('emails.transactionReceived.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.transactionReceived.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.transactionReceived.haveReceivedAmountGDDFrom', { transactionAmount, senderFirstName, senderLastName, senderEmail }) + p= t('emails.general.detailsYouFindOnLinkToYourAccount') + span= " " + a(href=overviewURL) #{overviewURL} + p= t('emails.general.pleaseDoNotReply') + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/transactionReceived/subject.pug b/backend/src/emails/transactionReceived/subject.pug new file mode 100644 index 000000000..630752b02 --- /dev/null +++ b/backend/src/emails/transactionReceived/subject.pug @@ -0,0 +1 @@ += t('emails.transactionReceived.subject') diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index d83d99132..1a06f46d9 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -2,14 +2,11 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { backendLogger as logger } from '@/server/logger' -import CONFIG from '@/config' import { Context, getUser } from '@/server/context' import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql' import { getCustomRepository, getConnection, In } from '@dbTools/typeorm' -import { sendTransactionReceivedEmail } from '@/mailer/sendTransactionReceivedEmail' - import { Transaction } from '@model/Transaction' import { TransactionList } from '@model/TransactionList' @@ -36,7 +33,10 @@ import Decimal from 'decimal.js-light' import { BalanceResolver } from './BalanceResolver' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { findUserByEmail } from './UserResolver' -import { sendTransactionLinkRedeemedEmail } from '@/emails/sendEmailVariants' +import { + sendTransactionLinkRedeemedEmail, + sendTransactionReceivedEmail, +} from '@/emails/sendEmailVariants' import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event' import { eventProtocol } from '@/event/EventProtocolEmitter' import { Decay } from '../model/Decay' @@ -168,17 +168,15 @@ export const executeTransaction = async ( await queryRunner.release() } logger.debug(`prepare Email for transaction received...`) - // send notification email - // TODO: translate await sendTransactionReceivedEmail({ + firstName: recipient.firstName, + lastName: recipient.lastName, + email: recipient.emailContact.email, + language: recipient.language, senderFirstName: sender.firstName, senderLastName: sender.lastName, - recipientFirstName: recipient.firstName, - recipientLastName: recipient.lastName, - email: recipient.emailContact.email, senderEmail: sender.emailContact.email, - amount, - overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + transactionAmount: amount, }) if (transactionLink) { await sendTransactionLinkRedeemedEmail({ diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index de1e657fe..27a943ccc 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -30,6 +30,7 @@ }, "general": { "amountGDD": "Betrag: {amountGDD} GDD", + "detailsYouFindOnLinkToYourAccount": "Details zur Transaktion findest du in deinem Gradido-Konto:", "helloName": "Hallo {firstName} {lastName},", "linkToYourAccount": "Link zu deinem Konto:", "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", @@ -44,10 +45,13 @@ "youOrSomeoneResetPassword": "du, oder jemand anderes, hast für dieses Konto ein Zurücksetzen des Passworts angefordert." }, "transactionLinkRedeemed": { - "detailsYouFindOnLinkToYourAccount": "Details zur Transaktion findest du in deinem Gradido-Konto:", "hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) hat soeben deinen Link eingelöst.", "memo": "Memo: {transactionMemo}", "subject": "Gradido: Dein Gradido-Link wurde eingelöst" + }, + "transactionReceived": { + "haveReceivedAmountGDDFrom": "du hast soeben {transactionAmount} GDD von {senderFirstName} {senderLastName} ({senderEmail}) erhalten.", + "subject": "Gradido: Du hast Gradidos erhalten" } }, "general": { diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index 34cf2512f..fb578bc40 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -30,6 +30,7 @@ }, "general": { "amountGDD": "Amount: {amountGDD} GDD", + "detailsYouFindOnLinkToYourAccount": "You can find transaction details in your Gradido account:", "helloName": "Hello {firstName} {lastName}", "linkToYourAccount": "Link to your account:", "orCopyLink": "or copy the link above into your browser window.", @@ -44,10 +45,13 @@ "youOrSomeoneResetPassword": "You, or someone else, requested a password reset for this account." }, "transactionLinkRedeemed": { - "detailsYouFindOnLinkToYourAccount": "You can find transaction details in your Gradido account:", "hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) has just redeemed your link.", "memo": "Memo: {transactionMemo}", "subject": "Gradido: Your Gradido link has been redeemed" + }, + "transactionReceived": { + "haveReceivedAmountGDDFrom": "You have just received {transactionAmount} GDD from {senderFirstName} {senderLastName} ({senderEmail}).", + "subject": "Gradido: You have received Gradidos" } }, "general": { From 9894b02f5110263c70031f95a555fa6fc78d8d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 28 Nov 2022 10:54:31 +0100 Subject: [PATCH 30/41] Test 'sendTransactionReceivedEmail' --- backend/src/emails/sendEmailVariants.test.ts | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 858b6426b..dd3c979dd 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -12,6 +12,7 @@ import { sendContributionRejectedEmail, sendResetPasswordEmail, sendTransactionLinkRedeemedEmail, + sendTransactionReceivedEmail, } from './sendEmailVariants' import { sendEmailTranslated } from './sendEmailTranslated' @@ -551,4 +552,73 @@ describe('sendEmailVariants', () => { }) }) }) + + describe('sendTransactionReceivedEmail', () => { + beforeAll(async () => { + result = await sendTransactionReceivedEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + senderEmail: 'bibi@bloxberg.de', + transactionAmount: new Decimal(37.4), + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'transactionReceived', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + senderEmail: 'bibi@bloxberg.de', + transactionAmount: '37.40', + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) + }) + + it('has expected result', () => { + expect(result).toMatchObject({ + envelope: { + from: 'info@gradido.net', + to: ['peter@lustig.de'], + }, + message: expect.any(String), + originalMessage: expect.objectContaining({ + to: 'Peter Lustig ', + from: 'Gradido (nicht antworten) ', + attachments: [], + subject: 'Gradido: You have received Gradidos', + html: expect.any(String), + text: expect.stringContaining('GRADIDO: YOU HAVE RECEIVED GRADIDOS'), + }), + }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain( + 'Gradido: You have received Gradidos', + ) + expect(result.originalMessage.html).toContain('>Gradido: You have received Gradidos') + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'You have just received 37.40 GDD from Bibi Bloxberg (bibi@bloxberg.de).', + ) + expect(result.originalMessage.html).toContain( + `You can find transaction details in your Gradido account: ${CONFIG.EMAIL_LINK_OVERVIEW}`, + ) + expect(result.originalMessage.html).toContain('Please do not reply to this email!') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') + }) + }) + }) }) From 93e3562318a1bc135070eea2275c492d97b91798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 28 Nov 2022 11:04:34 +0100 Subject: [PATCH 31/41] Remove old untranslated email 'sendTransactionReceivedEmail' --- .../sendTransactionReceivedEmail.test.ts | 38 ------------------- .../mailer/sendTransactionReceivedEmail.ts | 27 ------------- .../src/mailer/text/transactionReceived.ts | 29 -------------- 3 files changed, 94 deletions(-) delete mode 100644 backend/src/mailer/sendTransactionReceivedEmail.test.ts delete mode 100644 backend/src/mailer/sendTransactionReceivedEmail.ts delete mode 100644 backend/src/mailer/text/transactionReceived.ts diff --git a/backend/src/mailer/sendTransactionReceivedEmail.test.ts b/backend/src/mailer/sendTransactionReceivedEmail.test.ts deleted file mode 100644 index ca813c033..000000000 --- a/backend/src/mailer/sendTransactionReceivedEmail.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { sendTransactionReceivedEmail } from './sendTransactionReceivedEmail' -import { sendEMail } from './sendEMail' -import Decimal from 'decimal.js-light' - -jest.mock('./sendEMail', () => { - return { - __esModule: true, - sendEMail: jest.fn(), - } -}) - -describe('sendTransactionReceivedEmail', () => { - beforeEach(async () => { - await sendTransactionReceivedEmail({ - senderFirstName: 'Bibi', - senderLastName: 'Bloxberg', - recipientFirstName: 'Peter', - recipientLastName: 'Lustig', - email: 'peter@lustig.de', - senderEmail: 'bibi@bloxberg.de', - amount: new Decimal(42.0), - overviewURL: 'http://localhost/overview', - }) - }) - - it('calls sendEMail', () => { - expect(sendEMail).toBeCalledWith({ - to: `Peter Lustig `, - subject: 'Du hast Gradidos erhalten', - text: - expect.stringContaining('Hallo Peter Lustig') && - expect.stringContaining('42,00 GDD') && - expect.stringContaining('Bibi Bloxberg') && - expect.stringContaining('(bibi@bloxberg.de)') && - expect.stringContaining('http://localhost/overview'), - }) - }) -}) diff --git a/backend/src/mailer/sendTransactionReceivedEmail.ts b/backend/src/mailer/sendTransactionReceivedEmail.ts deleted file mode 100644 index 5e981659c..000000000 --- a/backend/src/mailer/sendTransactionReceivedEmail.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { backendLogger as logger } from '@/server/logger' -import Decimal from 'decimal.js-light' -import { sendEMail } from './sendEMail' -import { transactionReceived } from './text/transactionReceived' - -export const sendTransactionReceivedEmail = (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - email: string - senderEmail: string - amount: Decimal - overviewURL: string -}): Promise => { - logger.info( - `sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName}, - <${data.email}>, - subject=${transactionReceived.de.subject}, - text=${transactionReceived.de.text(data)}`, - ) - return sendEMail({ - to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`, - subject: transactionReceived.de.subject, - text: transactionReceived.de.text(data), - }) -} diff --git a/backend/src/mailer/text/transactionReceived.ts b/backend/src/mailer/text/transactionReceived.ts deleted file mode 100644 index 67758c0e1..000000000 --- a/backend/src/mailer/text/transactionReceived.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Decimal from 'decimal.js-light' - -export const transactionReceived = { - de: { - subject: 'Du hast Gradidos erhalten', - text: (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - email: string - senderEmail: string - amount: Decimal - overviewURL: string - }): string => - `Hallo ${data.recipientFirstName} ${data.recipientLastName}, - -du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${ - data.senderLastName - } (${data.senderEmail}) erhalten. - -Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL} - -Bitte antworte nicht auf diese E-Mail! - -Liebe Grüße -dein Gradido-Team`, - }, -} From 3b47330c925a5577411cc52b4c01d65365274990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 28 Nov 2022 11:05:12 +0100 Subject: [PATCH 32/41] Remove remainders of old untranslated emails --- backend/src/mailer/sendEMail.test.ts | 113 --------------------------- backend/src/mailer/sendEMail.ts | 48 ------------ 2 files changed, 161 deletions(-) delete mode 100644 backend/src/mailer/sendEMail.test.ts delete mode 100644 backend/src/mailer/sendEMail.ts diff --git a/backend/src/mailer/sendEMail.test.ts b/backend/src/mailer/sendEMail.test.ts deleted file mode 100644 index e062b71d8..000000000 --- a/backend/src/mailer/sendEMail.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { sendEMail } from './sendEMail' -import { createTransport } from 'nodemailer' -import CONFIG from '@/config' - -import { logger } from '@test/testSetup' - -CONFIG.EMAIL = false -CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL' -CONFIG.EMAIL_SMTP_PORT = '1234' -CONFIG.EMAIL_USERNAME = 'user' -CONFIG.EMAIL_PASSWORD = 'pwd' - -jest.mock('nodemailer', () => { - return { - __esModule: true, - createTransport: jest.fn(() => { - return { - sendMail: jest.fn(() => { - return { - messageId: 'message', - } - }), - } - }), - } -}) - -describe('sendEMail', () => { - let result: boolean - describe('config email is false', () => { - beforeEach(async () => { - jest.clearAllMocks() - result = await sendEMail({ - to: 'receiver@mail.org', - cc: 'support@gradido.net', - subject: 'Subject', - text: 'Text text text', - }) - }) - - it('logs warning', () => { - expect(logger.info).toBeCalledWith('Emails are disabled via config...') - }) - - it('returns false', () => { - expect(result).toBeFalsy() - }) - }) - - describe('config email is true', () => { - beforeEach(async () => { - jest.clearAllMocks() - CONFIG.EMAIL = true - result = await sendEMail({ - to: 'receiver@mail.org', - cc: 'support@gradido.net', - subject: 'Subject', - text: 'Text text text', - }) - }) - - it('calls the transporter', () => { - expect(createTransport).toBeCalledWith({ - host: 'EMAIL_SMTP_URL', - port: 1234, - secure: false, - requireTLS: true, - auth: { - user: 'user', - pass: 'pwd', - }, - }) - }) - - it('calls sendMail of transporter', () => { - expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({ - from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, - to: 'receiver@mail.org', - cc: 'support@gradido.net', - subject: 'Subject', - text: 'Text text text', - }) - }) - - it('returns true', () => { - expect(result).toBeTruthy() - }) - }) - - describe('with email EMAIL_TEST_MODUS true', () => { - beforeEach(async () => { - jest.clearAllMocks() - CONFIG.EMAIL = true - CONFIG.EMAIL_TEST_MODUS = true - result = await sendEMail({ - to: 'receiver@mail.org', - cc: 'support@gradido.net', - subject: 'Subject', - text: 'Text text text', - }) - }) - - it('calls sendMail of transporter with faked to', () => { - expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({ - from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, - to: CONFIG.EMAIL_TEST_RECEIVER, - cc: 'support@gradido.net', - subject: 'Subject', - text: 'Text text text', - }) - }) - }) -}) diff --git a/backend/src/mailer/sendEMail.ts b/backend/src/mailer/sendEMail.ts deleted file mode 100644 index 00282f232..000000000 --- a/backend/src/mailer/sendEMail.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { backendLogger as logger } from '@/server/logger' -import { createTransport } from 'nodemailer' - -import CONFIG from '@/config' - -export const sendEMail = async (emailDef: { - to: string - cc?: string - subject: string - text: string -}): Promise => { - logger.info( - `send Email: to=${emailDef.to}` + - (emailDef.cc ? `, cc=${emailDef.cc}` : '') + - `, subject=${emailDef.subject}, text=${emailDef.text}`, - ) - - if (!CONFIG.EMAIL) { - logger.info(`Emails are disabled via config...`) - return false - } - if (CONFIG.EMAIL_TEST_MODUS) { - logger.info( - `Testmodus=ON: change receiver from ${emailDef.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`, - ) - emailDef.to = CONFIG.EMAIL_TEST_RECEIVER - } - const transporter = createTransport({ - host: CONFIG.EMAIL_SMTP_URL, - port: Number(CONFIG.EMAIL_SMTP_PORT), - secure: false, // true for 465, false for other ports - requireTLS: true, - auth: { - user: CONFIG.EMAIL_USERNAME, - pass: CONFIG.EMAIL_PASSWORD, - }, - }) - const info = await transporter.sendMail({ - ...emailDef, - from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, - }) - if (!info.messageId) { - logger.error('error sending notification email, but transaction succeed') - throw new Error('error sending notification email, but transaction succeed') - } - logger.info('send Email successfully.') - return true -} From 4ae96a221396b8e4632839ea93d93833e8378394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 28 Nov 2022 11:13:04 +0100 Subject: [PATCH 33/41] Translate 'Gradido (nicht antworten)' --- backend/src/emails/sendEmailTranslated.test.ts | 4 ++-- backend/src/emails/sendEmailTranslated.ts | 2 +- backend/src/emails/sendEmailVariants.test.ts | 16 ++++++++-------- backend/src/locales/de.json | 1 + backend/src/locales/en.json | 1 + 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/backend/src/emails/sendEmailTranslated.test.ts b/backend/src/emails/sendEmailTranslated.test.ts index 72c4c508c..eb4b26b92 100644 --- a/backend/src/emails/sendEmailTranslated.test.ts +++ b/backend/src/emails/sendEmailTranslated.test.ts @@ -89,7 +89,7 @@ describe('sendEmailTranslated', () => { originalMessage: expect.objectContaining({ to: 'receiver@mail.org', cc: 'support@gradido.net', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Try To Register Again With Your Email', html: expect.stringContaining('Gradido: Try To Register Again With Your Email'), @@ -135,7 +135,7 @@ describe('sendEmailTranslated', () => { originalMessage: expect.objectContaining({ to: CONFIG.EMAIL_TEST_RECEIVER, cc: 'support@gradido.net', - from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, + from: `Gradido (do not answer) <${CONFIG.EMAIL_SENDER}>`, attachments: [], subject: 'Gradido: Try To Register Again With Your Email', html: expect.stringContaining('Gradido: Try To Register Again With Your Email'), diff --git a/backend/src/emails/sendEmailTranslated.ts b/backend/src/emails/sendEmailTranslated.ts index 9468e9f97..3f5d9d9fd 100644 --- a/backend/src/emails/sendEmailTranslated.ts +++ b/backend/src/emails/sendEmailTranslated.ts @@ -53,7 +53,7 @@ export const sendEmailTranslated = async (params: { // TESTING: see 'README.md' const email = new Email({ message: { - from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, + from: `Gradido (${i18n.__('emails.general.doNotAnswer')}) <${CONFIG.EMAIL_SENDER}>`, }, transport, preview: false, diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index dd3c979dd..262b91be2 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -83,7 +83,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Message about your common good contribution', html: expect.any(String), @@ -153,7 +153,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Email Verification', html: expect.any(String), @@ -223,7 +223,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Try To Register Again With Your Email', html: expect.any(String), @@ -305,7 +305,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Your common good contribution was confirmed', html: expect.any(String), @@ -375,7 +375,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Your common good contribution was rejected', html: expect.any(String), @@ -445,7 +445,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Reset password', html: expect.any(String), @@ -523,7 +523,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Your Gradido link has been redeemed', html: expect.any(String), @@ -596,7 +596,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: You have received Gradidos', html: expect.any(String), diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index 27a943ccc..8aff6caa4 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -31,6 +31,7 @@ "general": { "amountGDD": "Betrag: {amountGDD} GDD", "detailsYouFindOnLinkToYourAccount": "Details zur Transaktion findest du in deinem Gradido-Konto:", + "doNotAnswer": "nicht antworten", "helloName": "Hallo {firstName} {lastName},", "linkToYourAccount": "Link zu deinem Konto:", "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index fb578bc40..99217840e 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -31,6 +31,7 @@ "general": { "amountGDD": "Amount: {amountGDD} GDD", "detailsYouFindOnLinkToYourAccount": "You can find transaction details in your Gradido account:", + "doNotAnswer": "do not answer", "helloName": "Hello {firstName} {lastName}", "linkToYourAccount": "Link to your account:", "orCopyLink": "or copy the link above into your browser window.", From 88cae0e528eea80319306e8dcef8305f01140d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 29 Nov 2022 13:40:08 +0100 Subject: [PATCH 34/41] Move email '*.pug' templates in 'templates' folder --- backend/src/emails/sendEmailTranslated.ts | 2 +- backend/src/emails/{ => templates}/accountActivation/html.pug | 0 .../src/emails/{ => templates}/accountActivation/subject.pug | 0 .../emails/{ => templates}/accountMultiRegistration/html.pug | 0 .../emails/{ => templates}/accountMultiRegistration/subject.pug | 0 .../emails/{ => templates}/addedContributionMessage/html.pug | 0 .../emails/{ => templates}/addedContributionMessage/subject.pug | 0 .../src/emails/{ => templates}/contributionConfirmed/html.pug | 0 .../emails/{ => templates}/contributionConfirmed/subject.pug | 0 .../src/emails/{ => templates}/contributionRejected/html.pug | 0 .../src/emails/{ => templates}/contributionRejected/subject.pug | 0 backend/src/emails/{ => templates}/resetPassword/html.pug | 0 backend/src/emails/{ => templates}/resetPassword/subject.pug | 0 .../src/emails/{ => templates}/transactionLinkRedeemed/html.pug | 0 .../emails/{ => templates}/transactionLinkRedeemed/subject.pug | 0 backend/src/emails/{ => templates}/transactionReceived/html.pug | 0 .../src/emails/{ => templates}/transactionReceived/subject.pug | 0 17 files changed, 1 insertion(+), 1 deletion(-) rename backend/src/emails/{ => templates}/accountActivation/html.pug (100%) rename backend/src/emails/{ => templates}/accountActivation/subject.pug (100%) rename backend/src/emails/{ => templates}/accountMultiRegistration/html.pug (100%) rename backend/src/emails/{ => templates}/accountMultiRegistration/subject.pug (100%) rename backend/src/emails/{ => templates}/addedContributionMessage/html.pug (100%) rename backend/src/emails/{ => templates}/addedContributionMessage/subject.pug (100%) rename backend/src/emails/{ => templates}/contributionConfirmed/html.pug (100%) rename backend/src/emails/{ => templates}/contributionConfirmed/subject.pug (100%) rename backend/src/emails/{ => templates}/contributionRejected/html.pug (100%) rename backend/src/emails/{ => templates}/contributionRejected/subject.pug (100%) rename backend/src/emails/{ => templates}/resetPassword/html.pug (100%) rename backend/src/emails/{ => templates}/resetPassword/subject.pug (100%) rename backend/src/emails/{ => templates}/transactionLinkRedeemed/html.pug (100%) rename backend/src/emails/{ => templates}/transactionLinkRedeemed/subject.pug (100%) rename backend/src/emails/{ => templates}/transactionReceived/html.pug (100%) rename backend/src/emails/{ => templates}/transactionReceived/subject.pug (100%) diff --git a/backend/src/emails/sendEmailTranslated.ts b/backend/src/emails/sendEmailTranslated.ts index 3f5d9d9fd..69008c00e 100644 --- a/backend/src/emails/sendEmailTranslated.ts +++ b/backend/src/emails/sendEmailTranslated.ts @@ -63,7 +63,7 @@ export const sendEmailTranslated = async (params: { // ATTENTION: await is needed, because otherwise on send the email gets send in the language of the current user, because below the language gets reset await email .send({ - template: path.join(__dirname, params.template), + template: path.join(__dirname, 'templates', params.template), message: params.receiver, locals: params.locals, // the 'locale' in here seems not to be used by 'email-template', because it doesn't work if the language isn't set before by 'i18n.setLocale' }) diff --git a/backend/src/emails/accountActivation/html.pug b/backend/src/emails/templates/accountActivation/html.pug similarity index 100% rename from backend/src/emails/accountActivation/html.pug rename to backend/src/emails/templates/accountActivation/html.pug diff --git a/backend/src/emails/accountActivation/subject.pug b/backend/src/emails/templates/accountActivation/subject.pug similarity index 100% rename from backend/src/emails/accountActivation/subject.pug rename to backend/src/emails/templates/accountActivation/subject.pug diff --git a/backend/src/emails/accountMultiRegistration/html.pug b/backend/src/emails/templates/accountMultiRegistration/html.pug similarity index 100% rename from backend/src/emails/accountMultiRegistration/html.pug rename to backend/src/emails/templates/accountMultiRegistration/html.pug diff --git a/backend/src/emails/accountMultiRegistration/subject.pug b/backend/src/emails/templates/accountMultiRegistration/subject.pug similarity index 100% rename from backend/src/emails/accountMultiRegistration/subject.pug rename to backend/src/emails/templates/accountMultiRegistration/subject.pug diff --git a/backend/src/emails/addedContributionMessage/html.pug b/backend/src/emails/templates/addedContributionMessage/html.pug similarity index 100% rename from backend/src/emails/addedContributionMessage/html.pug rename to backend/src/emails/templates/addedContributionMessage/html.pug diff --git a/backend/src/emails/addedContributionMessage/subject.pug b/backend/src/emails/templates/addedContributionMessage/subject.pug similarity index 100% rename from backend/src/emails/addedContributionMessage/subject.pug rename to backend/src/emails/templates/addedContributionMessage/subject.pug diff --git a/backend/src/emails/contributionConfirmed/html.pug b/backend/src/emails/templates/contributionConfirmed/html.pug similarity index 100% rename from backend/src/emails/contributionConfirmed/html.pug rename to backend/src/emails/templates/contributionConfirmed/html.pug diff --git a/backend/src/emails/contributionConfirmed/subject.pug b/backend/src/emails/templates/contributionConfirmed/subject.pug similarity index 100% rename from backend/src/emails/contributionConfirmed/subject.pug rename to backend/src/emails/templates/contributionConfirmed/subject.pug diff --git a/backend/src/emails/contributionRejected/html.pug b/backend/src/emails/templates/contributionRejected/html.pug similarity index 100% rename from backend/src/emails/contributionRejected/html.pug rename to backend/src/emails/templates/contributionRejected/html.pug diff --git a/backend/src/emails/contributionRejected/subject.pug b/backend/src/emails/templates/contributionRejected/subject.pug similarity index 100% rename from backend/src/emails/contributionRejected/subject.pug rename to backend/src/emails/templates/contributionRejected/subject.pug diff --git a/backend/src/emails/resetPassword/html.pug b/backend/src/emails/templates/resetPassword/html.pug similarity index 100% rename from backend/src/emails/resetPassword/html.pug rename to backend/src/emails/templates/resetPassword/html.pug diff --git a/backend/src/emails/resetPassword/subject.pug b/backend/src/emails/templates/resetPassword/subject.pug similarity index 100% rename from backend/src/emails/resetPassword/subject.pug rename to backend/src/emails/templates/resetPassword/subject.pug diff --git a/backend/src/emails/transactionLinkRedeemed/html.pug b/backend/src/emails/templates/transactionLinkRedeemed/html.pug similarity index 100% rename from backend/src/emails/transactionLinkRedeemed/html.pug rename to backend/src/emails/templates/transactionLinkRedeemed/html.pug diff --git a/backend/src/emails/transactionLinkRedeemed/subject.pug b/backend/src/emails/templates/transactionLinkRedeemed/subject.pug similarity index 100% rename from backend/src/emails/transactionLinkRedeemed/subject.pug rename to backend/src/emails/templates/transactionLinkRedeemed/subject.pug diff --git a/backend/src/emails/transactionReceived/html.pug b/backend/src/emails/templates/transactionReceived/html.pug similarity index 100% rename from backend/src/emails/transactionReceived/html.pug rename to backend/src/emails/templates/transactionReceived/html.pug diff --git a/backend/src/emails/transactionReceived/subject.pug b/backend/src/emails/templates/transactionReceived/subject.pug similarity index 100% rename from backend/src/emails/transactionReceived/subject.pug rename to backend/src/emails/templates/transactionReceived/subject.pug From 3d55c7da9e7ffc1435fece650e760b9de04a597d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 29 Nov 2022 13:41:42 +0100 Subject: [PATCH 35/41] Fix production by copying missing email templates and locale files into the build folder --- backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index 519f9e6c0..c6b3dabc2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,7 @@ "license": "Apache-2.0", "private": false, "scripts": { - "build": "tsc --build", + "build": "tsc --build && mkdir -p build/src/emails/templates/ && cp -r src/emails/templates/* build/src/emails/templates/ && mkdir -p build/src/locales/ && cp -r src/locales/*.json build/src/locales/", "clean": "tsc --build --clean", "start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js", "dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts", From a7ee30fc3fd444ad28229c586d50939a302ac768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 29 Nov 2022 13:42:37 +0100 Subject: [PATCH 36/41] Fix some comments and readme spellings --- DOCKER_MORE_CLOSELY.md | 4 ++-- docker-compose.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DOCKER_MORE_CLOSELY.md b/DOCKER_MORE_CLOSELY.md index f2aae81c7..c21b99829 100644 --- a/DOCKER_MORE_CLOSELY.md +++ b/DOCKER_MORE_CLOSELY.md @@ -4,7 +4,7 @@ ***Attention:** For using Docker commands in Apple M1 environments!* -### Enviroment Variable For Apple M1 Platform +### Environment Variable For Apple M1 Platform To set the Docker platform environment variable in your terminal tab, run: @@ -27,7 +27,7 @@ $ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker- $ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up ``` -## Analysing Docker Builds +## Analyzing Docker Builds To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it! diff --git a/docker-compose.yml b/docker-compose.yml index 5f0ab4dde..5eea075c6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -108,7 +108,7 @@ services: #env_file: # - ./frontend/.env volumes: - # : – mirror bidirectional path in local context with path in Docker container + # : – mirror bidirectional path in local context with path in Docker container - ./logs/backend:/logs/backend ######################################################## From 317224db7a08027531f489e32476983f1e80091a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 29 Nov 2022 14:09:15 +0100 Subject: [PATCH 37/41] Refactor some readme's by moving their content into 'CONTRIBUTING.md' --- DOCKER_MORE_CLOSELY.md => CONTRIBUTING.md | 28 +++++++++++++++++++---- admin/src/locales/README.md | 3 --- backend/src/locales/README.md | 3 --- frontend/src/locales/README.md | 25 -------------------- 4 files changed, 23 insertions(+), 36 deletions(-) rename DOCKER_MORE_CLOSELY.md => CONTRIBUTING.md (65%) delete mode 100644 admin/src/locales/README.md delete mode 100644 backend/src/locales/README.md delete mode 100644 frontend/src/locales/README.md diff --git a/DOCKER_MORE_CLOSELY.md b/CONTRIBUTING.md similarity index 65% rename from DOCKER_MORE_CLOSELY.md rename to CONTRIBUTING.md index c21b99829..3ebcdb73a 100644 --- a/DOCKER_MORE_CLOSELY.md +++ b/CONTRIBUTING.md @@ -1,10 +1,28 @@ -# Docker More Closely +# Contributing -## Apple M1 Platform +If you contribute to our project, please consider the following points. + +## Localization + +### Quotation Marks + +The following characters are different from the programming quotation mark: + +`"` or `\"` + +Please copy and paste the following quotes for the languages: + +- de: „Dies ist ein Beispielsatz.“ +- en: “This is a sample sentence.” + - See + +## Docker – More Closely + +### Apple M1 Platform ***Attention:** For using Docker commands in Apple M1 environments!* -### Environment Variable For Apple M1 Platform +#### Environment Variable For Apple M1 Platform To set the Docker platform environment variable in your terminal tab, run: @@ -13,7 +31,7 @@ To set the Docker platform environment variable in your terminal tab, run: $ export DOCKER_DEFAULT_PLATFORM=linux/amd64 ``` -### Docker Compose Override File For Apple M1 Platform +#### Docker Compose Override File For Apple M1 Platform For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform: @@ -27,7 +45,7 @@ $ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker- $ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up ``` -## Analyzing Docker Builds +### Analyzing Docker Builds To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it! diff --git a/admin/src/locales/README.md b/admin/src/locales/README.md deleted file mode 100644 index 5d6bf75b1..000000000 --- a/admin/src/locales/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Localizations - -Please see [frontend localization](/frontend/src/locales/README.md). diff --git a/backend/src/locales/README.md b/backend/src/locales/README.md deleted file mode 100644 index 5d6bf75b1..000000000 --- a/backend/src/locales/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Localizations - -Please see [frontend localization](/frontend/src/locales/README.md). diff --git a/frontend/src/locales/README.md b/frontend/src/locales/README.md deleted file mode 100644 index 2c03abbd4..000000000 --- a/frontend/src/locales/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Localizations - -## Quotation Marks - -The following characters are different from the programming quotation mark: - -`"` - -### English - -In English, we use these double-barreled quotation marks: - -“This is a sample sentence.” - -Please copy and paste … - -See - -### German - -In German, we use these double-barreled quotation marks: - -„Dies ist ein Beispielsatz.“ - -Please copy and paste … From bc20bfa8f6e38e40fbcc67c692e926ad28fae1f0 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 30 Nov 2022 12:46:46 +0100 Subject: [PATCH 38/41] fix(backend): delete / undelete email contact as well --- .../graphql/resolver/AdminResolver.test.ts | 22 +++++++++++++++++++ backend/src/graphql/resolver/AdminResolver.ts | 9 ++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 503bab472..757f552f8 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -366,6 +366,19 @@ describe('AdminResolver', () => { expect(new Date(result.data.deleteUser)).toEqual(expect.any(Date)) }) + it('has deleted at set in users and user contacts', async () => { + await expect( + User.findOneOrFail({ + where: { id: user.id }, + withDeleted: true, + relations: ['emailContact'], + }), + ).resolves.toMatchObject({ + deletedAt: expect.any(Date), + emailContact: expect.objectContaining({ deletedAt: expect.any(Date) }), + }) + }) + describe('delete deleted user', () => { it('throws an error', async () => { jest.clearAllMocks() @@ -489,6 +502,15 @@ describe('AdminResolver', () => { }), ) }) + + it('has deleted at set to null in users and user contacts', async () => { + await expect( + User.findOneOrFail({ where: { id: user.id }, relations: ['emailContact'] }), + ).resolves.toMatchObject({ + deletedAt: null, + emailContact: expect.objectContaining({ deletedAt: null }), + }) + }) }) }) }) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 80c69a864..9ff4824e5 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -200,7 +200,7 @@ export class AdminResolver { @Arg('userId', () => Int) userId: number, @Ctx() context: Context, ): Promise { - const user = await dbUser.findOne({ id: userId }) + const user = await dbUser.findOne({ where: { id: userId }, relations: ['emailContact'] }) // user exists ? if (!user) { logger.error(`Could not find user with userId: ${userId}`) @@ -214,6 +214,7 @@ export class AdminResolver { } // soft-delete user await user.softRemove() + await user.emailContact.softRemove() const newUser = await dbUser.findOne({ id: userId }, { withDeleted: true }) return newUser ? newUser.deletedAt : null } @@ -221,7 +222,10 @@ export class AdminResolver { @Authorized([RIGHTS.UNDELETE_USER]) @Mutation(() => Date, { nullable: true }) async unDeleteUser(@Arg('userId', () => Int) userId: number): Promise { - const user = await dbUser.findOne({ id: userId }, { withDeleted: true }) + const user = await dbUser.findOne( + { id: userId }, + { withDeleted: true, relations: ['emailContact'] }, + ) if (!user) { logger.error(`Could not find user with userId: ${userId}`) throw new Error(`Could not find user with userId: ${userId}`) @@ -231,6 +235,7 @@ export class AdminResolver { throw new Error('User is not deleted') } await user.recover() + await user.emailContact.recover() return null } From d3678bb81c51672ceee0d07359a76500eac6cec3 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 1 Dec 2022 17:44:56 +0100 Subject: [PATCH 39/41] feat(backend): log client timezone offset --- backend/src/graphql/resolver/util/creations.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index eb4b6394d..54286d2aa 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -103,6 +103,9 @@ export const getUserCreation = async ( const getCreationMonths = (timezoneOffset: number): number[] => { const clientNow = new Date() clientNow.setTime(clientNow.getTime() - timezoneOffset * 60 * 1000) + logger.info( + `getCreationMonths -- offset: ${timezoneOffset} -- clientNow: ${clientNow.toISOString()}`, + ) return [ new Date(clientNow.getFullYear(), clientNow.getMonth() - 2, 1).getMonth() + 1, new Date(clientNow.getFullYear(), clientNow.getMonth() - 1, 1).getMonth() + 1, From bd3f05d6c8c57b1dc010507f115f7e47f72ab986 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 8 Dec 2022 19:11:01 +0100 Subject: [PATCH 40/41] Update backend/src/graphql/resolver/AdminResolver.test.ts Co-authored-by: Ulf Gebhardt --- backend/src/graphql/resolver/AdminResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 757f552f8..8554f990a 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -366,7 +366,7 @@ describe('AdminResolver', () => { expect(new Date(result.data.deleteUser)).toEqual(expect.any(Date)) }) - it('has deleted at set in users and user contacts', async () => { + it('has deleted_at set in users and user contacts', async () => { await expect( User.findOneOrFail({ where: { id: user.id }, From cbb688112f5f08ed7bb9fb38688dd2773d4b5d41 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 8 Dec 2022 19:11:09 +0100 Subject: [PATCH 41/41] Update backend/src/graphql/resolver/AdminResolver.test.ts Co-authored-by: Ulf Gebhardt --- backend/src/graphql/resolver/AdminResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 8554f990a..4a55bd0ad 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -503,7 +503,7 @@ describe('AdminResolver', () => { ) }) - it('has deleted at set to null in users and user contacts', async () => { + it('has deleted_at set to null in users and user contacts', async () => { await expect( User.findOneOrFail({ where: { id: user.id }, relations: ['emailContact'] }), ).resolves.toMatchObject({