Merge remote-tracking branch 'origin/master' into

2501-feature-federation-implement-a-graphql-client-to-request-getpublickey
This commit is contained in:
Claus-Peter Hübner 2023-02-13 21:12:41 +01:00
commit a10de9b55a
39 changed files with 705 additions and 433 deletions

View File

@ -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). 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) #### [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(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) - 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) - fix(frontend): throw proper frontend warning errors [`#2586`](https://github.com/gradido/gradido/pull/2586)

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=v1.2022-03-18
GRAPHQL_URI=http://localhost:4000/graphql GRAPHQL_URI=http://localhost:4000/graphql
WALLET_AUTH_URL=http://localhost/authenticate?token={token} WALLET_AUTH_URL=http://localhost/authenticate?token={token}
WALLET_URL=http://localhost/login WALLET_URL=http://localhost/login

View File

@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido", "description": "Administraion Interface for Gradido",
"main": "index.js", "main": "index.js",
"author": "Moriz Wahl", "author": "Moriz Wahl",
"version": "1.17.1", "version": "1.18.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": false, "private": false,
"scripts": { "scripts": {
@ -86,5 +86,10 @@
"> 1%", "> 1%",
"last 2 versions", "last 2 versions",
"not ie <= 10" "not ie <= 10"
],
"nodemonConfig": {
"ignore": [
"**/*.spec.js"
] ]
}
} }

View File

