mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into refactor-listUnconfirmedContribution-to-adminListAllContribution
This commit is contained in:
commit
4c03deccd3
@ -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
|
||||
|
||||
@ -86,5 +86,10 @@
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 10"
|
||||
]
|
||||
],
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"**/*.spec.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
CONFIG_VERSION=v14.2022-12-22
|
||||
|
||||
# Server
|
||||
PORT=4000
|
||||
JWT_SECRET=secret123
|
||||
|
||||
@ -72,5 +72,8 @@
|
||||
"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}`)
|
||||
|
||||
|
||||
@ -246,6 +246,7 @@ describe('Contribution Links', () => {
|
||||
})
|
||||
|
||||
it('returns an error if missing startDate', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -270,6 +271,7 @@ describe('Contribution Links', () => {
|
||||
})
|
||||
|
||||
it('returns an error if missing endDate', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -292,6 +294,7 @@ describe('Contribution Links', () => {
|
||||
})
|
||||
|
||||
it('returns an error if endDate is before startDate', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -316,27 +319,8 @@ describe('Contribution Links', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if name is an empty string', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
name: '',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The name must be initialized!')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('The name must be initialized!')
|
||||
})
|
||||
|
||||
it('returns an error if name is shorter than 5 characters', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -347,22 +331,17 @@ describe('Contribution Links', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
`The value of 'name' with a length of 3 did not fulfill the requested bounderies min=5 and max=100`,
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('The value of name is too short')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The value of 'name' with a length of 3 did not fulfill the requested bounderies min=5 and max=100`,
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('The value of name is too short', 3)
|
||||
})
|
||||
|
||||
it('returns an error if name is longer than 100 characters', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -373,42 +352,17 @@ describe('Contribution Links', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
`The value of 'name' with a length of 101 did not fulfill the requested bounderies min=5 and max=100`,
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('The value of name is too long')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The value of 'name' with a length of 101 did not fulfill the requested bounderies min=5 and max=100`,
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if memo is an empty string', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
memo: '',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The memo must be initialized!')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('The memo must be initialized!')
|
||||
expect(logger.error).toBeCalledWith('The value of name is too long', 101)
|
||||
})
|
||||
|
||||
it('returns an error if memo is shorter than 5 characters', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -419,22 +373,17 @@ describe('Contribution Links', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
`The value of 'memo' with a length of 3 did not fulfill the requested bounderies min=5 and max=255`,
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('The value of memo is too short')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The value of 'memo' with a length of 3 did not fulfill the requested bounderies min=5 and max=255`,
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('The value of memo is too short', 3)
|
||||
})
|
||||
|
||||
it('returns an error if memo is longer than 255 characters', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -445,22 +394,17 @@ describe('Contribution Links', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
`The value of 'memo' with a length of 256 did not fulfill the requested bounderies min=5 and max=255`,
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('The value of memo is too long')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The value of 'memo' with a length of 256 did not fulfill the requested bounderies min=5 and max=255`,
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('The value of memo is too long', 256)
|
||||
})
|
||||
|
||||
it('returns an error if amount is not positive', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -471,15 +415,13 @@ describe('Contribution Links', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The amount=0 must be initialized with a positiv value!')],
|
||||
errors: [new GraphQLError('The amount must be a positiv value')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'The amount=0 must be initialized with a positiv value!',
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('The amount must be a positiv value', new Decimal(0))
|
||||
})
|
||||
})
|
||||
|
||||
@ -530,14 +472,14 @@ describe('Contribution Links', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Contribution Link not found to given id.')],
|
||||
errors: [new GraphQLError('Contribution Link not found')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution Link not found to given id: -1')
|
||||
expect(logger.error).toBeCalledWith('Contribution Link not found', -1)
|
||||
})
|
||||
|
||||
describe('valid id', () => {
|
||||
@ -601,13 +543,13 @@ describe('Contribution Links', () => {
|
||||
mutate({ mutation: deleteContributionLink, variables: { id: -1 } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Contribution Link not found to given id.')],
|
||||
errors: [new GraphQLError('Contribution Link not found')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution Link not found to given id: -1')
|
||||
expect(logger.error).toBeCalledWith('Contribution Link not found', -1)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ import Paginated from '@arg/Paginated'
|
||||
|
||||
// TODO: this is a strange construct
|
||||
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
||||
import LogError from '@/server/LogError'
|
||||
|
||||
@Resolver()
|
||||
export class ContributionLinkResolver {
|
||||
@ -39,35 +40,22 @@ export class ContributionLinkResolver {
|
||||
}: ContributionLinkArgs,
|
||||
): Promise<ContributionLink> {
|
||||
isStartEndDateValid(validFrom, validTo)
|
||||
if (!name) {
|
||||
logger.error(`The name must be initialized!`)
|
||||
throw new Error(`The name must be initialized!`)
|
||||
if (name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS) {
|
||||
throw new LogError('The value of name is too short', name.length)
|
||||
}
|
||||
if (
|
||||
name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS ||
|
||||
name.length > CONTRIBUTIONLINK_NAME_MAX_CHARS
|
||||
) {
|
||||
const msg = `The value of 'name' with a length of ${name.length} did not fulfill the requested bounderies min=${CONTRIBUTIONLINK_NAME_MIN_CHARS} and max=${CONTRIBUTIONLINK_NAME_MAX_CHARS}`
|
||||
logger.error(`${msg}`)
|
||||
throw new Error(`${msg}`)
|
||||
if (name.length > CONTRIBUTIONLINK_NAME_MAX_CHARS) {
|
||||
throw new LogError('The value of name is too long', name.length)
|
||||
}
|
||||
if (!memo) {
|
||||
logger.error(`The memo must be initialized!`)
|
||||
throw new Error(`The memo must be initialized!`)
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
throw new LogError('The value of memo is too short', memo.length)
|
||||
}
|
||||
if (memo.length < MEMO_MIN_CHARS || memo.length > MEMO_MAX_CHARS) {
|
||||
const msg = `The value of 'memo' with a length of ${memo.length} did not fulfill the requested bounderies min=${MEMO_MIN_CHARS} and max=${MEMO_MAX_CHARS}`
|
||||
logger.error(`${msg}`)
|
||||
throw new Error(`${msg}`)
|
||||
}
|
||||
if (!amount) {
|
||||
logger.error(`The amount must be initialized!`)
|
||||
throw new Error('The amount must be initialized!')
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
throw new LogError('The value of memo is too long', memo.length)
|
||||
}
|
||||
if (!new Decimal(amount).isPositive()) {
|
||||
logger.error(`The amount=${amount} must be initialized with a positiv value!`)
|
||||
throw new Error(`The amount=${amount} must be initialized with a positiv value!`)
|
||||
throw new LogError('The amount must be a positiv value', amount)
|
||||
}
|
||||
|
||||
const dbContributionLink = new DbContributionLink()
|
||||
dbContributionLink.amount = amount
|
||||
dbContributionLink.name = name
|
||||
@ -107,8 +95,7 @@ export class ContributionLinkResolver {
|
||||
async deleteContributionLink(@Arg('id', () => Int) id: number): Promise<Date | null> {
|
||||
const contributionLink = await DbContributionLink.findOne(id)
|
||||
if (!contributionLink) {
|
||||
logger.error(`Contribution Link not found to given id: ${id}`)
|
||||
throw new Error('Contribution Link not found to given id.')
|
||||
throw new LogError('Contribution Link not found', id)
|
||||
}
|
||||
await contributionLink.softRemove()
|
||||
logger.debug(`deleteContributionLink successful!`)
|
||||
@ -134,8 +121,7 @@ export class ContributionLinkResolver {
|
||||
): Promise<ContributionLink> {
|
||||
const dbContributionLink = await DbContributionLink.findOne(id)
|
||||
if (!dbContributionLink) {
|
||||
logger.error(`Contribution Link not found to given id: ${id}`)
|
||||
throw new Error('Contribution Link not found to given id.')
|
||||
throw new LogError('Contribution Link not found', id)
|
||||
}
|
||||
dbContributionLink.amount = amount
|
||||
dbContributionLink.name = name
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -25,11 +25,7 @@ import {
|
||||
listContributions,
|
||||
adminListAllContributions,
|
||||
} from '@/seeds/graphql/queries'
|
||||
import {
|
||||
// sendAccountActivationEmail,
|
||||
sendContributionConfirmedEmail,
|
||||
// sendContributionRejectedEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
import { sendContributionConfirmedEmail } from '@/emails/sendEmailVariants'
|
||||
import {
|
||||
cleanDB,
|
||||
resetToken,
|
||||
@ -51,7 +47,6 @@ import { logger, i18n as localization } from '@test/testSetup'
|
||||
import { UserInputError } from 'apollo-server-express'
|
||||
import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz'
|
||||
|
||||
// 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')
|
||||
|
||||
@ -43,16 +43,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 { getLastTransaction } from './util/getLastTransaction'
|
||||
|
||||
@Resolver()
|
||||
export class ContributionResolver {
|
||||
@Authorized([RIGHTS.CREATE_CONTRIBUTION])
|
||||
@ -542,7 +546,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,
|
||||
@ -603,16 +607,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) {
|
||||
@ -763,6 +762,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,5 +1,3 @@
|
||||
CONFIG_VERSION=v1.2023-01-01
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
CONFIG_VERSION=v4.2022-12-20
|
||||
|
||||
# Environment
|
||||
DEFAULT_PUBLISHER_ID=2896
|
||||
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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!",
|
||||
|
||||
@ -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!",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user