Merge pull request #2398 from gradido/2392-refactor-more-emails-to-translatables

refactor(backend): refactor more emails to translatables
This commit is contained in:
Wolfgang Huß 2022-12-09 13:12:55 +01:00 committed by GitHub
commit 70a3b1bc7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1205 additions and 1017 deletions

View File

@ -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 <https://grammar.collinsdictionary.com/easy-learning/when-do-you-use-quotation-marks-or-in-english>
## Docker More Closely
### Apple M1 Platform
***Attention:** For using Docker commands in Apple M1 environments!* ***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: 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 $ 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: 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 $ 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! To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it!

View File

@ -8,7 +8,7 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"private": false, "private": false,
"scripts": { "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", "clean": "tsc --build --clean",
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js", "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", "dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts",

View File

@ -1 +0,0 @@
= t('emails.accountMultiRegistration.subject')

View File

@ -89,7 +89,7 @@ describe('sendEmailTranslated', () => {
originalMessage: expect.objectContaining({ originalMessage: expect.objectContaining({
to: 'receiver@mail.org', to: 'receiver@mail.org',
cc: 'support@gradido.net', cc: 'support@gradido.net',
from: 'Gradido (nicht antworten) <info@gradido.net>', from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [], attachments: [],
subject: 'Gradido: Try To Register Again With Your Email', subject: 'Gradido: Try To Register Again With Your Email',
html: expect.stringContaining('Gradido: Try To Register Again With Your Email'), html: expect.stringContaining('Gradido: Try To Register Again With Your Email'),
@ -107,4 +107,41 @@ describe('sendEmailTranslated', () => {
expect(i18n.__).toBeCalled() 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 (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'),
text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'),
}),
})
})
})
}) })

View File