@ -259,7 +259,7 @@ describe('CreationConfirm', () => {
describe('deny creation', () => { describe('deny creation', () => {
beforeEach(async () => { 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', () => { it('opens the overlay', () => {

View File

@ -129,6 +129,7 @@ export default {
fields() { fields() {
return [ return [
{ key: 'bookmark', label: this.$t('delete') }, { key: 'bookmark', label: this.$t('delete') },
{ key: 'deny', label: this.$t('deny') },
{ key: 'email', label: this.$t('e_mail') }, { key: 'email', label: this.$t('e_mail') },
{ key: 'firstName', label: this.$t('firstname') }, { key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') }, { key: 'lastName', label: this.$t('lastname') },
@ -149,7 +150,6 @@ export default {
}, },
{ key: 'moderator', label: this.$t('moderator') }, { key: 'moderator', label: this.$t('moderator') },
{ key: 'editCreation', label: this.$t('edit') }, { key: 'editCreation', label: this.$t('edit') },
{ key: 'deny', label: this.$t('deny') },
{ key: 'confirm', label: this.$t('save') }, { key: 'confirm', label: this.$t('save') },
] ]
}, },

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=v16.2023-02-02
# Server # Server
PORT=4000 PORT=4000
JWT_SECRET=secret123 JWT_SECRET=secret123

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-backend", "name": "gradido-backend",
"version": "1.17.1", "version": "1.18.2",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions", "description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend", "repository": "https://github.com/gradido/gradido/backend",
@ -72,5 +72,10 @@
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"tsconfig-paths": "^3.14.0", "tsconfig-paths": "^3.14.0",
"typescript": "^4.3.4" "typescript": "^4.3.4"
},
"nodemonConfig": {
"ignore": [
"**/*.test.ts"
]
} }
} }

View File

@ -10,6 +10,7 @@ import {
sendAccountMultiRegistrationEmail, sendAccountMultiRegistrationEmail,
sendContributionConfirmedEmail, sendContributionConfirmedEmail,
sendContributionDeniedEmail, sendContributionDeniedEmail,
sendContributionDeletedEmail,
sendResetPasswordEmail, sendResetPasswordEmail,
sendTransactionLinkRedeemedEmail, sendTransactionLinkRedeemedEmail,
sendTransactionReceivedEmail, 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 <peter@lustig.de>',
},
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 <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
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('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Your common good contribution was deleted</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Your common good contribution was deleted</h1>',
)
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: <a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
)
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
})
})
})
describe('sendResetPasswordEmail', () => { describe('sendResetPasswordEmail', () => {
beforeAll(async () => { beforeAll(async () => {
result = await sendResetPasswordEmail({ result = await sendResetPasswordEmail({

View File

@ -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<Record<string, unknown> | 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: { export const sendContributionDeniedEmail = (data: {
firstName: string firstName: string
lastName: string lastName: string

View File

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

View File

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

View File

@ -67,6 +67,7 @@ export class EventTransactionReceiveRedeem extends EventBasicTxX {}
export class EventContributionCreate extends EventBasicCt {} export class EventContributionCreate extends EventBasicCt {}
export class EventAdminContributionCreate extends EventBasicCt {} export class EventAdminContributionCreate extends EventBasicCt {}
export class EventAdminContributionDelete extends EventBasicCt {} export class EventAdminContributionDelete extends EventBasicCt {}
export class EventAdminContributionDeny extends EventBasicCt {}
export class EventAdminContributionUpdate extends EventBasicCt {} export class EventAdminContributionUpdate extends EventBasicCt {}
export class EventUserCreateContributionMessage extends EventBasicCtMsg {} export class EventUserCreateContributionMessage extends EventBasicCtMsg {}
export class EventAdminCreateContributionMessage extends EventBasicCtMsg {} export class EventAdminCreateContributionMessage extends EventBasicCtMsg {}
@ -298,6 +299,13 @@ export class Event {
return this 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 { public setEventAdminContributionUpdate(ev: EventAdminContributionUpdate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_CONTRIBUTION_UPDATE this.type = EventProtocolType.ADMIN_CONTRIBUTION_UPDATE

View File

@ -35,6 +35,7 @@ export enum EventProtocolType {
CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE', CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE',
ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE', ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE',
ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE', ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE',
ADMIN_CONTRIBUTION_DENY = 'ADMIN_CONTRIBUTION_DENY',
ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE', ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE',
USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE', USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE',
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE', ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',

View File

@ -15,6 +15,8 @@ import { calculateDecay } from '@/util/decay'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { GdtResolver } from './GdtResolver' import { GdtResolver } from './GdtResolver'
import { getLastTransaction } from './util/getLastTransaction'
@Resolver() @Resolver()
export class BalanceResolver { export class BalanceResolver {
@Authorized([RIGHTS.BALANCE]) @Authorized([RIGHTS.BALANCE])
@ -32,7 +34,7 @@ export class BalanceResolver {
const lastTransaction = context.lastTransaction const lastTransaction = context.lastTransaction
? context.lastTransaction ? context.lastTransaction
: await dbTransaction.findOne({ userId: user.id }, { order: { id: 'DESC' } }) : await getLastTransaction(user.id)
logger.debug(`lastTransaction=${lastTransaction}`) logger.debug(`lastTransaction=${lastTransaction}`)

View File

@ -88,6 +88,7 @@ describe('ContributionMessageResolver', () => {
describe('input not valid', () => { describe('input not valid', () => {
it('throws error when contribution does not exist', async () => { it('throws error when contribution does not exist', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: adminCreateContributionMessage, mutation: adminCreateContributionMessage,
@ -100,14 +101,22 @@ describe('ContributionMessageResolver', () => {
expect.objectContaining({ expect.objectContaining({
errors: [ errors: [
new GraphQLError( 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 () => { it('throws error when contribution.userId equals user.id', async () => {
jest.clearAllMocks()
await mutate({ await mutate({
mutation: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
@ -132,12 +141,19 @@ describe('ContributionMessageResolver', () => {
expect.objectContaining({ expect.objectContaining({
errors: [ errors: [
new GraphQLError( 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', () => { describe('valid input', () => {
@ -210,6 +226,7 @@ describe('ContributionMessageResolver', () => {
describe('input not valid', () => { describe('input not valid', () => {
it('throws error when contribution does not exist', async () => { it('throws error when contribution does not exist', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: createContributionMessage, mutation: createContributionMessage,
@ -222,14 +239,22 @@ describe('ContributionMessageResolver', () => {
expect.objectContaining({ expect.objectContaining({
errors: [ errors: [
new GraphQLError( 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 () => { it('throws error when other user tries to send createContributionMessage', async () => {
jest.clearAllMocks()
await mutate({ await mutate({
mutation: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
@ -246,12 +271,19 @@ describe('ContributionMessageResolver', () => {
expect.objectContaining({ expect.objectContaining({
errors: [ errors: [
new GraphQLError( 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', () => { describe('valid input', () => {

View File

@ -12,10 +12,10 @@ import { ContributionStatus } from '@enum/ContributionStatus'
import { Order } from '@enum/Order' import { Order } from '@enum/Order'
import Paginated from '@arg/Paginated' import Paginated from '@arg/Paginated'
import { backendLogger as logger } from '@/server/logger'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { Context, getUser } from '@/server/context' import { Context, getUser } from '@/server/context'
import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants' import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants'
import LogError from '@/server/LogError'
@Resolver() @Resolver()
export class ContributionMessageResolver { export class ContributionMessageResolver {
@ -54,8 +54,7 @@ export class ContributionMessageResolver {
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error(`ContributionMessage was not successful: ${e}`) throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e)
throw new Error(`ContributionMessage was not successful: ${e}`)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
@ -95,9 +94,7 @@ export class ContributionMessageResolver {
@Ctx() context: Context, @Ctx() context: Context,
): Promise<ContributionMessage> { ): Promise<ContributionMessage> {
const user = getUser(context) const user = getUser(context)
if (!user.emailContact) {
user.emailContact = await UserContact.findOneOrFail({ where: { id: user.emailId } })
}
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ') await queryRunner.startTransaction('REPEATABLE READ')
@ -108,12 +105,10 @@ export class ContributionMessageResolver {
relations: ['user'], relations: ['user'],
}) })
if (!contribution) { if (!contribution) {
logger.error('Contribution not found') throw new LogError('Contribution not found', contributionId)
throw new Error('Contribution not found')
} }
if (contribution.userId === user.id) { if (contribution.userId === user.id) {
logger.error('Admin can not answer on own contribution') throw new LogError('Admin can not answer on his own contribution', contributionId)
throw new Error('Admin can not answer on own contribution')
} }
if (!contribution.user.emailContact) { if (!contribution.user.emailContact) {
contribution.user.emailContact = await UserContact.findOneOrFail({ contribution.user.emailContact = await UserContact.findOneOrFail({
@ -149,8 +144,7 @@ export class ContributionMessageResolver {
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error(`ContributionMessage was not successful: ${e}`) throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e)
throw new Error(`ContributionMessage was not successful: ${e}`)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }

View File

@ -22,11 +22,7 @@ import {
listContributions, listContributions,
listUnconfirmedContributions, listUnconfirmedContributions,
} from '@/seeds/graphql/queries' } from '@/seeds/graphql/queries'
import { import { sendContributionConfirmedEmail } from '@/emails/sendEmailVariants'
// sendAccountActivationEmail,
sendContributionConfirmedEmail,
// sendContributionRejectedEmail,
} from '@/emails/sendEmailVariants'
import { import {
cleanDB, cleanDB,
resetToken, resetToken,
@ -46,8 +42,8 @@ import { User } from '@entity/User'
import { EventProtocolType } from '@/event/EventProtocolType' import { EventProtocolType } from '@/event/EventProtocolType'
import { logger, i18n as localization } from '@test/testSetup' import { logger, i18n as localization } from '@test/testSetup'
import { UserInputError } from 'apollo-server-express' 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 // mock account activation email to avoid console spam
jest.mock('@/emails/sendEmailVariants', () => { jest.mock('@/emails/sendEmailVariants', () => {
const originalModule = jest.requireActual('@/emails/sendEmailVariants') const originalModule = jest.requireActual('@/emails/sendEmailVariants')
@ -132,13 +128,13 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ 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', () => { 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 () => { it('throws error when memo length greater than 255 chars', async () => {
@ -155,13 +151,13 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ 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', () => { 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 () => { it('throws error when creationDate not-valid', async () => {
@ -422,31 +418,6 @@ describe('ContributionResolver', () => {
resetToken() 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', () => { describe('Memo length smaller than 5 chars', () => {
it('throws error', async () => { it('throws error', async () => {
jest.clearAllMocks() jest.clearAllMocks()
@ -463,13 +434,13 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ 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', () => { 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( ).resolves.toEqual(
expect.objectContaining({ 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', () => { 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( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [ errors: [new GraphQLError('Can not update contribution of another user')],
new GraphQLError(
'user of the pending contribution and send user does not correspond',
),
],
}), }),
) )
}) })
it('logs the error found', () => { it('logs the error found', () => {
expect(logger.error).toBeCalledWith( 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( ).resolves.toEqual(
expect.objectContaining({ 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', () => { describe('update too much so that the limit is exceeded', () => {
@ -615,16 +661,13 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ 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', () => { it('logs the error found', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith('Month of contribution can not be changed')
'No information for available creations with the given creationDate=',
'Invalid Date',
)
}) })
}) })
@ -1158,6 +1201,7 @@ describe('ContributionResolver', () => {
describe('wrong contribution id', () => { describe('wrong contribution id', () => {
it('returns an error', async () => { it('returns an error', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: deleteContribution, mutation: deleteContribution,
@ -1167,18 +1211,19 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('Contribution not found for given id.')], errors: [new GraphQLError('Contribution not found')],
}), }),
) )
}) })
it('logs the error 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', () => { describe('other user sends a deleteContribution', () => {
it('returns an error', async () => { it('returns an error', async () => {
jest.clearAllMocks()
await mutate({ await mutate({
mutation: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
@ -1198,7 +1243,11 @@ describe('ContributionResolver', () => {
}) })
it('logs the error found', () => { 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', () => { 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 }), mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ 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', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith('Could not find user', 'bibi@bloxberg.de')
'Could not find user with email: bibi@bloxberg.de',
)
}) })
}) })
@ -1568,7 +1618,7 @@ describe('ContributionResolver', () => {
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [ 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', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( 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( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [ 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', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( 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 }), mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError(`invalid Date for creationDate=invalid-date`)], errors: [new GraphQLError('CreationDate is invalid')],
}), }),
) )
}) })
it('logs the error thrown', () => { 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( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [ errors: [new GraphQLError('Could not find User')],
new GraphQLError('Could not find UserContact with email: bob@baumeister.de'),
],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith('Could not find User', 'bob@baumeister.de')
'Could not find UserContact with email: bob@baumeister.de',
)
}) })
}) })
@ -1856,13 +1910,13 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('User was deleted (stephen@hawking.uk)')], errors: [new GraphQLError('User was deleted')],
}), }),
) )
}) })
it('logs the error thrown', () => { 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( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('No contribution found to given id.')], errors: [new GraphQLError('Contribution not found')],
}), }),
) )
}) })
it('logs the error thrown', () => { 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({ expect.objectContaining({
errors: [ errors: [
new GraphQLError( 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', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( 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( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('Contribution not found for given id.')], errors: [new GraphQLError('Contribution not found')],
}), }),
) )
}) })
it('logs the error thrown', () => { 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( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('Contribution not found to given id.')], errors: [new GraphQLError('Contribution not found')],
}), }),
) )
}) })
it('logs the error thrown', () => { 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', () => { describe('confirm same contribution again', () => {
it('throws an error', async () => { it('throws an error', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: confirmContribution, mutation: confirmContribution,
@ -2368,11 +2423,18 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ 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', () => { describe('confirm two creations one after the other quickly', () => {

View File

@ -44,15 +44,20 @@ import {
EventContributionConfirm, EventContributionConfirm,
EventAdminContributionCreate, EventAdminContributionCreate,
EventAdminContributionDelete, EventAdminContributionDelete,
EventAdminContributionDeny,
EventAdminContributionUpdate, EventAdminContributionUpdate,
} from '@/event/Event' } from '@/event/Event'
import { writeEvent } from '@/event/EventProtocolEmitter' import { writeEvent } from '@/event/EventProtocolEmitter'
import { calculateDecay } from '@/util/decay' import { calculateDecay } from '@/util/decay'
import { import {
sendContributionConfirmedEmail, sendContributionConfirmedEmail,
sendContributionDeletedEmail,
sendContributionDeniedEmail, sendContributionDeniedEmail,
} from '@/emails/sendEmailVariants' } from '@/emails/sendEmailVariants'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction'
@Resolver() @Resolver()
export class ContributionResolver { export class ContributionResolver {
@ -63,14 +68,11 @@ export class ContributionResolver {
@Ctx() context: Context, @Ctx() context: Context,
): Promise<UnconfirmedContribution> { ): Promise<UnconfirmedContribution> {
const clientTimezoneOffset = getClientTimezoneOffset(context) 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) { if (memo.length < MEMO_MIN_CHARS) {
logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`) throw new LogError('Memo text is too short', memo.length)
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`) }
if (memo.length > MEMO_MAX_CHARS) {
throw new LogError('Memo text is too long', memo.length)
} }
const event = new Event() const event = new Event()
@ -112,16 +114,13 @@ export class ContributionResolver {
const user = getUser(context) const user = getUser(context)
const contribution = await DbContribution.findOne(id) const contribution = await DbContribution.findOne(id)
if (!contribution) { if (!contribution) {
logger.error('Contribution not found for given id') throw new LogError('Contribution not found', id)
throw new Error('Contribution not found for given id.')
} }
if (contribution.userId !== user.id) { if (contribution.userId !== user.id) {
logger.error('Can not delete contribution of another user') throw new LogError('Can not delete contribution of another user', contribution, user.id)
throw new Error('Can not delete contribution of another user')
} }
if (contribution.confirmedAt) { if (contribution.confirmedAt) {
logger.error('A confirmed contribution can not be deleted') throw new LogError('A confirmed contribution can not be deleted', contribution)
throw new Error('A confirmed contribution can not be deleted')
} }
contribution.contributionStatus = ContributionStatus.DELETED contribution.contributionStatus = ContributionStatus.DELETED
@ -215,14 +214,11 @@ export class ContributionResolver {
@Ctx() context: Context, @Ctx() context: Context,
): Promise<UnconfirmedContribution> { ): Promise<UnconfirmedContribution> {
const clientTimezoneOffset = getClientTimezoneOffset(context) 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) { if (memo.length < MEMO_MIN_CHARS) {
logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`) throw new LogError('Memo text is too short', memo.length)
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`) }
if (memo.length > MEMO_MAX_CHARS) {
throw new LogError('Memo text is too long', memo.length)
} }
const user = getUser(context) const user = getUser(context)
@ -231,22 +227,22 @@ export class ContributionResolver {
where: { id: contributionId, confirmedAt: IsNull(), deniedAt: IsNull() }, where: { id: contributionId, confirmedAt: IsNull(), deniedAt: IsNull() },
}) })
if (!contributionToUpdate) { if (!contributionToUpdate) {
logger.error('No contribution found to given id') throw new LogError('Contribution not found', contributionId)
throw new Error('No contribution found to given id.')
} }
if (contributionToUpdate.userId !== user.id) { if (contributionToUpdate.userId !== user.id) {
logger.error('user of the pending contribution and send user does not correspond') throw new LogError(
throw new Error('user of the pending contribution and send user does not correspond') 'Can not update contribution of another user',
contributionToUpdate,
user.id,
)
} }
if ( if (
contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS && contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS &&
contributionToUpdate.contributionStatus !== ContributionStatus.PENDING contributionToUpdate.contributionStatus !== ContributionStatus.PENDING
) { ) {
logger.error( throw new LogError(
`Contribution can not be updated since the state is ${contributionToUpdate.contributionStatus}`, 'Contribution can not be updated due to status',
) contributionToUpdate.contributionStatus,
throw new Error(
`Contribution can not be updated since the state is ${contributionToUpdate.contributionStatus}`,
) )
} }
const creationDateObj = new Date(creationDate) const creationDateObj = new Date(creationDate)
@ -254,8 +250,7 @@ export class ContributionResolver {
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) { if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset) creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset)
} else { } else {
logger.error('Currently the month of the contribution cannot change.') throw new LogError('Month of contribution can not be changed')
throw new Error('Currently the month of the contribution cannot change.')
} }
// all possible cases not to be true are thrown in this function // all possible cases not to be true are thrown in this function
@ -306,29 +301,24 @@ export class ContributionResolver {
) )
const clientTimezoneOffset = getClientTimezoneOffset(context) const clientTimezoneOffset = getClientTimezoneOffset(context)
if (!isValidDateString(creationDate)) { if (!isValidDateString(creationDate)) {
logger.error(`invalid Date for creationDate=${creationDate}`) throw new LogError('CreationDate is invalid', creationDate)
throw new Error(`invalid Date for creationDate=${creationDate}`)
} }
const emailContact = await UserContact.findOne({ const emailContact = await UserContact.findOne({
where: { email }, where: { email },
withDeleted: true, withDeleted: true,
relations: ['user'], relations: ['user'],
}) })
if (!emailContact) { if (!emailContact || !emailContact.user) {
logger.error(`Could not find user with email: ${email}`) throw new LogError('Could not find user', email)
throw new Error(`Could not find user with email: ${email}`)
} }
if (emailContact.deletedAt) { if (emailContact.deletedAt || emailContact.user.deletedAt) {
logger.error('This emailContact was deleted. Cannot create a contribution.') throw new LogError('Cannot create contribution since the user was deleted', emailContact)
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.emailChecked) { if (!emailContact.emailChecked) {
logger.error('Contribution could not be saved, Email is not activated') throw new LogError(
throw new Error('Contribution could not be saved, Email is not activated') 'Cannot create contribution since the users email is not activated',
emailContact,
)
} }
const event = new Event() const event = new Event()
@ -401,18 +391,11 @@ export class ContributionResolver {
withDeleted: true, withDeleted: true,
relations: ['user'], relations: ['user'],
}) })
if (!emailContact) { if (!emailContact || !emailContact.user) {
logger.error(`Could not find UserContact with email: ${email}`) throw new LogError('Could not find User', email)
throw new Error(`Could not find UserContact with email: ${email}`)
} }
const user = emailContact.user if (emailContact.deletedAt || emailContact.user.deletedAt) {
if (!user) { throw new LogError('User was deleted', email)
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})`)
} }
const moderator = getUser(context) const moderator = getUser(context)
@ -421,28 +404,25 @@ export class ContributionResolver {
where: { id, confirmedAt: IsNull(), deniedAt: IsNull() }, where: { id, confirmedAt: IsNull(), deniedAt: IsNull() },
}) })
if (!contributionToUpdate) { if (!contributionToUpdate) {
logger.error('No contribution found to given id.') throw new LogError('Contribution not found', id)
throw new Error('No contribution found to given id.')
} }
if (contributionToUpdate.userId !== user.id) { if (contributionToUpdate.userId !== emailContact.user.id) {
logger.error('user of the pending contribution and send user does not correspond') throw new LogError('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.moderatorId === null) { if (contributionToUpdate.moderatorId === null) {
logger.error('An admin is not allowed to update a user contribution.') throw new LogError('An admin is not allowed to update an user contribution')
throw new Error('An admin is not allowed to update a user contribution.')
} }
const creationDateObj = new Date(creationDate) 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()) { if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset) creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset)
} else { } else {
logger.error('Currently the month of the contribution cannot change.') throw new LogError('Month of contribution can not be changed')
throw new Error('Currently the month of the contribution cannot change.')
} }
// all possible cases not to be true are thrown in this function // all possible cases not to be true are thrown in this function
@ -460,11 +440,11 @@ export class ContributionResolver {
result.memo = contributionToUpdate.memo result.memo = contributionToUpdate.memo
result.date = contributionToUpdate.contributionDate result.date = contributionToUpdate.contributionDate
result.creation = await getUserCreation(user.id, clientTimezoneOffset) result.creation = await getUserCreation(emailContact.user.id, clientTimezoneOffset)
const event = new Event() const event = new Event()
const eventAdminContributionUpdate = new EventAdminContributionUpdate() const eventAdminContributionUpdate = new EventAdminContributionUpdate()
eventAdminContributionUpdate.userId = user.id eventAdminContributionUpdate.userId = emailContact.user.id
eventAdminContributionUpdate.amount = amount eventAdminContributionUpdate.amount = amount
eventAdminContributionUpdate.contributionId = contributionToUpdate.id eventAdminContributionUpdate.contributionId = contributionToUpdate.id
await writeEvent(event.setEventAdminContributionUpdate(eventAdminContributionUpdate)) await writeEvent(event.setEventAdminContributionUpdate(eventAdminContributionUpdate))
@ -517,19 +497,17 @@ export class ContributionResolver {
): Promise<boolean> { ): Promise<boolean> {
const contribution = await DbContribution.findOne(id) const contribution = await DbContribution.findOne(id)
if (!contribution) { if (!contribution) {
logger.error(`Contribution not found for given id: ${id}`) throw new LogError('Contribution not found', id)
throw new Error('Contribution not found for given id.')
} }
if (contribution.confirmedAt) { if (contribution.confirmedAt) {
logger.error('A confirmed contribution can not be deleted') throw new LogError('A confirmed contribution can not be deleted')
throw new Error('A confirmed contribution can not be deleted')
} }
const moderator = getUser(context) const moderator = getUser(context)
if ( if (
contribution.contributionType === ContributionType.USER && contribution.contributionType === ContributionType.USER &&
contribution.userId === moderator.id 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( const user = await DbUser.findOneOrFail(
{ id: contribution.userId }, { id: contribution.userId },
@ -546,7 +524,7 @@ export class ContributionResolver {
eventAdminContributionDelete.amount = contribution.amount eventAdminContributionDelete.amount = contribution.amount
eventAdminContributionDelete.contributionId = contribution.id eventAdminContributionDelete.contributionId = contribution.id
await writeEvent(event.setEventAdminContributionDelete(eventAdminContributionDelete)) await writeEvent(event.setEventAdminContributionDelete(eventAdminContributionDelete))
sendContributionDeniedEmail({ sendContributionDeletedEmail({
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
email: user.emailContact.email, email: user.emailContact.email,
@ -571,29 +549,24 @@ export class ContributionResolver {
const clientTimezoneOffset = getClientTimezoneOffset(context) const clientTimezoneOffset = getClientTimezoneOffset(context)
const contribution = await DbContribution.findOne(id) const contribution = await DbContribution.findOne(id)
if (!contribution) { if (!contribution) {
logger.error(`Contribution not found for given id: ${id}`) throw new LogError('Contribution not found', id)
throw new Error('Contribution not found to given id.')
} }
if (contribution.confirmedAt) { if (contribution.confirmedAt) {
logger.error(`Contribution already confirmd: ${id}`) throw new LogError('Contribution already confirmed', id)
throw new Error('Contribution already confirmd.')
} }
if (contribution.contributionStatus === 'DENIED') { if (contribution.contributionStatus === 'DENIED') {
logger.error(`Contribution already denied: ${id}`) throw new LogError('Contribution already denied', id)
throw new Error('Contribution already denied.')
} }
const moderatorUser = getUser(context) const moderatorUser = getUser(context)
if (moderatorUser.id === contribution.userId) { if (moderatorUser.id === contribution.userId) {
logger.error('Moderator can not confirm own contribution') throw new LogError('Moderator can not confirm own contribution')
throw new Error('Moderator can not confirm own contribution')
} }
const user = await DbUser.findOneOrFail( const user = await DbUser.findOneOrFail(
{ id: contribution.userId }, { id: contribution.userId },
{ withDeleted: true, relations: ['emailContact'] }, { withDeleted: true, relations: ['emailContact'] },
) )
if (user.deletedAt) { if (user.deletedAt) {
logger.error('This user was deleted. Cannot confirm a contribution.') throw new LogError('Can not confirm contribution since the user was deleted')
throw new Error('This user was deleted. Cannot confirm a contribution.')
} }
const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false) const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false)
validateContribution( validateContribution(
@ -607,16 +580,11 @@ export class ContributionResolver {
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED') await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
try {
const lastTransaction = await queryRunner.manager const lastTransaction = await getLastTransaction(contribution.userId)
.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') logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
try {
let newBalance = new Decimal(0) let newBalance = new Decimal(0)
let decay: Decay | null = null let decay: Decay | null = null
if (lastTransaction) { if (lastTransaction) {
@ -662,8 +630,7 @@ export class ContributionResolver {
}) })
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error('Creation was not successful', e) throw new LogError('Creation was not successful', e)
throw new Error('Creation was not successful.')
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
@ -738,17 +705,16 @@ export class ContributionResolver {
deniedBy: IsNull(), deniedBy: IsNull(),
}) })
if (!contributionToUpdate) { if (!contributionToUpdate) {
logger.error(`Contribution not found for given id: ${id}`) throw new LogError('Contribution not found', id)
throw new Error(`Contribution not found for given id.`)
} }
if ( if (
contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS && contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS &&
contributionToUpdate.contributionStatus !== ContributionStatus.PENDING contributionToUpdate.contributionStatus !== ContributionStatus.PENDING
) { ) {
logger.error( throw new LogError(
`Contribution state (${contributionToUpdate.contributionStatus}) is not allowed.`, 'Status of the contribution is not allowed',
contributionToUpdate.contributionStatus,
) )
throw new Error(`State of the contribution is not allowed.`)
} }
const moderator = getUser(context) const moderator = getUser(context)
const user = await DbUser.findOne( const user = await DbUser.findOne(
@ -756,10 +722,7 @@ export class ContributionResolver {
{ relations: ['emailContact'] }, { relations: ['emailContact'] },
) )
if (!user) { if (!user) {
logger.error( throw new LogError('Could not find User of the Contribution', contributionToUpdate.userId)
`Could not find User for the Contribution (userId: ${contributionToUpdate.userId}).`,
)
throw new Error('Could not find User for the Contribution.')
} }
contributionToUpdate.contributionStatus = ContributionStatus.DENIED contributionToUpdate.contributionStatus = ContributionStatus.DENIED
@ -767,6 +730,13 @@ export class ContributionResolver {
contributionToUpdate.deniedAt = new Date() contributionToUpdate.deniedAt = new Date()
const res = await contributionToUpdate.save() 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({ sendContributionDeniedEmail({
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,

View File

@ -33,6 +33,8 @@ import { executeTransaction } from './TransactionResolver'
import QueryLinkResult from '@union/QueryLinkResult' import QueryLinkResult from '@union/QueryLinkResult'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { getLastTransaction } from './util/getLastTransaction'
// TODO: do not export, test it inside the resolver // TODO: do not export, test it inside the resolver
export const transactionLinkCode = (date: Date): string => { export const transactionLinkCode = (date: Date): string => {
const time = date.getTime().toString(16) const time = date.getTime().toString(16)
@ -275,13 +277,7 @@ export class TransactionLinkResolver {
await queryRunner.manager.insert(DbContribution, contribution) await queryRunner.manager.insert(DbContribution, contribution)
const lastTransaction = await queryRunner.manager const lastTransaction = await getLastTransaction(user.id)
.createQueryBuilder()
.select('transaction')
.from(DbTransaction, 'transaction')
.where('transaction.userId = :id', { id: user.id })
.orderBy('transaction.id', 'DESC')
.getOne()
let newBalance = new Decimal(0) let newBalance = new Decimal(0)
let decay: Decay | null = null let decay: Decay | null = null

View File

@ -38,6 +38,8 @@ import { findUserByEmail } from './UserResolver'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { getLastTransaction } from './util/getLastTransaction'
export const executeTransaction = async ( export const executeTransaction = async (
amount: Decimal, amount: Decimal,
memo: string, memo: string,
@ -206,10 +208,7 @@ export class TransactionResolver {
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`) logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`)
// find current balance // find current balance
const lastTransaction = await dbTransaction.findOne( const lastTransaction = await getLastTransaction(user.id, ['contribution'])
{ userId: user.id },
{ order: { id: 'DESC' }, relations: ['contribution'] },
)
logger.debug(`lastTransaction=${lastTransaction}`) logger.debug(`lastTransaction=${lastTransaction}`)
const balanceResolver = new BalanceResolver() const balanceResolver = new BalanceResolver()

View File

@ -0,0 +1,14 @@
import { Transaction as DbTransaction } from '@entity/Transaction'
export const getLastTransaction = async (
userId: number,
relations?: string[],
): Promise<DbTransaction | undefined> => {
return DbTransaction.findOne(
{ userId },
{
order: { balanceDate: 'DESC', id: 'DESC' },
relations,
},
)
}

View File

@ -23,8 +23,13 @@
"commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.", "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" "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt"
}, },
"contributionRejected": { "contributionDeleted": {
"commonGoodContributionRejected": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} abgelehnt.", "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", "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“!" "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“!"
}, },

View File

@ -23,6 +23,11 @@
"commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.", "commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.",
"subject": "Gradido: Your contribution to the common good was confirmed" "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": { "contributionDenied": {
"commonGoodContributionDenied": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.", "commonGoodContributionDenied": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.",
"subject": "Gradido: Your common good contribution was rejected", "subject": "Gradido: Your common good contribution was rejected",

View File

@ -1,10 +1,10 @@
import { calculateDecay } from './decay' import { calculateDecay } from './decay'
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { Transaction } from '@entity/Transaction'
import { Decay } from '@model/Decay' import { Decay } from '@model/Decay'
import { getCustomRepository } from '@dbTools/typeorm' import { getCustomRepository } from '@dbTools/typeorm'
import { TransactionLinkRepository } from '@repository/TransactionLink' import { TransactionLinkRepository } from '@repository/TransactionLink'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { getLastTransaction } from '../graphql/resolver/util/getLastTransaction'
function isStringBoolean(value: string): boolean { function isStringBoolean(value: string): boolean {
const lowerValue = value.toLowerCase() const lowerValue = value.toLowerCase()
@ -20,7 +20,7 @@ async function calculateBalance(
time: Date, time: Date,
transactionLink?: dbTransactionLink | null, transactionLink?: dbTransactionLink | null,
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | 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 if (!lastTransaction) return null
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=v1.2022-03-18
DB_HOST=localhost DB_HOST=localhost
DB_PORT=3306 DB_PORT=3306
DB_USER=root DB_USER=root

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-database", "name": "gradido-database",
"version": "1.17.1", "version": "1.18.2",
"description": "Gradido Database Tool to execute database migrations", "description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database", "repository": "https://github.com/gradido/gradido/database",

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=v2.2023-02-03
# Database # Database
DB_HOST=localhost DB_HOST=localhost
DB_PORT=3306 DB_PORT=3306

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=v4.2022-12-20
# Environment # Environment
DEFAULT_PUBLISHER_ID=2896 DEFAULT_PUBLISHER_ID=2896

View File

@ -1,6 +1,6 @@
{ {
"name": "bootstrap-vue-gradido-wallet", "name": "bootstrap-vue-gradido-wallet",
"version": "1.17.1", "version": "1.18.2",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node run/server.js", "start": "node run/server.js",
@ -104,5 +104,10 @@
], ],
"author": "Gradido-Akademie - https://www.gradido.net/", "author": "Gradido-Akademie - https://www.gradido.net/",
"license": "Apache-2.0", "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"
]
}
} }

View File

@ -24,11 +24,6 @@ describe('ContributionForm', () => {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$d: jest.fn((d) => d), $d: jest.fn((d) => d),
$n: jest.fn((n) => n), $n: jest.fn((n) => n),
$store: {
state: {
creation: ['1000', '1000', '1000'],
},
},
$i18n: { $i18n: {
locale: 'en', locale: 'en',
}, },
@ -61,7 +56,7 @@ describe('ContributionForm', () => {
}) })
}) })
describe('dates', () => { describe('dates and max amounts', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper.setData({ await wrapper.setData({
form: { form: {
@ -73,204 +68,176 @@ describe('ContributionForm', () => {
}) })
}) })
describe('actual date', () => { describe('max amount reached for both months', () => {
describe('same month', () => { beforeEach(() => {
wrapper.setProps({
maxGddLastMonth: 0,
maxGddThisMonth: 0,
})
wrapper.setData({
form: {
id: null,
date: 'set',
memo: '',
amount: '',
},
})
})
it('shows message that no contributions are available', () => {
expect(wrapper.find('[data-test="contribtion-message"]').text()).toBe(
'contribution.noOpenCreation.allMonth',
)
})
})
describe('max amount reached for last month, no date selected', () => {
beforeEach(() => {
wrapper.setProps({
maxGddLastMonth: 0,
})
})
it('shows no message', () => {
expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false)
})
})
describe('max amount reached for last month, last month selected', () => {
beforeEach(async () => { beforeEach(async () => {
const now = new Date().toISOString() wrapper.setProps({
await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now) maxGddLastMonth: 0,
isThisMonth: false,
}) })
describe('isThisMonth', () => {
it('has true', () => {
expect(wrapper.vm.isThisMonth).toBe(true)
})
})
})
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)
})
})
})
})
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({ await wrapper.setData({
maximalDate: new Date(2020, 6, 6), form: {
form: { date: new Date(2020, 6, 6) }, id: null,
date: 'set',
memo: '',
amount: '',
},
}) })
}) })
describe('minimalDate', () => { it('shows message that no contributions are available for last month', () => {
it('has "2020-06-01T00:00:00.000Z"', () => { expect(wrapper.find('[data-test="contribtion-message"]').text()).toBe(
expect(wrapper.vm.minimalDate.toISOString()).toBe('2020-06-01T00:00:00.000Z') 'contribution.noOpenCreation.lastMonth',
)
}) })
}) })
describe('isThisMonth', () => { describe('max amount reached for last month, this month selected', () => {
it('has true', () => {
expect(wrapper.vm.isThisMonth).toBe(true)
})
})
})
describe('month before', () => {
beforeEach(async () => { beforeEach(async () => {
// jest.useFakeTimers('modern') wrapper.setProps({
// jest.setSystemTime(new Date('2020-07-06')) maxGddLastMonth: 0,
// console.log('middle of year date now:', wrapper.vm.minimalDate) isThisMonth: true,
// await wrapper })
// .findComponent({ name: 'BFormDatepicker' })
// .vm.$emit('input', wrapper.vm.minimalDate)
await wrapper.setData({ await wrapper.setData({
maximalDate: new Date(2020, 6, 6), form: {
form: { date: new Date(2020, 5, 6) }, id: null,
date: 'set',
memo: '',
amount: '',
},
}) })
}) })
describe('minimalDate', () => { it('shows no message', () => {
it('has "2020-06-01T00:00:00.000Z"', () => { expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false)
expect(wrapper.vm.minimalDate.toISOString()).toBe('2020-06-01T00:00:00.000Z')
}) })
}) })
describe('isThisMonth', () => { describe('max amount reached for this month, no date selected', () => {
it('has false', () => { beforeEach(() => {
expect(wrapper.vm.isThisMonth).toBe(false) wrapper.setProps({
}) maxGddThisMonth: 0,
})
}) })
}) })
describe.skip('date in january', () => { it('shows no message', () => {
describe('same month', () => { expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false)
})
})
describe('max amount reached for this month, this month selected', () => {
beforeEach(async () => { beforeEach(async () => {
wrapper.setProps({
maxGddThisMonth: 0,
isThisMonth: true,
})
await wrapper.setData({ await wrapper.setData({
maximalDate: new Date(2020, 0, 6), form: {
form: { date: new Date(2020, 0, 6) }, id: null,
date: 'set',
memo: '',
amount: '',
},
}) })
}) })
describe('minimalDate', () => { it('shows message that no contributions are available for last month', () => {
it('has "2019-12-01T00:00:00.000Z"', () => { expect(wrapper.find('[data-test="contribtion-message"]').text()).toBe(
expect(wrapper.vm.minimalDate.toISOString()).toBe('2019-12-01T00:00:00.000Z') 'contribution.noOpenCreation.thisMonth',
)
}) })
}) })
describe('isThisMonth', () => { describe('max amount reached for this month, last month selected', () => {
it('has true', () => {
expect(wrapper.vm.isThisMonth).toBe(true)
})
})
})
describe('month before', () => {
beforeEach(async () => { beforeEach(async () => {
// jest.useFakeTimers('modern') wrapper.setProps({
// jest.setSystemTime(new Date('2020-07-06')) maxGddThisMonth: 0,
// console.log('middle of year date now:', wrapper.vm.minimalDate) isThisMonth: false,
// await wrapper })
// .findComponent({ name: 'BFormDatepicker' })
// .vm.$emit('input', wrapper.vm.minimalDate)
await wrapper.setData({ await wrapper.setData({
maximalDate: new Date(2020, 0, 6), form: {
form: { date: new Date(2019, 11, 6) }, id: null,
date: 'set',
memo: '',
amount: '',
},
}) })
}) })
describe('minimalDate', () => { it('shows no message', () => {
it('has "2019-12-01T00:00:00.000Z"', () => { expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false)
expect(wrapper.vm.minimalDate.toISOString()).toBe('2019-12-01T00:00:00.000Z')
})
})
describe('isThisMonth', () => {
it('has false', () => {
expect(wrapper.vm.isThisMonth).toBe(false)
})
}) })
}) })
}) })
describe.skip('date with the 31st day of the month', () => { describe('default return message', () => {
describe('same month', () => { it('returns an empty string', () => {
beforeEach(async () => { expect(wrapper.vm.noOpenCreation).toBe('')
await wrapper.setData({
maximalDate: new Date('2022-10-31T00:00:00.000Z'),
form: { date: new Date('2022-10-31T00:00:00.000Z') },
}) })
}) })
describe('minimalDate', () => { describe('update amount', () => {
it('has "2022-09-01T00:00:00.000Z"', () => { beforeEach(() => {
expect(wrapper.vm.minimalDate.toISOString()).toBe('2022-09-01T00:00:00.000Z') wrapper.findComponent({ name: 'InputHour' }).vm.$emit('updateAmount', 20)
})
it('updates form amount', () => {
expect(wrapper.vm.form.amount).toBe('400.00')
}) })
}) })
describe('isThisMonth', () => { describe('watch value', () => {
it('has true', () => { beforeEach(() => {
expect(wrapper.vm.isThisMonth).toBe(true) wrapper.setProps({
}) value: {
}) id: 42,
date: 'set',
memo: 'Some Memo',
amount: '400.00',
},
}) })
}) })
describe.skip('date with the 28th day of the month', () => { it('updates form', () => {
describe('same month', () => { expect(wrapper.vm.form).toEqual({
beforeEach(async () => { id: 42,
await wrapper.setData({ date: 'set',
maximalDate: new Date('2023-02-28T00:00:00.000Z'), memo: 'Some Memo',
form: { date: new Date('2023-02-28T00:00:00.000Z') }, amount: '400.00',
})
})
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)
})
})
})
})
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('minimalDate', () => {
it('has "2024-01-01T00:00:00.000Z"', () => {
expect(wrapper.vm.minimalDate.toISOString()).toBe('2024-01-01T00:00:00.000Z')
})
})
describe('isThisMonth', () => {
it('has true', () => {
expect(wrapper.vm.isThisMonth).toBe(true)
})
})
}) })
}) })
}) })
@ -477,24 +444,23 @@ describe('ContributionForm', () => {
}) })
}) })
describe.skip('on trigger submit', () => { describe('on trigger submit', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper.find('form').trigger('submit') await wrapper.find('form').trigger('submit')
}) })
it('emits "update-contribution"', () => { it('emits "update-contribution"', () => {
expect(wrapper.emitted('update-contribution')).toEqual( expect(wrapper.emitted('update-contribution')).toEqual([
expect.arrayContaining([ [
expect.arrayContaining([
{ {
id: 2, id: 2,
date: now, date: now,
hours: 0,
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
amount: '200', amount: '200',
}, },
]), ],
]), ])
)
}) })
}) })
}) })

View File

@ -23,10 +23,7 @@
<template #nav-next-year><span></span></template> <template #nav-next-year><span></span></template>
</b-form-datepicker> </b-form-datepicker>
<div <div v-if="showMessage" class="p-3" data-test="contribtion-message">
v-if="(isThisMonth && maxGddThisMonth <= 0) || (!isThisMonth && maxGddLastMonth <= 0)"
class="p-3"
>
{{ noOpenCreation }} {{ noOpenCreation }}
</div> </div>
<div v-else> <div v-else>
@ -118,8 +115,8 @@ export default {
} }
}, },
methods: { methods: {
updateAmount(amount) { updateAmount(hours) {
this.form.amount = (amount * 20).toFixed(2).toString() this.form.amount = (hours * 20).toFixed(2).toString()
}, },
submit() { submit() {
this.$emit(this.form.id ? 'update-contribution' : 'set-contribution', { ...this.form }) this.$emit(this.form.id ? 'update-contribution' : 'set-contribution', { ...this.form })
@ -135,6 +132,15 @@ export default {
}, },
}, },
computed: { 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() { disabled() {
return ( return (
this.form.date === '' || this.form.date === '' ||

View File

@ -116,5 +116,15 @@ describe('ContributionList', () => {
expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 2 }]]) 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 }]])
})
})
}) })
}) })

View File

@ -67,6 +67,7 @@ export default {
} }
if (this.tokenExpiresInSeconds === 0) { if (this.tokenExpiresInSeconds === 0) {
this.$timer.stop('tokenExpires') this.$timer.stop('tokenExpires')
this.toastInfoNoHide(this.$t('session.automaticallyLoggedOut'))
this.$emit('logout') this.$emit('logout')
} }
}, },
@ -84,6 +85,7 @@ export default {
}) })
.catch(() => { .catch(() => {
this.$timer.stop('tokenExpires') this.$timer.stop('tokenExpires')
this.toastInfoNoHide(this.$t('session.automaticallyLoggedOut'))
this.$emit('logout') this.$emit('logout')
}) })
}, },

View File

@ -173,7 +173,7 @@
"GDD": "GDD", "GDD": "GDD",
"gddKonto": "GDD Konto", "gddKonto": "GDD Konto",
"gdd_per_link": { "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": "Link kopieren",
"copy-link-with-text": "Link und Text kopieren", "copy-link-with-text": "Link und Text kopieren",
"created": "Der Link wurde erstellt!", "created": "Der Link wurde erstellt!",
@ -272,6 +272,7 @@
"send_gdd": "GDD versenden", "send_gdd": "GDD versenden",
"send_per_link": "GDD versenden per Link", "send_per_link": "GDD versenden per Link",
"session": { "session": {
"automaticallyLoggedOut": "Du wurdest automatisch abgemeldet",
"extend": "Angemeldet bleiben", "extend": "Angemeldet bleiben",
"lightText": "Wenn du länger als 10 Minuten keine Aktion getätigt hast, wirst du aus Sicherheitsgründen abgemeldet.", "lightText": "Wenn du länger als 10 Minuten keine Aktion getätigt hast, wirst du aus Sicherheitsgründen abgemeldet.",
"logoutIn": "Abmelden in ", "logoutIn": "Abmelden in ",

View File

@ -173,7 +173,7 @@
"GDD": "GDD", "GDD": "GDD",
"gddKonto": "GDD Konto", "gddKonto": "GDD Konto",
"gdd_per_link": { "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": "Copy link",
"copy-link-with-text": "Copy link and text", "copy-link-with-text": "Copy link and text",
"created": "Link was created!", "created": "Link was created!",
@ -272,6 +272,7 @@
"send_gdd": "Send GDD", "send_gdd": "Send GDD",
"send_per_link": "Send GDD via Link", "send_per_link": "Send GDD via Link",
"session": { "session": {
"automaticallyLoggedOut": "You have been automatically logged out.",
"extend": "Stay logged in", "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.", "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 ", "logoutIn": "Log out in ",

View File

@ -18,11 +18,18 @@ export const toasters = {
variant: 'warning', variant: 'warning',
}) })
}, },
toastInfoNoHide(message) {
this.toast(message, {
title: this.$t('navigation.info'),
variant: 'warning',
noAutoHide: true,
})
},
toast(message, options) { toast(message, options) {
if (message.replace) message = message.replace(/^GraphQL error: /, '') if (message.replace) message = message.replace(/^GraphQL error: /, '')
this.$root.$bvToast.toast(message, { this.$root.$bvToast.toast(message, {
autoHideDelay: 5000,
appendToast: true, appendToast: true,
autoHideDelay: 5000,
solid: true, solid: true,
toaster: 'b-toaster-top-right', toaster: 'b-toaster-top-right',
headerClass: 'gdd-toaster-title', headerClass: 'gdd-toaster-title',

View File

@ -70,6 +70,8 @@ describe('Community', () => {
lastName: 'Bloxberg', lastName: 'Bloxberg',
state: 'IN_PROGRESS', state: 'IN_PROGRESS',
messagesCount: 0, messagesCount: 0,
deniedAt: null,
deniedBy: null,
}, },
{ {
id: 1550, id: 1550,
@ -84,6 +86,8 @@ describe('Community', () => {
lastName: 'Bloxberg', lastName: 'Bloxberg',
state: 'CONFIRMED', state: 'CONFIRMED',
messagesCount: 0, messagesCount: 0,
deniedAt: null,
deniedBy: null,
}, },
], ],
contributionCount: 1, contributionCount: 1,
@ -112,6 +116,10 @@ describe('Community', () => {
confirmedAt: null, confirmedAt: null,
firstName: 'Bibi', firstName: 'Bibi',
lastName: 'Bloxberg', lastName: 'Bloxberg',
deniedAt: null,
deniedBy: null,
messagesCount: 0,
state: 'IN_PROGRESS',
}, },
{ {
id: 1550, id: 1550,
@ -124,7 +132,10 @@ describe('Community', () => {
firstName: 'Bibi', firstName: 'Bibi',
contributionDate: '2022-06-15T08:47:06.000Z', contributionDate: '2022-06-15T08:47:06.000Z',
lastName: 'Bloxberg', lastName: 'Bloxberg',
deniedAt: null,
deniedBy: null,
messagesCount: 0, messagesCount: 0,
state: 'IN_PROGRESS',
}, },
{ {
id: 1556, id: 1556,
@ -137,6 +148,10 @@ describe('Community', () => {
confirmedAt: null, confirmedAt: null,
firstName: 'Bob', firstName: 'Bob',
lastName: 'der Baumeister', lastName: 'der Baumeister',
deniedAt: null,
deniedBy: null,
messagesCount: 0,
state: 'IN_PROGRESS',
}, },
], ],
contributionCount: 3, contributionCount: 3,

View File

@ -6,9 +6,9 @@
{{ CONFIG.COMMUNITY_DESCRIPTION }} {{ CONFIG.COMMUNITY_DESCRIPTION }}
</div> </div>
<div> <div>
<router-link :to="CONFIG.COMMUNITY_URL"> <b-link :href="CONFIG.COMMUNITY_URL">
{{ CONFIG.COMMUNITY_URL }} {{ CONFIG.COMMUNITY_URL }}
</router-link> </b-link>
</div> </div>
<hr /> <hr />
<div class="h3">{{ $t('community.openContributionLinks') }}</div> <div class="h3">{{ $t('community.openContributionLinks') }}</div>

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido", "name": "gradido",
"version": "1.17.1", "version": "1.18.2",
"description": "Gradido", "description": "Gradido",
"main": "index.js", "main": "index.js",
"repository": "git@github.com:gradido/gradido.git", "repository": "git@github.com:gradido/gradido.git",