diff --git a/CHANGELOG.md b/CHANGELOG.md index 358e4670a..4bfc66e39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,57 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [1.18.2](https://github.com/gradido/gradido/compare/1.18.1...1.18.2) + +- fix(admin): deny contribution button to left [`#2699`](https://github.com/gradido/gradido/pull/2699) + +#### [1.18.1](https://github.com/gradido/gradido/compare/1.18.0...1.18.1) + +> 10 February 2023 + +- chore(release): version 1.18.1 [`#2698`](https://github.com/gradido/gradido/pull/2698) +- fix(frontend): fix is last month for empty form date [`#2697`](https://github.com/gradido/gradido/pull/2697) +- fix(frontend): community link [`#2696`](https://github.com/gradido/gradido/pull/2696) + +#### [1.18.0](https://github.com/gradido/gradido/compare/1.17.1...1.18.0) + +> 9 February 2023 + +- feat(release): version 1.18.0 [`#2690`](https://github.com/gradido/gradido/pull/2690) +- refactor(frontend): toast by automatically logged out [`#2681`](https://github.com/gradido/gradido/pull/2681) +- refactor(frontend): change text for gdd_per_link.choose-amount [`#2638`](https://github.com/gradido/gradido/pull/2638) +- fix(backend): emails for deny and delete contribution [`#2688`](https://github.com/gradido/gradido/pull/2688) +- refactor(other): remove config version from `.env.dist` [`#2686`](https://github.com/gradido/gradido/pull/2686) +- refactor(backend): use LogError on contributionMessageResolver [`#2663`](https://github.com/gradido/gradido/pull/2663) +- refactor(backend): get last transaction by only one function [`#2668`](https://github.com/gradido/gradido/pull/2668) +- refactor(other): don't rebuild modul if unit test file has been changed [`#2667`](https://github.com/gradido/gradido/pull/2667) +- refactor(backend): use LogError on contributionLinkResolver [`#2662`](https://github.com/gradido/gradido/pull/2662) +- refactor(backend): remove event protocol config switch [`#2670`](https://github.com/gradido/gradido/pull/2670) +- refactor(backend): event protocol [`#2652`](https://github.com/gradido/gradido/pull/2652) +- refactor(frontend): sidebar becomes smaller when critical phase [`#2649`](https://github.com/gradido/gradido/pull/2649) +- refactor(backend): use LogError on sendEMailTranslated [`#2656`](https://github.com/gradido/gradido/pull/2656) +- refactor(backend): log error class [`#2640`](https://github.com/gradido/gradido/pull/2640) +- feat(backend): add filterState parameter to listAllContributions query [`#2619`](https://github.com/gradido/gradido/pull/2619) +- refactor(frontend): there is no message when a month is fully created [`#2626`](https://github.com/gradido/gradido/pull/2626) +- refactor(frontend): better text alignment on send via link [`#2637`](https://github.com/gradido/gradido/pull/2637) +- refactor(frontend): text changed as indicated in the issues [`#2642`](https://github.com/gradido/gradido/pull/2642) +- refactor(frontend): when you click on create, you will be directed to the form [`#2645`](https://github.com/gradido/gradido/pull/2645) +- feat(backend): federation: separated dht-hub features in new dht-node modul [`#2510`](https://github.com/gradido/gradido/pull/2510) +- refactor(backend): refine assembly of error message in user resolver [`#2636`](https://github.com/gradido/gradido/pull/2636) +- fix(backend): unit tests creations for 31st day [`#2641`](https://github.com/gradido/gradido/pull/2641) +- fix(workflow): properly lint pr - prevent requirement to restart linting [`#2635`](https://github.com/gradido/gradido/pull/2635) +- feat(frontend): 'yes'-button shows which dialog is currently open with a different color [`#2629`](https://github.com/gradido/gradido/pull/2629) +- feat(backend): federation implement multiple apollo graphql endpoints [`#2459`](https://github.com/gradido/gradido/pull/2459) +- refactor(frontend): add legend to all contribution tab, and add tests. [`#2625`](https://github.com/gradido/gradido/pull/2625) +- feat(frontend): unit tests community page [`#2587`](https://github.com/gradido/gradido/pull/2587) +- feat(backend): deny contributions [`#2461`](https://github.com/gradido/gradido/pull/2461) +- refactor(admin): update yarn.lock file of admin. [`#2579`](https://github.com/gradido/gradido/pull/2579) + #### [1.17.1](https://github.com/gradido/gradido/compare/1.17.0...1.17.1) +> 20 January 2023 + +- chore(release): v1.17.1 [`#2588`](https://github.com/gradido/gradido/pull/2588) - refactor(frontend): change contribution memo add word-break [`#2583`](https://github.com/gradido/gradido/pull/2583) - refactor(admin): add text-break on all table memo fields [`#2584`](https://github.com/gradido/gradido/pull/2584) - fix(frontend): throw proper frontend warning errors [`#2586`](https://github.com/gradido/gradido/pull/2586) 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/admin/package.json b/admin/package.json index 8270c4da6..941a9bf69 100644 --- a/admin/package.json +++ b/admin/package.json @@ -3,7 +3,7 @@ "description": "Administraion Interface for Gradido", "main": "index.js", "author": "Moriz Wahl", - "version": "1.17.1", + "version": "1.18.2", "license": "Apache-2.0", "private": false, "scripts": { @@ -86,5 +86,10 @@ "> 1%", "last 2 versions", "not ie <= 10" - ] + ], + "nodemonConfig": { + "ignore": [ + "**/*.spec.js" + ] + } } diff --git a/admin/src/pages/CreationConfirm.spec.js b/admin/src/pages/CreationConfirm.spec.js index d47233ded..99dbda219 100644 --- a/admin/src/pages/CreationConfirm.spec.js +++ b/admin/src/pages/CreationConfirm.spec.js @@ -259,7 +259,7 @@ describe('CreationConfirm', () => { describe('deny creation', () => { beforeEach(async () => { - await wrapper.findAll('tr').at(1).findAll('button').at(2).trigger('click') + await wrapper.findAll('tr').at(1).findAll('button').at(1).trigger('click') }) it('opens the overlay', () => { diff --git a/admin/src/pages/CreationConfirm.vue b/admin/src/pages/CreationConfirm.vue index e87dfc247..c6576e5ba 100644 --- a/admin/src/pages/CreationConfirm.vue +++ b/admin/src/pages/CreationConfirm.vue @@ -129,6 +129,7 @@ export default { fields() { return [ { key: 'bookmark', label: this.$t('delete') }, + { key: 'deny', label: this.$t('deny') }, { key: 'email', label: this.$t('e_mail') }, { key: 'firstName', label: this.$t('firstname') }, { key: 'lastName', label: this.$t('lastname') }, @@ -149,7 +150,6 @@ export default { }, { key: 'moderator', label: this.$t('moderator') }, { key: 'editCreation', label: this.$t('edit') }, - { key: 'deny', label: this.$t('deny') }, { key: 'confirm', label: this.$t('save') }, ] }, diff --git a/backend/.env.dist b/backend/.env.dist index 73308656c..b6216f8e9 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -1,5 +1,3 @@ -CONFIG_VERSION=v16.2023-02-02 - # Server PORT=4000 JWT_SECRET=secret123 diff --git a/backend/package.json b/backend/package.json index 8f0675170..859485ed7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "gradido-backend", - "version": "1.17.1", + "version": "1.18.2", "description": "Gradido unified backend providing an API-Service for Gradido Transactions", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/backend", @@ -72,5 +72,10 @@ "ts-node": "^10.0.0", "tsconfig-paths": "^3.14.0", "typescript": "^4.3.4" + }, + "nodemonConfig": { + "ignore": [ + "**/*.test.ts" + ] } } 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/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index 26f9cd656..65cccf4d4 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -15,6 +15,8 @@ import { calculateDecay } from '@/util/decay' import { RIGHTS } from '@/auth/RIGHTS' import { GdtResolver } from './GdtResolver' +import { getLastTransaction } from './util/getLastTransaction' + @Resolver() export class BalanceResolver { @Authorized([RIGHTS.BALANCE]) @@ -32,7 +34,7 @@ export class BalanceResolver { const lastTransaction = context.lastTransaction ? context.lastTransaction - : await dbTransaction.findOne({ userId: user.id }, { order: { id: 'DESC' } }) + : await getLastTransaction(user.id) logger.debug(`lastTransaction=${lastTransaction}`) 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..4a74029ad 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, @@ -46,8 +42,8 @@ import { User } from '@entity/User' import { EventProtocolType } from '@/event/EventProtocolType' import { logger, i18n as localization } from '@test/testSetup' import { UserInputError } from 'apollo-server-express' +import { ContributionStatus } from '../enum/ContributionStatus' -// 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') @@ -132,13 +128,13 @@ describe('ContributionResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('memo text is too short (5 characters minimum)')], + errors: [new GraphQLError('Memo text is too short')], }), ) }) it('logs the error found', () => { - expect(logger.error).toBeCalledWith(`memo text is too short: memo.length=4 < 5`) + expect(logger.error).toBeCalledWith('Memo text is too short', 4) }) it('throws error when memo length greater than 255 chars', async () => { @@ -155,13 +151,13 @@ describe('ContributionResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('memo text is too long (255 characters maximum)')], + errors: [new GraphQLError('Memo text is too long')], }), ) }) it('logs the error found', () => { - expect(logger.error).toBeCalledWith(`memo text is too long: memo.length=259 > 255`) + expect(logger.error).toBeCalledWith('Memo text is too long', 259) }) it('throws error when creationDate not-valid', async () => { @@ -422,31 +418,6 @@ describe('ContributionResolver', () => { resetToken() }) - describe('wrong contribution id', () => { - it('throws an error', async () => { - jest.clearAllMocks() - await expect( - mutate({ - mutation: updateContribution, - variables: { - contributionId: -1, - amount: 100.0, - memo: 'Test env contribution', - creationDate: new Date().toString(), - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('No contribution found to given id.')], - }), - ) - }) - - it('logs the error found', () => { - expect(logger.error).toBeCalledWith('No contribution found to given id') - }) - }) - describe('Memo length smaller than 5 chars', () => { it('throws error', async () => { jest.clearAllMocks() @@ -463,13 +434,13 @@ describe('ContributionResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('memo text is too short (5 characters minimum)')], + errors: [new GraphQLError('Memo text is too short')], }), ) }) it('logs the error found', () => { - expect(logger.error).toBeCalledWith('memo text is too short: memo.length=4 < 5') + expect(logger.error).toBeCalledWith('Memo text is too short', 4) }) }) @@ -489,13 +460,38 @@ describe('ContributionResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('memo text is too long (255 characters maximum)')], + errors: [new GraphQLError('Memo text is too long')], }), ) }) it('logs the error found', () => { - expect(logger.error).toBeCalledWith('memo text is too long: memo.length=259 > 255') + expect(logger.error).toBeCalledWith('Memo text is too long', 259) + }) + }) + + describe('wrong contribution id', () => { + it('throws an error', async () => { + jest.clearAllMocks() + await expect( + mutate({ + mutation: updateContribution, + variables: { + contributionId: -1, + amount: 100.0, + memo: 'Test env contribution', + creationDate: new Date().toString(), + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Contribution not found')], + }), + ) + }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith('Contribution not found', -1) }) }) @@ -521,18 +517,16 @@ describe('ContributionResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [ - new GraphQLError( - 'user of the pending contribution and send user does not correspond', - ), - ], + errors: [new GraphQLError('Can not update contribution of another user')], }), ) }) it('logs the error found', () => { expect(logger.error).toBeCalledWith( - 'user of the pending contribution and send user does not correspond', + 'Can not update contribution of another user', + expect.any(Object), + expect.any(Number), ) }) }) @@ -553,12 +547,64 @@ describe('ContributionResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('An admin is not allowed to update a user contribution.')], + errors: [new GraphQLError('An admin is not allowed to update an user contribution')], }), ) }) - // TODO check that the error is logged (need to modify AdminResolver, avoid conflicts) + it('logs the error found', () => { + expect(logger.error).toBeCalledWith( + 'An admin is not allowed to update an user contribution', + ) + }) + }) + + describe('contribution has wrong status', () => { + beforeAll(async () => { + const contribution = await Contribution.findOneOrFail({ + id: result.data.createContribution.id, + }) + contribution.contributionStatus = ContributionStatus.DELETED + contribution.save() + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + const contribution = await Contribution.findOneOrFail({ + id: result.data.createContribution.id, + }) + contribution.contributionStatus = ContributionStatus.PENDING + contribution.save() + }) + + it('throws an error', async () => { + jest.clearAllMocks() + await expect( + mutate({ + mutation: updateContribution, + variables: { + contributionId: result.data.createContribution.id, + amount: 10.0, + memo: 'Test env contribution', + creationDate: new Date().toString(), + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Contribution can not be updated due to status')], + }), + ) + }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith( + 'Contribution can not be updated due to status', + ContributionStatus.DELETED, + ) + }) }) describe('update too much so that the limit is exceeded', () => { @@ -615,16 +661,13 @@ describe('ContributionResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('Currently the month of the contribution cannot change.')], + errors: [new GraphQLError('Month of contribution can not be changed')], }), ) }) - it.skip('logs the error found', () => { - expect(logger.error).toBeCalledWith( - 'No information for available creations with the given creationDate=', - 'Invalid Date', - ) + it('logs the error found', () => { + expect(logger.error).toBeCalledWith('Month of contribution can not be changed') }) }) @@ -1158,6 +1201,7 @@ describe('ContributionResolver', () => { describe('wrong contribution id', () => { it('returns an error', async () => { + jest.clearAllMocks() await expect( mutate({ mutation: deleteContribution, @@ -1167,18 +1211,19 @@ describe('ContributionResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('Contribution not found for given id.')], + errors: [new GraphQLError('Contribution not found')], }), ) }) it('logs the error found', () => { - expect(logger.error).toBeCalledWith('Contribution not found for given id') + expect(logger.error).toBeCalledWith('Contribution not found', -1) }) }) describe('other user sends a deleteContribution', () => { it('returns an error', async () => { + jest.clearAllMocks() await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, @@ -1198,7 +1243,11 @@ describe('ContributionResolver', () => { }) it('logs the error found', () => { - expect(logger.error).toBeCalledWith('Can not delete contribution of another user') + expect(logger.error).toBeCalledWith( + 'Can not delete contribution of another user', + expect.any(Object), + expect.any(Number), + ) }) }) @@ -1274,7 +1323,10 @@ describe('ContributionResolver', () => { }) it('logs the error found', () => { - expect(logger.error).toBeCalledWith('A confirmed contribution can not be deleted') + expect(logger.error).toBeCalledWith( + 'A confirmed contribution can not be deleted', + expect.objectContaining({ contributionStatus: 'CONFIRMED' }), + ) }) }) }) @@ -1540,15 +1592,13 @@ describe('ContributionResolver', () => { mutate({ mutation: adminCreateContribution, variables }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('Could not find user with email: bibi@bloxberg.de')], + errors: [new GraphQLError('Could not find user')], }), ) }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'Could not find user with email: bibi@bloxberg.de', - ) + expect(logger.error).toBeCalledWith('Could not find user', 'bibi@bloxberg.de') }) }) @@ -1568,7 +1618,7 @@ describe('ContributionResolver', () => { ).resolves.toEqual( expect.objectContaining({ errors: [ - new GraphQLError('This user was deleted. Cannot create a contribution.'), + new GraphQLError('Cannot create contribution since the user was deleted'), ], }), ) @@ -1576,7 +1626,12 @@ describe('ContributionResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'This user was deleted. Cannot create a contribution.', + 'Cannot create contribution since the user was deleted', + expect.objectContaining({ + user: expect.objectContaining({ + deletedAt: new Date('2018-03-14T09:17:52.000Z'), + }), + }), ) }) }) @@ -1597,7 +1652,9 @@ describe('ContributionResolver', () => { ).resolves.toEqual( expect.objectContaining({ errors: [ - new GraphQLError('Contribution could not be saved, Email is not activated'), + new GraphQLError( + 'Cannot create contribution since the users email is not activated', + ), ], }), ) @@ -1605,7 +1662,8 @@ describe('ContributionResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'Contribution could not be saved, Email is not activated', + 'Cannot create contribution since the users email is not activated', + expect.objectContaining({ emailChecked: false }), ) }) }) @@ -1624,13 +1682,13 @@ describe('ContributionResolver', () => { mutate({ mutation: adminCreateContribution, variables }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError(`invalid Date for creationDate=invalid-date`)], + errors: [new GraphQLError('CreationDate is invalid')], }), ) }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith(`invalid Date for creationDate=invalid-date`) + expect(logger.error).toBeCalledWith('CreationDate is invalid', 'invalid-date') }) }) @@ -1826,17 +1884,13 @@ describe('ContributionResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [ - new GraphQLError('Could not find UserContact with email: bob@baumeister.de'), - ], + errors: [new GraphQLError('Could not find User')], }), ) }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'Could not find UserContact with email: bob@baumeister.de', - ) + expect(logger.error).toBeCalledWith('Could not find User', 'bob@baumeister.de') }) }) @@ -1856,13 +1910,13 @@ describe('ContributionResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('User was deleted (stephen@hawking.uk)')], + errors: [new GraphQLError('User was deleted')], }), ) }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('User was deleted (stephen@hawking.uk)') + expect(logger.error).toBeCalledWith('User was deleted', 'stephen@hawking.uk') }) }) @@ -1882,13 +1936,13 @@ describe('ContributionResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('No contribution found to given id.')], + errors: [new GraphQLError('Contribution not found')], }), ) }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('No contribution found to given id.') + expect(logger.error).toBeCalledWith('Contribution not found', -1) }) }) @@ -1912,7 +1966,7 @@ describe('ContributionResolver', () => { expect.objectContaining({ errors: [ new GraphQLError( - 'user of the pending contribution and send user does not correspond', + 'User of the pending contribution and send user does not correspond', ), ], }), @@ -1921,7 +1975,7 @@ describe('ContributionResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'user of the pending contribution and send user does not correspond', + 'User of the pending contribution and send user does not correspond', ) }) }) @@ -2116,13 +2170,13 @@ describe('ContributionResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('Contribution not found for given id.')], + errors: [new GraphQLError('Contribution not found')], }), ) }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Contribution not found for given id: -1') + expect(logger.error).toBeCalledWith('Contribution not found', -1) }) }) @@ -2242,13 +2296,13 @@ describe('ContributionResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('Contribution not found to given id.')], + errors: [new GraphQLError('Contribution not found')], }), ) }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Contribution not found for given id: -1') + expect(logger.error).toBeCalledWith('Contribution not found', -1) }) }) @@ -2359,6 +2413,7 @@ describe('ContributionResolver', () => { describe('confirm same contribution again', () => { it('throws an error', async () => { + jest.clearAllMocks() await expect( mutate({ mutation: confirmContribution, @@ -2368,11 +2423,18 @@ describe('ContributionResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('Contribution already confirmd.')], + errors: [new GraphQLError('Contribution already confirmed')], }), ) }) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'Contribution already confirmed', + expect.any(Number), + ) + }) }) describe('confirm two creations one after the other quickly', () => { diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index bf80bcb4d..926742d8a 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -44,15 +44,20 @@ 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' +import LogError from '@/server/LogError' + +import { getLastTransaction } from './util/getLastTransaction' @Resolver() export class ContributionResolver { @@ -63,14 +68,11 @@ export class ContributionResolver { @Ctx() context: Context, ): Promise { const clientTimezoneOffset = getClientTimezoneOffset(context) - if (memo.length > MEMO_MAX_CHARS) { - logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`) - throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`) - } - if (memo.length < MEMO_MIN_CHARS) { - logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`) - throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`) + throw new LogError('Memo text is too short', memo.length) + } + if (memo.length > MEMO_MAX_CHARS) { + throw new LogError('Memo text is too long', memo.length) } const event = new Event() @@ -112,16 +114,13 @@ export class ContributionResolver { const user = getUser(context) const contribution = await DbContribution.findOne(id) if (!contribution) { - logger.error('Contribution not found for given id') - throw new Error('Contribution not found for given id.') + throw new LogError('Contribution not found', id) } if (contribution.userId !== user.id) { - logger.error('Can not delete contribution of another user') - throw new Error('Can not delete contribution of another user') + throw new LogError('Can not delete contribution of another user', contribution, user.id) } if (contribution.confirmedAt) { - logger.error('A confirmed contribution can not be deleted') - throw new Error('A confirmed contribution can not be deleted') + throw new LogError('A confirmed contribution can not be deleted', contribution) } contribution.contributionStatus = ContributionStatus.DELETED @@ -215,14 +214,11 @@ export class ContributionResolver { @Ctx() context: Context, ): Promise { const clientTimezoneOffset = getClientTimezoneOffset(context) - if (memo.length > MEMO_MAX_CHARS) { - logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`) - throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`) - } - if (memo.length < MEMO_MIN_CHARS) { - logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`) - throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`) + throw new LogError('Memo text is too short', memo.length) + } + if (memo.length > MEMO_MAX_CHARS) { + throw new LogError('Memo text is too long', memo.length) } const user = getUser(context) @@ -231,22 +227,22 @@ export class ContributionResolver { where: { id: contributionId, confirmedAt: IsNull(), deniedAt: IsNull() }, }) if (!contributionToUpdate) { - logger.error('No contribution found to given id') - throw new Error('No contribution found to given id.') + throw new LogError('Contribution not found', contributionId) } if (contributionToUpdate.userId !== user.id) { - logger.error('user of the pending contribution and send user does not correspond') - throw new Error('user of the pending contribution and send user does not correspond') + throw new LogError( + 'Can not update contribution of another user', + contributionToUpdate, + user.id, + ) } if ( contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS && contributionToUpdate.contributionStatus !== ContributionStatus.PENDING ) { - logger.error( - `Contribution can not be updated since the state is ${contributionToUpdate.contributionStatus}`, - ) - throw new Error( - `Contribution can not be updated since the state is ${contributionToUpdate.contributionStatus}`, + throw new LogError( + 'Contribution can not be updated due to status', + contributionToUpdate.contributionStatus, ) } const creationDateObj = new Date(creationDate) @@ -254,8 +250,7 @@ export class ContributionResolver { if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) { creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset) } else { - logger.error('Currently the month of the contribution cannot change.') - throw new Error('Currently the month of the contribution cannot change.') + throw new LogError('Month of contribution can not be changed') } // all possible cases not to be true are thrown in this function @@ -306,29 +301,24 @@ export class ContributionResolver { ) const clientTimezoneOffset = getClientTimezoneOffset(context) if (!isValidDateString(creationDate)) { - logger.error(`invalid Date for creationDate=${creationDate}`) - throw new Error(`invalid Date for creationDate=${creationDate}`) + throw new LogError('CreationDate is invalid', creationDate) } const emailContact = await UserContact.findOne({ where: { email }, withDeleted: true, relations: ['user'], }) - if (!emailContact) { - logger.error(`Could not find user with email: ${email}`) - throw new Error(`Could not find user with email: ${email}`) + if (!emailContact || !emailContact.user) { + throw new LogError('Could not find user', email) } - if (emailContact.deletedAt) { - logger.error('This emailContact was deleted. Cannot create a contribution.') - throw new Error('This emailContact was deleted. Cannot create a contribution.') - } - if (emailContact.user.deletedAt) { - logger.error('This user was deleted. Cannot create a contribution.') - throw new Error('This user was deleted. Cannot create a contribution.') + if (emailContact.deletedAt || emailContact.user.deletedAt) { + throw new LogError('Cannot create contribution since the user was deleted', emailContact) } if (!emailContact.emailChecked) { - logger.error('Contribution could not be saved, Email is not activated') - throw new Error('Contribution could not be saved, Email is not activated') + throw new LogError( + 'Cannot create contribution since the users email is not activated', + emailContact, + ) } const event = new Event() @@ -401,18 +391,11 @@ export class ContributionResolver { withDeleted: true, relations: ['user'], }) - if (!emailContact) { - logger.error(`Could not find UserContact with email: ${email}`) - throw new Error(`Could not find UserContact with email: ${email}`) + if (!emailContact || !emailContact.user) { + throw new LogError('Could not find User', email) } - const user = emailContact.user - if (!user) { - logger.error(`Could not find User to emailContact: ${email}`) - throw new Error(`Could not find User to emailContact: ${email}`) - } - if (user.deletedAt) { - logger.error(`User was deleted (${email})`) - throw new Error(`User was deleted (${email})`) + if (emailContact.deletedAt || emailContact.user.deletedAt) { + throw new LogError('User was deleted', email) } const moderator = getUser(context) @@ -421,28 +404,25 @@ export class ContributionResolver { where: { id, confirmedAt: IsNull(), deniedAt: IsNull() }, }) if (!contributionToUpdate) { - logger.error('No contribution found to given id.') - throw new Error('No contribution found to given id.') + throw new LogError('Contribution not found', id) } - if (contributionToUpdate.userId !== user.id) { - logger.error('user of the pending contribution and send user does not correspond') - throw new Error('user of the pending contribution and send user does not correspond') + if (contributionToUpdate.userId !== emailContact.user.id) { + throw new LogError('User of the pending contribution and send user does not correspond') } if (contributionToUpdate.moderatorId === null) { - logger.error('An admin is not allowed to update a user contribution.') - throw new Error('An admin is not allowed to update a user contribution.') + throw new LogError('An admin is not allowed to update an user contribution') } const creationDateObj = new Date(creationDate) - let creations = await getUserCreation(user.id, clientTimezoneOffset) + let creations = await getUserCreation(emailContact.user.id, clientTimezoneOffset) + // TODO: remove this restriction if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) { creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset) } else { - logger.error('Currently the month of the contribution cannot change.') - throw new Error('Currently the month of the contribution cannot change.') + throw new LogError('Month of contribution can not be changed') } // all possible cases not to be true are thrown in this function @@ -460,11 +440,11 @@ export class ContributionResolver { result.memo = contributionToUpdate.memo result.date = contributionToUpdate.contributionDate - result.creation = await getUserCreation(user.id, clientTimezoneOffset) + result.creation = await getUserCreation(emailContact.user.id, clientTimezoneOffset) const event = new Event() const eventAdminContributionUpdate = new EventAdminContributionUpdate() - eventAdminContributionUpdate.userId = user.id + eventAdminContributionUpdate.userId = emailContact.user.id eventAdminContributionUpdate.amount = amount eventAdminContributionUpdate.contributionId = contributionToUpdate.id await writeEvent(event.setEventAdminContributionUpdate(eventAdminContributionUpdate)) @@ -517,19 +497,17 @@ export class ContributionResolver { ): Promise { const contribution = await DbContribution.findOne(id) if (!contribution) { - logger.error(`Contribution not found for given id: ${id}`) - throw new Error('Contribution not found for given id.') + throw new LogError('Contribution not found', id) } if (contribution.confirmedAt) { - logger.error('A confirmed contribution can not be deleted') - throw new Error('A confirmed contribution can not be deleted') + throw new LogError('A confirmed contribution can not be deleted') } const moderator = getUser(context) if ( contribution.contributionType === ContributionType.USER && contribution.userId === moderator.id ) { - throw new Error('Own contribution can not be deleted as admin') + throw new LogError('Own contribution can not be deleted as admin') } const user = await DbUser.findOneOrFail( { id: contribution.userId }, @@ -546,7 +524,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, @@ -571,29 +549,24 @@ export class ContributionResolver { const clientTimezoneOffset = getClientTimezoneOffset(context) const contribution = await DbContribution.findOne(id) if (!contribution) { - logger.error(`Contribution not found for given id: ${id}`) - throw new Error('Contribution not found to given id.') + throw new LogError('Contribution not found', id) } if (contribution.confirmedAt) { - logger.error(`Contribution already confirmd: ${id}`) - throw new Error('Contribution already confirmd.') + throw new LogError('Contribution already confirmed', id) } if (contribution.contributionStatus === 'DENIED') { - logger.error(`Contribution already denied: ${id}`) - throw new Error('Contribution already denied.') + throw new LogError('Contribution already denied', id) } const moderatorUser = getUser(context) if (moderatorUser.id === contribution.userId) { - logger.error('Moderator can not confirm own contribution') - throw new Error('Moderator can not confirm own contribution') + throw new LogError('Moderator can not confirm own contribution') } const user = await DbUser.findOneOrFail( { id: contribution.userId }, { withDeleted: true, relations: ['emailContact'] }, ) if (user.deletedAt) { - logger.error('This user was deleted. Cannot confirm a contribution.') - throw new Error('This user was deleted. Cannot confirm a contribution.') + throw new LogError('Can not confirm contribution since the user was deleted') } const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false) validateContribution( @@ -607,16 +580,11 @@ export class ContributionResolver { const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED') - try { - const lastTransaction = await queryRunner.manager - .createQueryBuilder() - .select('transaction') - .from(DbTransaction, 'transaction') - .where('transaction.userId = :id', { id: contribution.userId }) - .orderBy('transaction.id', 'DESC') - .getOne() - logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined') + const lastTransaction = await getLastTransaction(contribution.userId) + logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined') + + try { let newBalance = new Decimal(0) let decay: Decay | null = null if (lastTransaction) { @@ -662,8 +630,7 @@ export class ContributionResolver { }) } catch (e) { await queryRunner.rollbackTransaction() - logger.error('Creation was not successful', e) - throw new Error('Creation was not successful.') + throw new LogError('Creation was not successful', e) } finally { await queryRunner.release() } @@ -738,17 +705,16 @@ export class ContributionResolver { deniedBy: IsNull(), }) if (!contributionToUpdate) { - logger.error(`Contribution not found for given id: ${id}`) - throw new Error(`Contribution not found for given id.`) + throw new LogError('Contribution not found', id) } if ( contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS && contributionToUpdate.contributionStatus !== ContributionStatus.PENDING ) { - logger.error( - `Contribution state (${contributionToUpdate.contributionStatus}) is not allowed.`, + throw new LogError( + 'Status of the contribution is not allowed', + contributionToUpdate.contributionStatus, ) - throw new Error(`State of the contribution is not allowed.`) } const moderator = getUser(context) const user = await DbUser.findOne( @@ -756,10 +722,7 @@ export class ContributionResolver { { relations: ['emailContact'] }, ) if (!user) { - logger.error( - `Could not find User for the Contribution (userId: ${contributionToUpdate.userId}).`, - ) - throw new Error('Could not find User for the Contribution.') + throw new LogError('Could not find User of the Contribution', contributionToUpdate.userId) } contributionToUpdate.contributionStatus = ContributionStatus.DENIED @@ -767,6 +730,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/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index df70b4bc9..b3376c65f 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -33,6 +33,8 @@ import { executeTransaction } from './TransactionResolver' import QueryLinkResult from '@union/QueryLinkResult' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' +import { getLastTransaction } from './util/getLastTransaction' + // TODO: do not export, test it inside the resolver export const transactionLinkCode = (date: Date): string => { const time = date.getTime().toString(16) @@ -275,13 +277,7 @@ export class TransactionLinkResolver { await queryRunner.manager.insert(DbContribution, contribution) - const lastTransaction = await queryRunner.manager - .createQueryBuilder() - .select('transaction') - .from(DbTransaction, 'transaction') - .where('transaction.userId = :id', { id: user.id }) - .orderBy('transaction.id', 'DESC') - .getOne() + const lastTransaction = await getLastTransaction(user.id) let newBalance = new Decimal(0) let decay: Decay | null = null diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index b75782abf..bedf8c533 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -38,6 +38,8 @@ import { findUserByEmail } from './UserResolver' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' +import { getLastTransaction } from './util/getLastTransaction' + export const executeTransaction = async ( amount: Decimal, memo: string, @@ -206,10 +208,7 @@ export class TransactionResolver { logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`) // find current balance - const lastTransaction = await dbTransaction.findOne( - { userId: user.id }, - { order: { id: 'DESC' }, relations: ['contribution'] }, - ) + const lastTransaction = await getLastTransaction(user.id, ['contribution']) logger.debug(`lastTransaction=${lastTransaction}`) const balanceResolver = new BalanceResolver() diff --git a/backend/src/graphql/resolver/util/getLastTransaction.ts b/backend/src/graphql/resolver/util/getLastTransaction.ts new file mode 100644 index 000000000..5b3e862c2 --- /dev/null +++ b/backend/src/graphql/resolver/util/getLastTransaction.ts @@ -0,0 +1,14 @@ +import { Transaction as DbTransaction } from '@entity/Transaction' + +export const getLastTransaction = async ( + userId: number, + relations?: string[], +): Promise => { + return DbTransaction.findOne( + { userId }, + { + order: { balanceDate: 'DESC', id: 'DESC' }, + relations, + }, + ) +} 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/backend/src/util/validate.ts b/backend/src/util/validate.ts index 397a38730..482e9eb50 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -1,10 +1,10 @@ import { calculateDecay } from './decay' import Decimal from 'decimal.js-light' -import { Transaction } from '@entity/Transaction' import { Decay } from '@model/Decay' import { getCustomRepository } from '@dbTools/typeorm' import { TransactionLinkRepository } from '@repository/TransactionLink' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' +import { getLastTransaction } from '../graphql/resolver/util/getLastTransaction' function isStringBoolean(value: string): boolean { const lowerValue = value.toLowerCase() @@ -20,7 +20,7 @@ async function calculateBalance( time: Date, transactionLink?: dbTransactionLink | null, ): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> { - const lastTransaction = await Transaction.findOne({ userId }, { order: { id: 'DESC' } }) + const lastTransaction = await getLastTransaction(userId) if (!lastTransaction) return null const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) 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/database/package.json b/database/package.json index f4e1c7e84..5be01a5d5 100644 --- a/database/package.json +++ b/database/package.json @@ -1,6 +1,6 @@ { "name": "gradido-database", - "version": "1.17.1", + "version": "1.18.2", "description": "Gradido Database Tool to execute database migrations", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/database", diff --git a/dht-node/.env.dist b/dht-node/.env.dist index 82050b2af..5d287ee1a 100644 --- a/dht-node/.env.dist +++ b/dht-node/.env.dist @@ -1,5 +1,3 @@ -CONFIG_VERSION=v2.2023-02-03 - # 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 diff --git a/frontend/package.json b/frontend/package.json index 29c440988..9aa457c19 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "bootstrap-vue-gradido-wallet", - "version": "1.17.1", + "version": "1.18.2", "private": true, "scripts": { "start": "node run/server.js", @@ -104,5 +104,10 @@ ], "author": "Gradido-Akademie - https://www.gradido.net/", "license": "Apache-2.0", - "description": "Gradido, the Natural Economy of Life, is a way to worldwide prosperity and peace in harmony with nature. - Gradido, die Natürliche Ökonomie des lebens, ist ein Weg zu weltweitem Wohlstand und Frieden in Harmonie mit der Natur." + "description": "Gradido, the Natural Economy of Life, is a way to worldwide prosperity and peace in harmony with nature. - Gradido, die Natürliche Ökonomie des lebens, ist ein Weg zu weltweitem Wohlstand und Frieden in Harmonie mit der Natur.", + "nodemonConfig": { + "ignore": [ + "**/*.spec.js" + ] + } } diff --git a/frontend/src/components/Contributions/ContributionForm.spec.js b/frontend/src/components/Contributions/ContributionForm.spec.js index 020e3f552..71ead88b9 100644 --- a/frontend/src/components/Contributions/ContributionForm.spec.js +++ b/frontend/src/components/Contributions/ContributionForm.spec.js @@ -24,11 +24,6 @@ describe('ContributionForm', () => { $t: jest.fn((t) => t), $d: jest.fn((d) => d), $n: jest.fn((n) => n), - $store: { - state: { - creation: ['1000', '1000', '1000'], - }, - }, $i18n: { locale: 'en', }, @@ -61,7 +56,7 @@ describe('ContributionForm', () => { }) }) - describe('dates', () => { + describe('dates and max amounts', () => { beforeEach(async () => { await wrapper.setData({ form: { @@ -73,204 +68,176 @@ describe('ContributionForm', () => { }) }) - describe('actual date', () => { - describe('same month', () => { - beforeEach(async () => { - const now = new Date().toISOString() - await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now) + describe('max amount reached for both months', () => { + beforeEach(() => { + wrapper.setProps({ + maxGddLastMonth: 0, + maxGddThisMonth: 0, }) - - describe('isThisMonth', () => { - it('has true', () => { - expect(wrapper.vm.isThisMonth).toBe(true) - }) + wrapper.setData({ + form: { + id: null, + date: 'set', + memo: '', + amount: '', + }, }) }) - describe.skip('month before', () => { - beforeEach(async () => { - await wrapper - .findComponent({ name: 'BFormDatepicker' }) - .vm.$emit('input', wrapper.vm.minimalDate) - }) - - describe('isThisMonth', () => { - it('has false', () => { - expect(wrapper.vm.isThisMonth).toBe(false) - }) - }) + it('shows message that no contributions are available', () => { + expect(wrapper.find('[data-test="contribtion-message"]').text()).toBe( + 'contribution.noOpenCreation.allMonth', + ) }) }) - describe.skip('date in middle of year', () => { - describe('same month', () => { - beforeEach(async () => { - // jest.useFakeTimers('modern') - // jest.setSystemTime(new Date('2020-07-06')) - // await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now) - await wrapper.setData({ - maximalDate: new Date(2020, 6, 6), - form: { date: new Date(2020, 6, 6) }, - }) - }) - - describe('minimalDate', () => { - it('has "2020-06-01T00:00:00.000Z"', () => { - expect(wrapper.vm.minimalDate.toISOString()).toBe('2020-06-01T00:00:00.000Z') - }) - }) - - describe('isThisMonth', () => { - it('has true', () => { - expect(wrapper.vm.isThisMonth).toBe(true) - }) + describe('max amount reached for last month, no date selected', () => { + beforeEach(() => { + wrapper.setProps({ + maxGddLastMonth: 0, }) }) - describe('month before', () => { - beforeEach(async () => { - // jest.useFakeTimers('modern') - // jest.setSystemTime(new Date('2020-07-06')) - // console.log('middle of year date – now:', wrapper.vm.minimalDate) - // await wrapper - // .findComponent({ name: 'BFormDatepicker' }) - // .vm.$emit('input', wrapper.vm.minimalDate) - await wrapper.setData({ - maximalDate: new Date(2020, 6, 6), - form: { date: new Date(2020, 5, 6) }, - }) - }) - - describe('minimalDate', () => { - it('has "2020-06-01T00:00:00.000Z"', () => { - expect(wrapper.vm.minimalDate.toISOString()).toBe('2020-06-01T00:00:00.000Z') - }) - }) - - describe('isThisMonth', () => { - it('has false', () => { - expect(wrapper.vm.isThisMonth).toBe(false) - }) - }) + it('shows no message', () => { + expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false) }) }) - describe.skip('date in january', () => { - describe('same month', () => { - beforeEach(async () => { - await wrapper.setData({ - maximalDate: new Date(2020, 0, 6), - form: { date: new Date(2020, 0, 6) }, - }) + describe('max amount reached for last month, last month selected', () => { + beforeEach(async () => { + wrapper.setProps({ + maxGddLastMonth: 0, + isThisMonth: false, }) - - describe('minimalDate', () => { - it('has "2019-12-01T00:00:00.000Z"', () => { - expect(wrapper.vm.minimalDate.toISOString()).toBe('2019-12-01T00:00:00.000Z') - }) - }) - - describe('isThisMonth', () => { - it('has true', () => { - expect(wrapper.vm.isThisMonth).toBe(true) - }) + await wrapper.setData({ + form: { + id: null, + date: 'set', + memo: '', + amount: '', + }, }) }) - describe('month before', () => { - beforeEach(async () => { - // jest.useFakeTimers('modern') - // jest.setSystemTime(new Date('2020-07-06')) - // console.log('middle of year date – now:', wrapper.vm.minimalDate) - // await wrapper - // .findComponent({ name: 'BFormDatepicker' }) - // .vm.$emit('input', wrapper.vm.minimalDate) - await wrapper.setData({ - maximalDate: new Date(2020, 0, 6), - form: { date: new Date(2019, 11, 6) }, - }) - }) - - describe('minimalDate', () => { - it('has "2019-12-01T00:00:00.000Z"', () => { - expect(wrapper.vm.minimalDate.toISOString()).toBe('2019-12-01T00:00:00.000Z') - }) - }) - - describe('isThisMonth', () => { - it('has false', () => { - expect(wrapper.vm.isThisMonth).toBe(false) - }) - }) + it('shows message that no contributions are available for last month', () => { + expect(wrapper.find('[data-test="contribtion-message"]').text()).toBe( + 'contribution.noOpenCreation.lastMonth', + ) }) }) - describe.skip('date with the 31st day of the month', () => { - describe('same month', () => { - beforeEach(async () => { - await wrapper.setData({ - maximalDate: new Date('2022-10-31T00:00:00.000Z'), - form: { date: new Date('2022-10-31T00:00:00.000Z') }, - }) + describe('max amount reached for last month, this month selected', () => { + beforeEach(async () => { + wrapper.setProps({ + maxGddLastMonth: 0, + isThisMonth: true, }) + await wrapper.setData({ + form: { + id: null, + date: 'set', + memo: '', + amount: '', + }, + }) + }) - describe('minimalDate', () => { - it('has "2022-09-01T00:00:00.000Z"', () => { - expect(wrapper.vm.minimalDate.toISOString()).toBe('2022-09-01T00:00:00.000Z') - }) - }) - - describe('isThisMonth', () => { - it('has true', () => { - expect(wrapper.vm.isThisMonth).toBe(true) - }) - }) + it('shows no message', () => { + expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false) }) }) - describe.skip('date with the 28th day of the month', () => { - describe('same month', () => { - beforeEach(async () => { - await wrapper.setData({ - maximalDate: new Date('2023-02-28T00:00:00.000Z'), - form: { date: new Date('2023-02-28T00:00:00.000Z') }, - }) + describe('max amount reached for this month, no date selected', () => { + beforeEach(() => { + wrapper.setProps({ + maxGddThisMonth: 0, }) + }) - describe('minimalDate', () => { - it('has "2023-01-01T00:00:00.000Z"', () => { - expect(wrapper.vm.minimalDate.toISOString()).toBe('2023-01-01T00:00:00.000Z') - }) - }) - - describe('isThisMonth', () => { - it('has true', () => { - expect(wrapper.vm.isThisMonth).toBe(true) - }) - }) + it('shows no message', () => { + expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false) }) }) - describe.skip('date with 29.02.2024 leap year', () => { - describe('same month', () => { - beforeEach(async () => { - await wrapper.setData({ - maximalDate: new Date('2024-02-29T00:00:00.000Z'), - form: { date: new Date('2024-02-29T00:00:00.000Z') }, - }) + describe('max amount reached for this month, this month selected', () => { + beforeEach(async () => { + wrapper.setProps({ + maxGddThisMonth: 0, + isThisMonth: true, }) + await wrapper.setData({ + form: { + id: null, + date: 'set', + memo: '', + amount: '', + }, + }) + }) - describe('minimalDate', () => { - it('has "2024-01-01T00:00:00.000Z"', () => { - expect(wrapper.vm.minimalDate.toISOString()).toBe('2024-01-01T00:00:00.000Z') - }) - }) + it('shows message that no contributions are available for last month', () => { + expect(wrapper.find('[data-test="contribtion-message"]').text()).toBe( + 'contribution.noOpenCreation.thisMonth', + ) + }) + }) - describe('isThisMonth', () => { - it('has true', () => { - expect(wrapper.vm.isThisMonth).toBe(true) - }) + describe('max amount reached for this month, last month selected', () => { + beforeEach(async () => { + wrapper.setProps({ + maxGddThisMonth: 0, + isThisMonth: false, }) + await wrapper.setData({ + form: { + id: null, + date: 'set', + memo: '', + amount: '', + }, + }) + }) + + it('shows no message', () => { + expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false) + }) + }) + }) + + describe('default return message', () => { + it('returns an empty string', () => { + expect(wrapper.vm.noOpenCreation).toBe('') + }) + }) + + describe('update amount', () => { + beforeEach(() => { + wrapper.findComponent({ name: 'InputHour' }).vm.$emit('updateAmount', 20) + }) + + it('updates form amount', () => { + expect(wrapper.vm.form.amount).toBe('400.00') + }) + }) + + describe('watch value', () => { + beforeEach(() => { + wrapper.setProps({ + value: { + id: 42, + date: 'set', + memo: 'Some Memo', + amount: '400.00', + }, + }) + }) + + it('updates form', () => { + expect(wrapper.vm.form).toEqual({ + id: 42, + date: 'set', + memo: 'Some Memo', + amount: '400.00', }) }) }) @@ -477,24 +444,23 @@ describe('ContributionForm', () => { }) }) - describe.skip('on trigger submit', () => { + describe('on trigger submit', () => { beforeEach(async () => { await wrapper.find('form').trigger('submit') }) it('emits "update-contribution"', () => { - expect(wrapper.emitted('update-contribution')).toEqual( - expect.arrayContaining([ - expect.arrayContaining([ - { - id: 2, - date: now, - memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', - amount: '200', - }, - ]), - ]), - ) + expect(wrapper.emitted('update-contribution')).toEqual([ + [ + { + id: 2, + date: now, + hours: 0, + memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', + amount: '200', + }, + ], + ]) }) }) }) diff --git a/frontend/src/components/Contributions/ContributionForm.vue b/frontend/src/components/Contributions/ContributionForm.vue index 9a2ffb54b..0d512ab63 100644 --- a/frontend/src/components/Contributions/ContributionForm.vue +++ b/frontend/src/components/Contributions/ContributionForm.vue @@ -23,10 +23,7 @@ -
+
{{ noOpenCreation }}
@@ -118,8 +115,8 @@ export default { } }, methods: { - updateAmount(amount) { - this.form.amount = (amount * 20).toFixed(2).toString() + updateAmount(hours) { + this.form.amount = (hours * 20).toFixed(2).toString() }, submit() { this.$emit(this.form.id ? 'update-contribution' : 'set-contribution', { ...this.form }) @@ -135,6 +132,15 @@ export default { }, }, computed: { + showMessage() { + if (this.maxGddThisMonth <= 0 && this.maxGddLastMonth <= 0) return true + if (this.form.date) + return ( + (this.isThisMonth && this.maxGddThisMonth <= 0) || + (!this.isThisMonth && this.maxGddLastMonth <= 0) + ) + return false + }, disabled() { return ( this.form.date === '' || diff --git a/frontend/src/components/Contributions/ContributionList.spec.js b/frontend/src/components/Contributions/ContributionList.spec.js index a1dfc934d..de875cf74 100644 --- a/frontend/src/components/Contributions/ContributionList.spec.js +++ b/frontend/src/components/Contributions/ContributionList.spec.js @@ -116,5 +116,15 @@ describe('ContributionList', () => { expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 2 }]]) }) }) + + describe('update status', () => { + beforeEach(() => { + wrapper.findComponent({ name: 'ContributionListItem' }).vm.$emit('update-state', { id: 2 }) + }) + + it('emits update status', () => { + expect(wrapper.emitted('update-state')).toEqual([[{ id: 2 }]]) + }) + }) }) }) diff --git a/frontend/src/components/SessionLogoutTimeout.vue b/frontend/src/components/SessionLogoutTimeout.vue index 1ebff752a..7a11d1d83 100644 --- a/frontend/src/components/SessionLogoutTimeout.vue +++ b/frontend/src/components/SessionLogoutTimeout.vue @@ -67,6 +67,7 @@ export default { } if (this.tokenExpiresInSeconds === 0) { this.$timer.stop('tokenExpires') + this.toastInfoNoHide(this.$t('session.automaticallyLoggedOut')) this.$emit('logout') } }, @@ -84,6 +85,7 @@ export default { }) .catch(() => { this.$timer.stop('tokenExpires') + this.toastInfoNoHide(this.$t('session.automaticallyLoggedOut')) this.$emit('logout') }) }, diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index eff4237ca..c826b9e30 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -173,7 +173,7 @@ "GDD": "GDD", "gddKonto": "GDD Konto", "gdd_per_link": { - "choose-amount": "Wähle einen Betrag aus, welchen du per Link versenden möchtest. Du kannst auch noch eine Nachricht eintragen. Beim Klick „Jetzt generieren“ wird ein Link erstellt, den du versenden kannst.", + "choose-amount": "Wähle einen Betrag aus, welchen du per Link versenden möchtest, und trage eine Nachricht ein. Die Nachricht ist ein Pflichtfeld.", "copy-link": "Link kopieren", "copy-link-with-text": "Link und Text kopieren", "created": "Der Link wurde erstellt!", @@ -272,6 +272,7 @@ "send_gdd": "GDD versenden", "send_per_link": "GDD versenden per Link", "session": { + "automaticallyLoggedOut": "Du wurdest automatisch abgemeldet", "extend": "Angemeldet bleiben", "lightText": "Wenn du länger als 10 Minuten keine Aktion getätigt hast, wirst du aus Sicherheitsgründen abgemeldet.", "logoutIn": "Abmelden in ", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 068282891..193aff666 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -173,7 +173,7 @@ "GDD": "GDD", "gddKonto": "GDD Konto", "gdd_per_link": { - "choose-amount": "Select an amount that you would like to send via link. You can also enter a message. Click 'Generate now' to create a link that you can share.", + "choose-amount": "Select an amount you want to send via link and enter a message. The message is mandatory.", "copy-link": "Copy link", "copy-link-with-text": "Copy link and text", "created": "Link was created!", @@ -272,6 +272,7 @@ "send_gdd": "Send GDD", "send_per_link": "Send GDD via Link", "session": { + "automaticallyLoggedOut": "You have been automatically logged out.", "extend": "Stay logged in", "lightText": "If you have not performed any action for more than 10 minutes, you will be logged out for security reasons.", "logoutIn": "Log out in ", diff --git a/frontend/src/mixins/toaster.js b/frontend/src/mixins/toaster.js index 175ffeec0..6ed73b697 100644 --- a/frontend/src/mixins/toaster.js +++ b/frontend/src/mixins/toaster.js @@ -18,11 +18,18 @@ export const toasters = { variant: 'warning', }) }, + toastInfoNoHide(message) { + this.toast(message, { + title: this.$t('navigation.info'), + variant: 'warning', + noAutoHide: true, + }) + }, toast(message, options) { if (message.replace) message = message.replace(/^GraphQL error: /, '') this.$root.$bvToast.toast(message, { - autoHideDelay: 5000, appendToast: true, + autoHideDelay: 5000, solid: true, toaster: 'b-toaster-top-right', headerClass: 'gdd-toaster-title', diff --git a/frontend/src/pages/Community.spec.js b/frontend/src/pages/Community.spec.js index ecfc01716..b4d1677df 100644 --- a/frontend/src/pages/Community.spec.js +++ b/frontend/src/pages/Community.spec.js @@ -70,6 +70,8 @@ describe('Community', () => { lastName: 'Bloxberg', state: 'IN_PROGRESS', messagesCount: 0, + deniedAt: null, + deniedBy: null, }, { id: 1550, @@ -84,6 +86,8 @@ describe('Community', () => { lastName: 'Bloxberg', state: 'CONFIRMED', messagesCount: 0, + deniedAt: null, + deniedBy: null, }, ], contributionCount: 1, @@ -112,6 +116,10 @@ describe('Community', () => { confirmedAt: null, firstName: 'Bibi', lastName: 'Bloxberg', + deniedAt: null, + deniedBy: null, + messagesCount: 0, + state: 'IN_PROGRESS', }, { id: 1550, @@ -124,7 +132,10 @@ describe('Community', () => { firstName: 'Bibi', contributionDate: '2022-06-15T08:47:06.000Z', lastName: 'Bloxberg', + deniedAt: null, + deniedBy: null, messagesCount: 0, + state: 'IN_PROGRESS', }, { id: 1556, @@ -137,6 +148,10 @@ describe('Community', () => { confirmedAt: null, firstName: 'Bob', lastName: 'der Baumeister', + deniedAt: null, + deniedBy: null, + messagesCount: 0, + state: 'IN_PROGRESS', }, ], contributionCount: 3, diff --git a/frontend/src/pages/InfoStatistic.vue b/frontend/src/pages/InfoStatistic.vue index b27166dc1..1bfb53d4a 100644 --- a/frontend/src/pages/InfoStatistic.vue +++ b/frontend/src/pages/InfoStatistic.vue @@ -6,9 +6,9 @@ {{ CONFIG.COMMUNITY_DESCRIPTION }}
- + {{ CONFIG.COMMUNITY_URL }} - +

{{ $t('community.openContributionLinks') }}
diff --git a/package.json b/package.json index 7a01da338..2220c1a85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gradido", - "version": "1.17.1", + "version": "1.18.2", "description": "Gradido", "main": "index.js", "repository": "git@github.com:gradido/gradido.git",