@ -1,18 +1,17 @@
import CONFIG from '@/config'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import path from 'path' import path from 'path'
import { createTransport } from 'nodemailer' import { createTransport } from 'nodemailer'
import Email from 'email-templates' import Email from 'email-templates'
import i18n from 'i18n' import i18n from 'i18n'
import CONFIG from '@/config'
export const sendEmailTranslated = async (params: { export const sendEmailTranslated = async (params: {
receiver: { receiver: {
to: string to: string
cc?: string cc?: string
} }
template: string template: string
locals: Record<string, string> locals: Record<string, unknown>
}): Promise<Record<string, unknown> | null> => { }): Promise<Record<string, unknown> | null> => {
let resultSend: Record<string, unknown> | null = null let resultSend: Record<string, unknown> | null = null
@ -32,8 +31,7 @@ export const sendEmailTranslated = async (params: {
logger.info(`Emails are disabled via config...`) logger.info(`Emails are disabled via config...`)
return null return null
} }
// because 'CONFIG.EMAIL_TEST_MODUS' can be boolean 'true' or string '`false`' if (CONFIG.EMAIL_TEST_MODUS) {
if (CONFIG.EMAIL_TEST_MODUS === true) {
logger.info( logger.info(
`Testmodus=ON: change receiver from ${params.receiver.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`, `Testmodus=ON: change receiver from ${params.receiver.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`,
) )
@ -50,12 +48,12 @@ 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' // TESTING: see 'README.md'
const email = new Email({ const email = new Email({
message: { message: {
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, from: `Gradido (${i18n.__('emails.general.doNotAnswer')}) <${CONFIG.EMAIL_SENDER}>`,
}, },
transport, transport,
preview: false, preview: false,
@ -65,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 // 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 await email
.send({ .send({
template: path.join(__dirname, params.template), template: path.join(__dirname, 'templates', params.template),
message: params.receiver, 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' 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'
}) })

View File

@ -1,12 +1,34 @@
/* 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 CONFIG from '@/config'
import { sendAccountMultiRegistrationEmail } from './sendEmailVariants' import {
sendAddedContributionMessageEmail,
sendAccountActivationEmail,
sendAccountMultiRegistrationEmail,
sendContributionConfirmedEmail,
sendContributionRejectedEmail,
sendResetPasswordEmail,
sendTransactionLinkRedeemedEmail,
sendTransactionReceivedEmail,
} from './sendEmailVariants'
import { sendEmailTranslated } from './sendEmailTranslated' import { sendEmailTranslated } from './sendEmailTranslated'
CONFIG.EMAIL = true let con: any
CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL' let testEnv: any
CONFIG.EMAIL_SMTP_PORT = '1234'
CONFIG.EMAIL_USERNAME = 'user' beforeAll(async () => {
CONFIG.EMAIL_PASSWORD = 'pwd' testEnv = await testEnvironment(logger, localization)
con = testEnv.con
// await cleanDB()
})
afterAll(async () => {
// await cleanDB()
await con.close()
})
jest.mock('./sendEmailTranslated', () => { jest.mock('./sendEmailTranslated', () => {
const originalModule = jest.requireActual('./sendEmailTranslated') const originalModule = jest.requireActual('./sendEmailTranslated')
@ -17,7 +39,154 @@ jest.mock('./sendEmailTranslated', () => {
}) })
describe('sendEmailVariants', () => { describe('sendEmailVariants', () => {
let result: Record<string, unknown> | null // 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 <peter@lustig.de>',
},
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 <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
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('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Message about your common good contribution</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Message about your common good contribution</h1>',
)
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:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
)
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
})
})
})
describe('sendAccountActivationEmail', () => {
beforeAll(async () => {
result = await sendAccountActivationEmail({
firstName: 'Peter',
lastName: 'Lustig',
email: 'peter@lustig.de',
language: 'en',
activationLink: 'http://localhost/checkEmail/6627633878930542284',
timeDurationObject: { hours: 23, minutes: 30 },
})
})
describe('calls "sendEmailTranslated"', () => {
it('with expected parameters', () => {
expect(sendEmailTranslated).toBeCalledWith({
receiver: {
to: 'Peter Lustig <peter@lustig.de>',
},
template: 'accountActivation',
locals: {
firstName: 'Peter',
lastName: 'Lustig',
locale: 'en',
activationLink: 'http://localhost/checkEmail/6627633878930542284',
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 <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: Email Verification',
html: expect.any(String),
text: expect.stringContaining('GRADIDO: EMAIL VERIFICATION'),
}),
})
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain('<title>Gradido: Email Verification</title>')
expect(result.originalMessage.html).toContain('>Gradido: Email Verification</h1>')
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(
'<a href="http://localhost/checkEmail/6627633878930542284">http://localhost/checkEmail/6627633878930542284</a>',
)
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(
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
)
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
})
})
})
describe('sendAccountMultiRegistrationEmail', () => { describe('sendAccountMultiRegistrationEmail', () => {
beforeAll(async () => { beforeAll(async () => {
@ -54,34 +223,401 @@ describe('sendEmailVariants', () => {
message: expect.any(String), message: expect.any(String),
originalMessage: expect.objectContaining({ originalMessage: expect.objectContaining({
to: 'Peter Lustig <peter@lustig.de>', to: 'Peter Lustig <peter@lustig.de>',
from: 'Gradido (nicht antworten) <info@gradido.net>', from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [], attachments: [],
subject: 'Gradido: Try To Register Again With Your Email', subject: 'Gradido: Try To Register Again With Your Email',
html: html: expect.any(String),
expect.stringContaining(
'<title>Gradido: Try To Register Again With Your Email</title>',
) &&
expect.stringContaining('>Gradido: Try To Register Again With Your Email</h1>') &&
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(
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
) &&
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,<br><span>your Gradido team'),
text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'), text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'),
}), }),
}) })
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Try To Register Again With Your Email</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Try To Register Again With Your Email</h1>',
)
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(
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
)
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('Kind regards,<br><span>your Gradido team')
})
})
})
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 <peter@lustig.de>',
},
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 <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
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('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Your common good contribution was confirmed</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Your common good contribution was confirmed</h1>',
)
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:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
)
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
})
})
})
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 <peter@lustig.de>',
},
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 <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
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('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Your common good contribution was rejected</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Your common good contribution was rejected</h1>',
)
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:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
)
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
})
})
})
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 <peter@lustig.de>',
},
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 <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: Reset password',
html: expect.any(String),
text: expect.stringContaining('GRADIDO: RESET PASSWORD'),
}),
})
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain('<title>Gradido: Reset password</title>')
expect(result.originalMessage.html).toContain('>Gradido: Reset password</h1>')
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(
'<a href="http://localhost/reset-password/3762660021544901417">http://localhost/reset-password/3762660021544901417</a>',
)
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(
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
)
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
})
})
})
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 <peter@lustig.de>',
},
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 <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
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('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Your Gradido link has been redeemed</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Your Gradido link has been redeemed</h1>',
)
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:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
)
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
})
})
})
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 <peter@lustig.de>',
},
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 <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: You have received Gradidos',
html: expect.any(String),
text: expect.stringContaining('GRADIDO: YOU HAVE RECEIVED GRADIDOS'),
}),
})
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: You have received Gradidos</title>',
)
expect(result.originalMessage.html).toContain('>Gradido: You have received Gradidos</h1>')
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:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
)
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
}) })
}) })
}) })

View File

@ -1,6 +1,56 @@
import Decimal from 'decimal.js-light'
import CONFIG from '@/config' import CONFIG from '@/config'
import { decimalSeparatorByLanguage } from '@/util/utilities'
import { sendEmailTranslated } from './sendEmailTranslated' import { sendEmailTranslated } from './sendEmailTranslated'
export const sendAddedContributionMessageEmail = (data: {
firstName: string
lastName: string
email: string
language: string
senderFirstName: string
senderLastName: string
contributionMemo: string
}): Promise<Record<string, unknown> | 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
email: string
language: string
activationLink: string
timeDurationObject: Record<string, unknown>
}): Promise<Record<string, unknown> | null> => {
return sendEmailTranslated({
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
template: 'accountActivation',
locals: {
firstName: data.firstName,
lastName: data.lastName,
locale: data.language,
activationLink: data.activationLink,
timeDurationObject: data.timeDurationObject,
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
},
})
}
export const sendAccountMultiRegistrationEmail = (data: { export const sendAccountMultiRegistrationEmail = (data: {
firstName: string firstName: string
lastName: string lastName: string
@ -11,10 +61,136 @@ export const sendAccountMultiRegistrationEmail = (data: {
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
template: 'accountMultiRegistration', template: 'accountMultiRegistration',
locals: { locals: {
locale: data.language,
firstName: data.firstName, firstName: data.firstName,
lastName: data.lastName, lastName: data.lastName,
locale: data.language,
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
}, },
}) })
} }
export const sendContributionConfirmedEmail = (data: {
firstName: string
lastName: string
email: string
language: string
senderFirstName: string
senderLastName: string
contributionMemo: string
contributionAmount: Decimal
}): Promise<Record<string, unknown> | null> => {
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: decimalSeparatorByLanguage(data.contributionAmount, data.language),
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
},
})
}
export const sendContributionRejectedEmail = (data: {
firstName: string
lastName: string
email: string
language: string
senderFirstName: string
senderLastName: string
contributionMemo: string
}): Promise<Record<string, unknown> | 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,
},
})
}
export const sendResetPasswordEmail = (data: {
firstName: string
lastName: string
email: string
language: string
resetLink: string
timeDurationObject: Record<string, unknown>
}): Promise<Record<string, unknown> | 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,
},
})
}
export const sendTransactionLinkRedeemedEmail = (data: {
firstName: string
lastName: string
email: string
language: string
senderFirstName: string
senderLastName: string
senderEmail: string
transactionMemo: string
transactionAmount: Decimal
}): Promise<Record<string, unknown> | 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,
},
})
}
export const sendTransactionReceivedEmail = (data: {
firstName: string
lastName: string
email: string
language: string
senderFirstName: string
senderLastName: string
senderEmail: string
transactionAmount: Decimal
}): Promise<Record<string, unknown> | 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,
},
})
}

View File

@ -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.general.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')

View File

@ -0,0 +1 @@
= t('emails.accountActivation.subject')

View File

@ -5,7 +5,7 @@ html(lang=locale)
body body
h1(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.subject') h1(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.subject')
#container.col #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') p= t('emails.accountMultiRegistration.emailReused')
br br
span= t('emails.accountMultiRegistration.emailExists') span= t('emails.accountMultiRegistration.emailExists')
@ -17,6 +17,6 @@ html(lang=locale)
p= t('emails.accountMultiRegistration.ifYouAreNotTheOne') p= t('emails.accountMultiRegistration.ifYouAreNotTheOne')
br br
a(href='https://gradido.net/de/contact/') https://gradido.net/de/contact/ 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 br
span= t('emails.accountMultiRegistration.yourGradidoTeam') span= t('emails.general.yourGradidoTeam')

View File

@ -0,0 +1 @@
= t('emails.accountMultiRegistration.subject')

View File

@ -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.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')

View File

@ -0,0 +1 @@
= t('emails.addedContributionMessage.subject')

View File

@ -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.general.amountGDD', { amountGDD: 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')

View File

@ -0,0 +1 @@
= t('emails.contributionConfirmed.subject')

View File

@ -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')

View File

@ -0,0 +1 @@
= t('emails.contributionRejected.subject')

View File

@ -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')

View File

@ -0,0 +1 @@
= t('emails.resetPassword.subject')

View File

@ -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.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')

View File

@ -0,0 +1 @@
= t('emails.transactionLinkRedeemed.subject')

View File

@ -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')

View File

@ -0,0 +1 @@
= t('emails.transactionReceived.subject')

View File

@ -3,6 +3,7 @@
import { objectValuesToArray } from '@/util/utilities' import { objectValuesToArray } from '@/util/utilities'
import { testEnvironment, resetToken, cleanDB, contributionDateFormatter } from '@test/helpers' import { testEnvironment, resetToken, cleanDB, contributionDateFormatter } from '@test/helpers'
import { logger, i18n as localization } from '@test/testSetup'
import { userFactory } from '@/seeds/factory/user' import { userFactory } from '@/seeds/factory/user'
import { creationFactory } from '@/seeds/factory/creation' import { creationFactory } from '@/seeds/factory/creation'
import { creations } from '@/seeds/creation/index' import { creations } from '@/seeds/creation/index'
@ -35,30 +36,31 @@ import {
} from '@/seeds/graphql/queries' } from '@/seeds/graphql/queries'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { User } from '@entity/User' import { User } from '@entity/User'
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ import {
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' // sendAccountActivationEmail,
sendContributionConfirmedEmail,
// sendContributionRejectedEmail,
} from '@/emails/sendEmailVariants'
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { Contribution } from '@entity/Contribution' import { Contribution } from '@entity/Contribution'
import { Transaction as DbTransaction } from '@entity/Transaction' import { Transaction as DbTransaction } from '@entity/Transaction'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail'
import { EventProtocol } from '@entity/EventProtocol' import { EventProtocol } from '@entity/EventProtocol'
import { EventProtocolType } from '@/event/EventProtocolType' import { EventProtocolType } from '@/event/EventProtocolType'
import { logger } from '@test/testSetup'
// mock account activation email to avoid console spam // mock account activation email to avoid console spam
jest.mock('@/mailer/sendAccountActivationEmail', () => { jest.mock('@/emails/sendEmailVariants', () => {
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
return { return {
__esModule: true, __esModule: true,
sendAccountActivationEmail: jest.fn(), ...originalModule,
} // TODO: test the call of …
}) // sendAccountActivationEmail: jest.fn((a) => originalModule.sendAccountActivationEmail(a)),
sendContributionConfirmedEmail: jest.fn((a) =>
// mock account activation email to avoid console spam originalModule.sendContributionConfirmedEmail(a),
jest.mock('@/mailer/sendContributionConfirmedEmail', () => { ),
return { // TODO: test the call of …
__esModule: true, // sendContributionRejectedEmail: jest.fn((a) => originalModule.sendContributionRejectedEmail(a)),
sendContributionConfirmedEmail: jest.fn(),
} }
}) })
@ -66,7 +68,7 @@ let mutate: any, query: any, con: any
let testEnv: any let testEnv: any
beforeAll(async () => { beforeAll(async () => {
testEnv = await testEnvironment() testEnv = await testEnvironment(logger, localization)
mutate = testEnv.mutate mutate = testEnv.mutate
query = testEnv.query query = testEnv.query
con = testEnv.con con = testEnv.con
@ -1737,17 +1739,16 @@ describe('AdminResolver', () => {
}) })
it('calls sendContributionConfirmedEmail', async () => { it('calls sendContributionConfirmedEmail', async () => {
expect(sendContributionConfirmedEmail).toBeCalledWith( expect(sendContributionConfirmedEmail).toBeCalledWith({
expect.objectContaining({ firstName: 'Bibi',
contributionMemo: 'Herzlich Willkommen bei Gradido liebe Bibi!', lastName: 'Bloxberg',
overviewURL: 'http://localhost/overview', email: 'bibi@bloxberg.de',
recipientEmail: 'bibi@bloxberg.de', language: 'de',
recipientFirstName: 'Bibi', senderFirstName: 'Peter',
recipientLastName: 'Bloxberg', senderLastName: 'Lustig',
senderFirstName: 'Peter', contributionMemo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
senderLastName: 'Lustig', contributionAmount: expect.decimalEqual(450),
}), })
)
}) })
it('stores the send confirmation email event in the database', async () => { it('stores the send confirmation email event in the database', async () => {

View File

@ -39,8 +39,14 @@ import { Decay } from '@model/Decay'
import Paginated from '@arg/Paginated' import Paginated from '@arg/Paginated'
import TransactionLinkFilters from '@arg/TransactionLinkFilters' import TransactionLinkFilters from '@arg/TransactionLinkFilters'
import { Order } from '@enum/Order' import { Order } from '@enum/Order'
import { findUserByEmail, activationLink, printTimeDuration } from './UserResolver' import { getTimeDurationObject } from '@/util/time'
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' import { findUserByEmail, activationLink } from './UserResolver'
import {
sendAddedContributionMessageEmail,
sendAccountActivationEmail,
sendContributionConfirmedEmail,
sendContributionRejectedEmail,
} from '@/emails/sendEmailVariants'
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
import CONFIG from '@/config' import CONFIG from '@/config'
import { import {
@ -63,9 +69,6 @@ import { ContributionMessage as DbContributionMessage } from '@entity/Contributi
import ContributionMessageArgs from '@arg/ContributionMessageArgs' import ContributionMessageArgs from '@arg/ContributionMessageArgs'
import { ContributionMessageType } from '@enum/MessageType' import { ContributionMessageType } from '@enum/MessageType'
import { ContributionMessage } from '@model/ContributionMessage' 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 { eventProtocol } from '@/event/EventProtocolEmitter'
import { import {
Event, Event,
@ -492,14 +495,13 @@ export class AdminResolver {
event.setEventAdminContributionDelete(eventAdminContributionDelete), event.setEventAdminContributionDelete(eventAdminContributionDelete),
) )
sendContributionRejectedEmail({ sendContributionRejectedEmail({
firstName: user.firstName,
lastName: user.lastName,
email: user.emailContact.email,
language: user.language,
senderFirstName: moderator.firstName, senderFirstName: moderator.firstName,
senderLastName: moderator.lastName, senderLastName: moderator.lastName,
recipientEmail: user.emailContact.email,
recipientFirstName: user.firstName,
recipientLastName: user.lastName,
contributionMemo: contribution.memo, contributionMemo: contribution.memo,
contributionAmount: contribution.amount,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
}) })
return !!res return !!res
@ -587,14 +589,14 @@ export class AdminResolver {
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.info('creation commited successfuly.') logger.info('creation commited successfuly.')
sendContributionConfirmedEmail({ sendContributionConfirmedEmail({
firstName: user.firstName,
lastName: user.lastName,
email: user.emailContact.email,
language: user.language,
senderFirstName: moderatorUser.firstName, senderFirstName: moderatorUser.firstName,
senderLastName: moderatorUser.lastName, senderLastName: moderatorUser.lastName,
recipientFirstName: user.firstName,
recipientLastName: user.lastName,
recipientEmail: user.emailContact.email,
contributionMemo: contribution.memo, contributionMemo: contribution.memo,
contributionAmount: contribution.amount, contributionAmount: contribution.amount,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
}) })
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
@ -655,17 +657,18 @@ export class AdminResolver {
} }
const emailContact = user.emailContact const emailContact = user.emailContact
if (emailContact.deletedAt) { if (emailContact.deletedAt) {
logger.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 htis User is deleted.`) throw new Error(`The emailContact: ${email} of this User is deleted.`)
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendAccountActivationEmail({ const emailSent = await sendAccountActivationEmail({
link: activationLink(emailContact.emailVerificationCode),
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
email, 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 // In case EMails are disabled log the activation link for the user
@ -900,15 +903,13 @@ export class AdminResolver {
} }
await sendAddedContributionMessageEmail({ await sendAddedContributionMessageEmail({
firstName: contribution.user.firstName,
lastName: contribution.user.lastName,
email: contribution.user.emailContact.email,
language: contribution.user.language,
senderFirstName: user.firstName, senderFirstName: user.firstName,
senderLastName: user.lastName, senderLastName: user.lastName,
recipientFirstName: contribution.user.firstName,
recipientLastName: contribution.user.lastName,
recipientEmail: contribution.user.emailContact.email,
senderEmail: user.emailContact.email,
contributionMemo: contribution.memo, contributionMemo: contribution.memo,
message,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
}) })
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
} catch (e) { } catch (e) {

View File

@ -2,6 +2,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { cleanDB, resetToken, testEnvironment } from '@test/helpers' import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
import { logger, i18n as localization } from '@test/testSetup'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { import {
adminCreateContributionMessage, adminCreateContributionMessage,
@ -13,12 +14,16 @@ import { listContributionMessages } from '@/seeds/graphql/queries'
import { userFactory } from '@/seeds/factory/user' import { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { peterLustig } from '@/seeds/users/peter-lustig' 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 { return {
__esModule: true, __esModule: true,
sendAddedContributionMessageEmail: jest.fn(), ...originalModule,
sendAddedContributionMessageEmail: jest.fn((a) =>
originalModule.sendAddedContributionMessageEmail(a),
),
} }
}) })
@ -27,7 +32,7 @@ let testEnv: any
let result: any let result: any
beforeAll(async () => { beforeAll(async () => {
testEnv = await testEnvironment() testEnv = await testEnvironment(logger, localization)
mutate = testEnv.mutate mutate = testEnv.mutate
con = testEnv.con con = testEnv.con
await cleanDB() await cleanDB()
@ -162,15 +167,13 @@ describe('ContributionMessageResolver', () => {
it('calls sendAddedContributionMessageEmail', async () => { it('calls sendAddedContributionMessageEmail', async () => {
expect(sendAddedContributionMessageEmail).toBeCalledWith({ expect(sendAddedContributionMessageEmail).toBeCalledWith({
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
language: 'de',
senderFirstName: 'Peter', senderFirstName: 'Peter',
senderLastName: 'Lustig', senderLastName: 'Lustig',
recipientFirstName: 'Bibi',
recipientLastName: 'Bloxberg',
recipientEmail: 'bibi@bloxberg.de',
senderEmail: 'peter@lustig.de',
contributionMemo: 'Test env contribution', contributionMemo: 'Test env contribution',
message: 'Admin Test',
overviewURL: 'http://localhost/overview',
}) })
}) })
}) })

View File

@ -2,14 +2,11 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import CONFIG from '@/config'
import { Context, getUser } from '@/server/context' import { Context, getUser } from '@/server/context'
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql' import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
import { getCustomRepository, getConnection, In } from '@dbTools/typeorm' import { getCustomRepository, getConnection, In } from '@dbTools/typeorm'
import { sendTransactionReceivedEmail } from '@/mailer/sendTransactionReceivedEmail'
import { Transaction } from '@model/Transaction' import { Transaction } from '@model/Transaction'
import { TransactionList } from '@model/TransactionList' import { TransactionList } from '@model/TransactionList'
@ -36,7 +33,10 @@ import Decimal from 'decimal.js-light'
import { BalanceResolver } from './BalanceResolver' import { BalanceResolver } from './BalanceResolver'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
import { findUserByEmail } from './UserResolver' import { findUserByEmail } from './UserResolver'
import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed' import {
sendTransactionLinkRedeemedEmail,
sendTransactionReceivedEmail,
} from '@/emails/sendEmailVariants'
import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event' import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event'
import { eventProtocol } from '@/event/EventProtocolEmitter' import { eventProtocol } from '@/event/EventProtocolEmitter'
@ -159,29 +159,27 @@ export const executeTransaction = async (
await queryRunner.release() await queryRunner.release()
} }
logger.debug(`prepare Email for transaction received...`) logger.debug(`prepare Email for transaction received...`)
// send notification email
// TODO: translate
await sendTransactionReceivedEmail({ await sendTransactionReceivedEmail({
firstName: recipient.firstName,
lastName: recipient.lastName,
email: recipient.emailContact.email,
language: recipient.language,
senderFirstName: sender.firstName, senderFirstName: sender.firstName,
senderLastName: sender.lastName, senderLastName: sender.lastName,
recipientFirstName: recipient.firstName,
recipientLastName: recipient.lastName,
email: recipient.emailContact.email,
senderEmail: sender.emailContact.email, senderEmail: sender.emailContact.email,
amount, transactionAmount: amount,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
}) })
if (transactionLink) { if (transactionLink) {
await sendTransactionLinkRedeemedEmail({ await sendTransactionLinkRedeemedEmail({
firstName: sender.firstName,
lastName: sender.lastName,
email: sender.emailContact.email,
language: sender.language,
senderFirstName: recipient.firstName, senderFirstName: recipient.firstName,
senderLastName: recipient.lastName, senderLastName: recipient.lastName,
recipientFirstName: sender.firstName,
recipientLastName: sender.lastName,
email: sender.emailContact.email,
senderEmail: recipient.emailContact.email, senderEmail: recipient.emailContact.email,
amount, transactionAmount: amount,
memo, transactionMemo: memo,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
}) })
} }
logger.info(`finished executeTransaction successfully`) logger.info(`finished executeTransaction successfully`)

View File

@ -2,6 +2,8 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers' 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 { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { import {
@ -18,18 +20,18 @@ import { verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queri
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { User } from '@entity/User' import { User } from '@entity/User'
import CONFIG from '@/config' import CONFIG from '@/config'
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' import {
import { sendAccountMultiRegistrationEmail } from '@/emails/sendEmailVariants' sendAccountActivationEmail,
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' sendAccountMultiRegistrationEmail,
import { printTimeDuration, activationLink } from './UserResolver' sendResetPasswordEmail,
} from '@/emails/sendEmailVariants'
import { activationLink } from './UserResolver'
import { contributionLinkFactory } from '@/seeds/factory/contributionLink' import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
import { transactionLinkFactory } from '@/seeds/factory/transactionLink' import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
import { ContributionLink } from '@model/ContributionLink' import { ContributionLink } from '@model/ContributionLink'
import { TransactionLink } from '@entity/TransactionLink' import { TransactionLink } from '@entity/TransactionLink'
import { EventProtocolType } from '@/event/EventProtocolType' import { EventProtocolType } from '@/event/EventProtocolType'
import { EventProtocol } from '@entity/EventProtocol' import { EventProtocol } from '@entity/EventProtocol'
import { logger, i18n as localization } from '@test/testSetup'
import { validate as validateUUID, version as versionUUID } from 'uuid' import { validate as validateUUID, version as versionUUID } from 'uuid'
import { peterLustig } from '@/seeds/users/peter-lustig' import { peterLustig } from '@/seeds/users/peter-lustig'
import { UserContact } from '@entity/UserContact' import { UserContact } from '@entity/UserContact'
@ -42,24 +44,16 @@ import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils'
// import { klicktippSignIn } from '@/apis/KlicktippController' // import { klicktippSignIn } from '@/apis/KlicktippController'
jest.mock('@/mailer/sendAccountActivationEmail', () => {
return {
__esModule: true,
sendAccountActivationEmail: jest.fn(),
}
})
jest.mock('@/emails/sendEmailVariants', () => { jest.mock('@/emails/sendEmailVariants', () => {
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
return { return {
__esModule: true, __esModule: true,
sendAccountMultiRegistrationEmail: jest.fn(), ...originalModule,
} sendAccountActivationEmail: jest.fn((a) => originalModule.sendAccountActivationEmail(a)),
}) sendAccountMultiRegistrationEmail: jest.fn((a) =>
originalModule.sendAccountMultiRegistrationEmail(a),
jest.mock('@/mailer/sendResetPasswordEmail', () => { ),
return { sendResetPasswordEmail: jest.fn((a) => originalModule.sendResetPasswordEmail(a)),
__esModule: true,
sendResetPasswordEmail: jest.fn(),
} }
}) })
@ -184,11 +178,15 @@ describe('UserResolver', () => {
emailVerificationCode, emailVerificationCode,
).replace(/{code}/g, '') ).replace(/{code}/g, '')
expect(sendAccountActivationEmail).toBeCalledWith({ expect(sendAccountActivationEmail).toBeCalledWith({
link: activationLink,
firstName: 'Peter', firstName: 'Peter',
lastName: 'Lustig', lastName: 'Lustig',
email: 'peter@lustig.de', email: 'peter@lustig.de',
duration: expect.any(String), language: 'de',
activationLink,
timeDurationObject: expect.objectContaining({
hours: expect.any(Number),
minutes: expect.any(Number),
}),
}) })
}) })
@ -853,11 +851,15 @@ describe('UserResolver', () => {
it('sends reset password email', () => { it('sends reset password email', () => {
expect(sendResetPasswordEmail).toBeCalledWith({ expect(sendResetPasswordEmail).toBeCalledWith({
link: activationLink(emailContact.emailVerificationCode),
firstName: 'Bibi', firstName: 'Bibi',
lastName: 'Bloxberg', lastName: 'Bloxberg',
email: 'bibi@bloxberg.de', 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),
}),
}) })
}) })

View File

@ -9,6 +9,7 @@ import { User } from '@model/User'
import { User as DbUser } from '@entity/User' import { User as DbUser } from '@entity/User'
import { UserContact as DbUserContact } from '@entity/UserContact' import { UserContact as DbUserContact } from '@entity/UserContact'
import { communityDbUser } from '@/util/communityUser' import { communityDbUser } from '@/util/communityUser'
import { getTimeDurationObject, printTimeDuration } from '@/util/time'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink' import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
import { encode } from '@/auth/JWT' import { encode } from '@/auth/JWT'
@ -17,9 +18,11 @@ import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs' import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs'
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware' import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
import { OptInType } from '@enum/OptInType' import { OptInType } from '@enum/OptInType'
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail' import {
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' sendAccountActivationEmail,
import { sendAccountMultiRegistrationEmail } from '@/emails/sendEmailVariants' sendAccountMultiRegistrationEmail,
sendResetPasswordEmail,
} from '@/emails/sendEmailVariants'
import { klicktippSignIn } from '@/apis/KlicktippController' import { klicktippSignIn } from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { hasElopageBuys } from '@/util/hasElopageBuys' import { hasElopageBuys } from '@/util/hasElopageBuys'
@ -507,11 +510,12 @@ export class UserResolver {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendAccountActivationEmail({ const emailSent = await sendAccountActivationEmail({
link: activationLink,
firstName, firstName,
lastName, lastName,
email, 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}`) logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
eventSendConfirmEmail.userId = dbUser.id eventSendConfirmEmail.userId = dbUser.id
@ -570,12 +574,13 @@ export class UserResolver {
// optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD) // optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
logger.info(`optInCode for ${email}=${dbUserContact}`) logger.info(`optInCode for ${email}=${dbUserContact}`)
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendResetPasswordEmailMailer({ const emailSent = await sendResetPasswordEmail({
link: activationLink(dbUserContact.emailVerificationCode),
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
email, 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 */ /* uncomment this, when you need the activation link on the console */
@ -913,20 +918,3 @@ const canResendOptIn = (optIn: LoginEmailOptIn): boolean => {
const canEmailResend = (updatedAt: Date): boolean => { const canEmailResend = (updatedAt: Date): boolean => {
return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME) return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME)
} }
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
}

View File

@ -1,15 +1,61 @@
{ {
"emails": { "emails": {
"addedContributionMessage": {
"commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.",
"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:",
"subject": "Gradido: E-Mail Überprüfung"
},
"accountMultiRegistration": { "accountMultiRegistration": {
"emailExists": "Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto.", "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.",
"helloName": "Hallo {firstName} {lastName}",
"ifYouAreNotTheOne": "Wenn du nicht derjenige bist, der sich versucht hat erneut zu registrieren, wende dich bitte an unseren support:", "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:", "onForgottenPasswordClickLink": "Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest:",
"onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.",
"sincerelyYours": "Mit freundlichen Grüßen,", "subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail"
"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.",
"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": {
"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.",
"pleaseDoNotReply": "Bitte antworte nicht auf diese E-Mail!",
"sincerelyYours": "Liebe Grüße",
"yourGradidoTeam": "dein Gradido-Team" "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": {
"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": {
"decimalSeparator": ","
} }
} }

View File

@ -1,15 +1,61 @@
{ {
"emails": { "emails": {
"accountMultiRegistration": { "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!"
},
"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:",
"subject": "Gradido: Email Verification"
},
"accountMultiRegistration": {
"emailExists": "However, an account already exists for your email address.", "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.", "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:", "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:", "onForgottenPasswordClickLink": "Please click on the following link if you have forgotten your password:",
"onForgottenPasswordCopyLink": "or copy the link above into your browser window.", "onForgottenPasswordCopyLink": "or copy the link above into your browser window.",
"sincerelyYours": "Sincerely yours,", "subject": "Gradido: Try To Register Again With Your Email"
"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.",
"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": {
"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.",
"pleaseDoNotReply": "Please do not reply to this email!",
"sincerelyYours": "Kind regards,",
"yourGradidoTeam": "your Gradido team" "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": {
"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": {
"decimalSeparator": "."
}
}

View File

@ -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 <peter@lustig.de>`,
subject: 'Gradido: E-Mail Überprüfung',
text:
expect.stringContaining('Hallo Peter Lustig') &&
expect.stringContaining('activationLink') &&
expect.stringContaining('23 Stunden und 30 Minuten'),
})
})
})

View File

@ -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<boolean> => {
return sendEMail({
to: `${data.firstName} ${data.lastName} <${data.email}>`,
subject: accountActivation.de.subject,
text: accountActivation.de.text({ ...data, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD }),
})
}

View File

@ -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 <bibi@bloxberg.de>`,
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'),
})
})
})

View File

@ -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<boolean> => {
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),
})
}

View File

@ -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 <bibi@bloxberg.de>',
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'),
})
})
})

