diff --git a/admin/.env.dist b/admin/.env.dist index d7044669a..66c84dda8 100644 --- a/admin/.env.dist +++ b/admin/.env.dist @@ -1,5 +1,3 @@ -CONFIG_VERSION=v1.2022-03-18 - GRAPHQL_URI=http://localhost:4000/graphql WALLET_AUTH_URL=http://localhost/authenticate?token={token} WALLET_URL=http://localhost/login diff --git a/backend/.env.dist b/backend/.env.dist index 3e54b0566..8cf89ff0c 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -1,5 +1,3 @@ -CONFIG_VERSION=v14.2022-12-22 - # Server PORT=4000 JWT_SECRET=secret123 diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index ddbc387a1..7e499feb9 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -10,6 +10,7 @@ import { sendAccountMultiRegistrationEmail, sendContributionConfirmedEmail, sendContributionDeniedEmail, + sendContributionDeletedEmail, sendResetPasswordEmail, sendTransactionLinkRedeemedEmail, sendTransactionReceivedEmail, @@ -438,6 +439,84 @@ describe('sendEmailVariants', () => { }) }) + describe('sendContributionDeletedEmail', () => { + beforeAll(async () => { + result = await sendContributionDeletedEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'contributionDeleted', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL, + communityURL: CONFIG.COMMUNITY_URL, + }, + }) + }) + + it('has expected result', () => { + expect(result).toMatchObject({ + envelope: { + from: 'info@gradido.net', + to: ['peter@lustig.de'], + }, + message: expect.any(String), + originalMessage: expect.objectContaining({ + to: 'Peter Lustig ', + from: 'Gradido (do not answer) ', + attachments: [], + subject: 'Gradido: Your common good contribution was deleted', + html: expect.any(String), + text: expect.stringContaining('GRADIDO: YOUR COMMON GOOD CONTRIBUTION WAS DELETED'), + }), + }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain( + 'Gradido: Your common good contribution was deleted', + ) + expect(result.originalMessage.html).toContain( + '>Gradido: Your common good contribution was deleted', + ) + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'Your public good contribution “My contribution.” was deleted 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: ${CONFIG.EMAIL_LINK_OVERVIEW}`, + ) + expect(result.originalMessage.html).toContain('Please do not reply to this email!') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') + expect(result.originalMessage.html).toContain('—————') + expect(result.originalMessage.html).toContain( + '
Gradido-Akademie Logo

