mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-12 23:35:50 +00:00
Merge remote-tracking branch 'origin/master' into
2501-feature-federation-implement-a-graphql-client-to-request-getpublickey
This commit is contained in:
commit
a10de9b55a
49
CHANGELOG.md
49
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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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') },
|
||||
]
|
||||
},
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
CONFIG_VERSION=v16.2023-02-02
|
||||
|
||||
# Server
|
||||
PORT=4000
|
||||
JWT_SECRET=secret123
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 <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', () => {
|
||||
beforeAll(async () => {
|
||||
result = await sendResetPasswordEmail({
|
||||
|
||||
@ -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: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
|
||||
16
backend/src/emails/templates/contributionDeleted/html.pug
Normal file
16
backend/src/emails/templates/contributionDeleted/html.pug
Normal 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
|
||||
@ -0,0 +1 @@
|
||||
= t('emails.contributionDeleted.subject')
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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}`)
|
||||
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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<ContributionMessage> {
|
||||
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()
|
||||
}
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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<UnconfirmedContribution> {
|
||||
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<UnconfirmedContribution> {
|
||||
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<boolean> {
|
||||
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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
14
backend/src/graphql/resolver/util/getLastTransaction.ts
Normal file
14
backend/src/graphql/resolver/util/getLastTransaction.ts
Normal 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,
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -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“!"
|
||||
},
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
CONFIG_VERSION=v1.2022-03-18
|
||||
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
CONFIG_VERSION=v2.2023-02-03
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
CONFIG_VERSION=v4.2022-12-20
|
||||
|
||||
# Environment
|
||||
DEFAULT_PUBLISHER_ID=2896
|
||||
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -23,10 +23,7 @@
|
||||
<template #nav-next-year><span></span></template>
|
||||
</b-form-datepicker>
|
||||
|
||||
<div
|
||||
v-if="(isThisMonth && maxGddThisMonth <= 0) || (!isThisMonth && maxGddLastMonth <= 0)"
|
||||
class="p-3"
|
||||
>
|
||||
<div v-if="showMessage" class="p-3" data-test="contribtion-message">
|
||||
{{ noOpenCreation }}
|
||||
</div>
|
||||
<div v-else>
|
||||
@ -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 === '' ||
|
||||
|
||||
@ -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 }]])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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')
|
||||
})
|
||||
},
|
||||
|
||||
@ -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 ",
|
||||
|
||||
@ -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 ",
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -6,9 +6,9 @@
|
||||
{{ CONFIG.COMMUNITY_DESCRIPTION }}
|
||||
</div>
|
||||
<div>
|
||||
<router-link :to="CONFIG.COMMUNITY_URL">
|
||||
<b-link :href="CONFIG.COMMUNITY_URL">
|
||||
{{ CONFIG.COMMUNITY_URL }}
|
||||
</router-link>
|
||||
</b-link>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="h3">{{ $t('community.openContributionLinks') }}</div>
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user