View File

@ -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<boolean> => {
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),
})
}

View File

@ -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 <bibi@bloxberg.de>',
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'),
})
})
})

View File

@ -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<boolean> => {
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),
})
}

View File

@ -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',
})
})
})
})

View File

@ -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<boolean> => {
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
}

View File

@ -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 <peter@lustig.de>`,
subject: 'Gradido: Passwort zurücksetzen',
text:
expect.stringContaining('Hallo Peter Lustig') &&
expect.stringContaining('resetLink') &&
expect.stringContaining('23 Stunden und 30 Minuten'),
})
})
})

View File

@ -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<boolean> => {
return sendEMail({
to: `${data.firstName} ${data.lastName} <${data.email}>`,
subject: resetPassword.de.subject,
text: resetPassword.de.text({ ...data, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD }),
})
}

View File

@ -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 <bibi@bloxberg.de>`,
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!'),
})
})
})

View File

@ -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<boolean> => {
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),
})
}

View File

@ -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 <peter@lustig.de>`,
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'),
})
})
})

View File

@ -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<boolean> => {
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),
})
}

View File

@ -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`,
},
}

View File

@ -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`,
},
}

View File

@ -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`,
},
}

View File

@ -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`,
},
}

View File

@ -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`,
},
}

View File

@ -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`,
},
}

View File

@ -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`,
},
}