Gradido-Akademie
Institut für Wirtschaftsbionik
Pfarrweg 2
74653 Künzelsau
Deutschland
support@supportmail.com
http://localhost/', + ) + }) + }) + }) + describe('sendResetPasswordEmail', () => { beforeAll(async () => { result = await sendResetPasswordEmail({ diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index 681ee56af..4e3881829 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -103,6 +103,32 @@ export const sendContributionConfirmedEmail = (data: { }) } +export const sendContributionDeletedEmail = (data: { + firstName: string + lastName: string + email: string + language: string + senderFirstName: string + senderLastName: string + contributionMemo: string +}): Promise | null> => { + return sendEmailTranslated({ + receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, + template: 'contributionDeleted', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + senderFirstName: data.senderFirstName, + senderLastName: data.senderLastName, + contributionMemo: data.contributionMemo, + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL, + communityURL: CONFIG.COMMUNITY_URL, + }, + }) +} + export const sendContributionDeniedEmail = (data: { firstName: string lastName: string diff --git a/backend/src/emails/templates/contributionDeleted/html.pug b/backend/src/emails/templates/contributionDeleted/html.pug new file mode 100644 index 000000000..d6b3ea207 --- /dev/null +++ b/backend/src/emails/templates/contributionDeleted/html.pug @@ -0,0 +1,16 @@ +doctype html +html(lang=locale) + head + title= t('emails.contributionDeleted.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.contributionDeleted.subject') + #container.col + include ../hello.pug + p= t('emails.contributionDeleted.commonGoodContributionDeleted', { senderFirstName, senderLastName, contributionMemo }) + p= t('emails.contributionDeleted.toSeeContributionsAndMessages') + p + = t('emails.general.linkToYourAccount') + = " " + a(href=overviewURL) #{overviewURL} + p= t('emails.general.pleaseDoNotReply') + include ../greatingFormularImprint.pug diff --git a/backend/src/emails/templates/contributionDeleted/subject.pug b/backend/src/emails/templates/contributionDeleted/subject.pug new file mode 100644 index 000000000..024588472 --- /dev/null +++ b/backend/src/emails/templates/contributionDeleted/subject.pug @@ -0,0 +1 @@ += t('emails.contributionDeleted.subject') diff --git a/backend/src/event/Event.ts b/backend/src/event/Event.ts index 6a1233224..77e86ad46 100644 --- a/backend/src/event/Event.ts +++ b/backend/src/event/Event.ts @@ -67,6 +67,7 @@ export class EventTransactionReceiveRedeem extends EventBasicTxX {} export class EventContributionCreate extends EventBasicCt {} export class EventAdminContributionCreate extends EventBasicCt {} export class EventAdminContributionDelete extends EventBasicCt {} +export class EventAdminContributionDeny extends EventBasicCt {} export class EventAdminContributionUpdate extends EventBasicCt {} export class EventUserCreateContributionMessage extends EventBasicCtMsg {} export class EventAdminCreateContributionMessage extends EventBasicCtMsg {} @@ -298,6 +299,13 @@ export class Event { return this } + public setEventAdminContributionDeny(ev: EventAdminContributionDeny): Event { + this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) + this.type = EventProtocolType.ADMIN_CONTRIBUTION_DENY + + return this + } + public setEventAdminContributionUpdate(ev: EventAdminContributionUpdate): Event { this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) this.type = EventProtocolType.ADMIN_CONTRIBUTION_UPDATE diff --git a/backend/src/event/EventProtocolType.ts b/backend/src/event/EventProtocolType.ts index b7c2f0151..ccd15d238 100644 --- a/backend/src/event/EventProtocolType.ts +++ b/backend/src/event/EventProtocolType.ts @@ -35,6 +35,7 @@ export enum EventProtocolType { CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE', ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE', ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE', + ADMIN_CONTRIBUTION_DENY = 'ADMIN_CONTRIBUTION_DENY', ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE', USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE', ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE', diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts index 436830c2c..f3e5e865d 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts @@ -88,6 +88,7 @@ describe('ContributionMessageResolver', () => { describe('input not valid', () => { it('throws error when contribution does not exist', async () => { + jest.clearAllMocks() await expect( mutate({ mutation: adminCreateContributionMessage, @@ -100,14 +101,22 @@ describe('ContributionMessageResolver', () => { expect.objectContaining({ errors: [ new GraphQLError( - 'ContributionMessage was not successful: Error: Contribution not found', + 'ContributionMessage was not sent successfully: Error: Contribution not found', ), ], }), ) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'ContributionMessage was not sent successfully: Error: Contribution not found', + new Error('Contribution not found'), + ) + }) + it('throws error when contribution.userId equals user.id', async () => { + jest.clearAllMocks() await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, @@ -132,12 +141,19 @@ describe('ContributionMessageResolver', () => { expect.objectContaining({ errors: [ new GraphQLError( - 'ContributionMessage was not successful: Error: Admin can not answer on own contribution', + 'ContributionMessage was not sent successfully: Error: Admin can not answer on his own contribution', ), ], }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'ContributionMessage was not sent successfully: Error: Admin can not answer on his own contribution', + new Error('Admin can not answer on his own contribution'), + ) + }) }) describe('valid input', () => { @@ -210,6 +226,7 @@ describe('ContributionMessageResolver', () => { describe('input not valid', () => { it('throws error when contribution does not exist', async () => { + jest.clearAllMocks() await expect( mutate({ mutation: createContributionMessage, @@ -222,14 +239,22 @@ describe('ContributionMessageResolver', () => { expect.objectContaining({ errors: [ new GraphQLError( - 'ContributionMessage was not successful: Error: Contribution not found', + 'ContributionMessage was not sent successfully: Error: Contribution not found', ), ], }), ) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'ContributionMessage was not sent successfully: Error: Contribution not found', + new Error('Contribution not found'), + ) + }) + it('throws error when other user tries to send createContributionMessage', async () => { + jest.clearAllMocks() await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, @@ -246,12 +271,19 @@ describe('ContributionMessageResolver', () => { expect.objectContaining({ errors: [ new GraphQLError( - 'ContributionMessage was not successful: Error: Can not send message to contribution of another user', + 'ContributionMessage was not sent successfully: Error: Can not send message to contribution of another user', ), ], }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'ContributionMessage was not sent successfully: Error: Can not send message to contribution of another user', + new Error('Can not send message to contribution of another user'), + ) + }) }) describe('valid input', () => { diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.ts b/backend/src/graphql/resolver/ContributionMessageResolver.ts index 38bea804e..4248946b1 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.ts @@ -12,10 +12,10 @@ import { ContributionStatus } from '@enum/ContributionStatus' import { Order } from '@enum/Order' import Paginated from '@arg/Paginated' -import { backendLogger as logger } from '@/server/logger' import { RIGHTS } from '@/auth/RIGHTS' import { Context, getUser } from '@/server/context' import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants' +import LogError from '@/server/LogError' @Resolver() export class ContributionMessageResolver { @@ -54,8 +54,7 @@ export class ContributionMessageResolver { await queryRunner.commitTransaction() } catch (e) { await queryRunner.rollbackTransaction() - logger.error(`ContributionMessage was not successful: ${e}`) - throw new Error(`ContributionMessage was not successful: ${e}`) + throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e) } finally { await queryRunner.release() } @@ -95,9 +94,7 @@ export class ContributionMessageResolver { @Ctx() context: Context, ): Promise { const user = getUser(context) - if (!user.emailContact) { - user.emailContact = await UserContact.findOneOrFail({ where: { id: user.emailId } }) - } + const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() await queryRunner.startTransaction('REPEATABLE READ') @@ -108,12 +105,10 @@ export class ContributionMessageResolver { relations: ['user'], }) if (!contribution) { - logger.error('Contribution not found') - throw new Error('Contribution not found') + throw new LogError('Contribution not found', contributionId) } if (contribution.userId === user.id) { - logger.error('Admin can not answer on own contribution') - throw new Error('Admin can not answer on own contribution') + throw new LogError('Admin can not answer on his own contribution', contributionId) } if (!contribution.user.emailContact) { contribution.user.emailContact = await UserContact.findOneOrFail({ @@ -149,8 +144,7 @@ export class ContributionMessageResolver { await queryRunner.commitTransaction() } catch (e) { await queryRunner.rollbackTransaction() - logger.error(`ContributionMessage was not successful: ${e}`) - throw new Error(`ContributionMessage was not successful: ${e}`) + throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e) } finally { await queryRunner.release() } diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index d48502c69..03ad002ad 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -22,11 +22,7 @@ import { listContributions, listUnconfirmedContributions, } from '@/seeds/graphql/queries' -import { - // sendAccountActivationEmail, - sendContributionConfirmedEmail, - // sendContributionRejectedEmail, -} from '@/emails/sendEmailVariants' +import { sendContributionConfirmedEmail } from '@/emails/sendEmailVariants' import { cleanDB, resetToken, @@ -47,7 +43,6 @@ import { EventProtocolType } from '@/event/EventProtocolType' import { logger, i18n as localization } from '@test/testSetup' import { UserInputError } from 'apollo-server-express' -// mock account activation email to avoid console spam // mock account activation email to avoid console spam jest.mock('@/emails/sendEmailVariants', () => { const originalModule = jest.requireActual('@/emails/sendEmailVariants') diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 205d7086e..c46a49555 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -44,12 +44,14 @@ import { EventContributionConfirm, EventAdminContributionCreate, EventAdminContributionDelete, + EventAdminContributionDeny, EventAdminContributionUpdate, } from '@/event/Event' import { writeEvent } from '@/event/EventProtocolEmitter' import { calculateDecay } from '@/util/decay' import { sendContributionConfirmedEmail, + sendContributionDeletedEmail, sendContributionDeniedEmail, } from '@/emails/sendEmailVariants' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' @@ -548,7 +550,7 @@ export class ContributionResolver { eventAdminContributionDelete.amount = contribution.amount eventAdminContributionDelete.contributionId = contribution.id await writeEvent(event.setEventAdminContributionDelete(eventAdminContributionDelete)) - sendContributionDeniedEmail({ + sendContributionDeletedEmail({ firstName: user.firstName, lastName: user.lastName, email: user.emailContact.email, @@ -764,6 +766,13 @@ export class ContributionResolver { contributionToUpdate.deniedAt = new Date() const res = await contributionToUpdate.save() + const event = new Event() + const eventAdminContributionDeny = new EventAdminContributionDeny() + eventAdminContributionDeny.userId = contributionToUpdate.userId + eventAdminContributionDeny.amount = contributionToUpdate.amount + eventAdminContributionDeny.contributionId = contributionToUpdate.id + await writeEvent(event.setEventAdminContributionDeny(eventAdminContributionDeny)) + sendContributionDeniedEmail({ firstName: user.firstName, lastName: user.lastName, diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index 38b53508b..304ae2adc 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -23,8 +23,13 @@ "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.", + "contributionDeleted": { + "commonGoodContributionDeleted": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} gelöscht.", + "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde gelöscht", + "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“!" + }, + "contributionDenied": { + "commonGoodContributionDenied": "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“!" }, diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index 5cde70d26..bdc92b2cf 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -23,6 +23,11 @@ "commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.", "subject": "Gradido: Your contribution to the common good was confirmed" }, + "contributionDeleted": { + "commonGoodContributionDeleted": "Your public good contribution “{contributionMemo}” was deleted by {senderFirstName} {senderLastName}.", + "subject": "Gradido: Your common good contribution was deleted", + "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!" + }, "contributionDenied": { "commonGoodContributionDenied": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.", "subject": "Gradido: Your common good contribution was rejected", diff --git a/database/.env.dist b/database/.env.dist index 58362a7b9..689e4f509 100644 --- a/database/.env.dist +++ b/database/.env.dist @@ -1,5 +1,3 @@ -CONFIG_VERSION=v1.2022-03-18 - DB_HOST=localhost DB_PORT=3306 DB_USER=root diff --git a/dht-node/.env.dist b/dht-node/.env.dist index 09e25ccb6..e005ca748 100644 --- a/dht-node/.env.dist +++ b/dht-node/.env.dist @@ -1,5 +1,3 @@ -CONFIG_VERSION=v1.2023-01-01 - # Database DB_HOST=localhost DB_PORT=3306 diff --git a/frontend/.env.dist b/frontend/.env.dist index 5ce6b430d..427d43359 100644 --- a/frontend/.env.dist +++ b/frontend/.env.dist @@ -1,5 +1,3 @@ -CONFIG_VERSION=v4.2022-12-20 - # Environment DEFAULT_PUBLISHER_ID=2896