View File

@ -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`,
},
}

16
backend/src/util/time.ts Normal file
View File

@ -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
}

View File

@ -1,5 +1,16 @@
import Decimal from 'decimal.js-light'
import i18n from 'i18n'
export const objectValuesToArray = (obj: { [x: string]: string }): Array<string> => { export const objectValuesToArray = (obj: { [x: string]: string }): Array<string> => {
return Object.keys(obj).map(function (key) { return Object.keys(obj).map(function (key) {
return obj[key] return obj[key]
}) })
} }
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
}

View File

@ -1,6 +1,10 @@
import CONFIG from '@/config'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import { i18n } from '@/server/localization' import { i18n } from '@/server/localization'
CONFIG.EMAIL = true
CONFIG.EMAIL_TEST_MODUS = false
jest.setTimeout(1000000) jest.setTimeout(1000000)
jest.mock('@/server/logger', () => { jest.mock('@/server/logger', () => {

View File

@ -108,7 +108,7 @@ services:
#env_file: #env_file:
# - ./frontend/.env # - ./frontend/.env
volumes: volumes:
# <host_machine_directy>:<container_directory> mirror bidirectional path in local context with path in Docker container # <host_machine_directory>:<container_directory> mirror bidirectional path in local context with path in Docker container
- ./logs/backend:/logs/backend - ./logs/backend:/logs/backend
######################################################## ########################################################