Merge branch '2000-contribution-list' into 2000-contribution-update

This commit is contained in:
elweyn 2022-07-05 08:53:49 +02:00
commit 60aae29cff
9 changed files with 291 additions and 17 deletions

View File

@ -26,6 +26,8 @@ export enum RIGHTS {
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
GDT_BALANCE = 'GDT_BALANCE',
CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION',
LIST_CONTRIBUTIONS = 'LIST_CONTRIBUTIONS',
UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION',
// Admin
SEARCH_USERS = 'SEARCH_USERS',
SET_USER_ROLE = 'SET_USER_ROLE',

View File

@ -24,6 +24,8 @@ export const ROLE_USER = new Role('user', [
RIGHTS.LIST_TRANSACTION_LINKS,
RIGHTS.GDT_BALANCE,
RIGHTS.CREATE_CONTRIBUTION,
RIGHTS.LIST_CONTRIBUTIONS,
RIGHTS.UPDATE_CONTRIBUTION,
])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights

View File

@ -0,0 +1,43 @@
import { ObjectType, Field, Int } from 'type-graphql'
import Decimal from 'decimal.js-light'
import { Contribution as dbContribution } from '@entity/Contribution'
import { User } from './User'
@ObjectType()
export class Contribution {
constructor(contribution: dbContribution, user: User) {
this.id = contribution.id
this.user = user
this.amount = contribution.amount
this.memo = contribution.memo
this.createdAt = contribution.createdAt
this.deletedAt = contribution.deletedAt
}
@Field(() => Number)
id: number
@Field(() => User)
user: User
@Field(() => Decimal)
amount: Decimal
@Field(() => String)
memo: string
@Field(() => Date)
createdAt: Date
@Field(() => Date, { nullable: true })
deletedAt: Date | null
}
@ObjectType()
export class ContributionListResult {
@Field(() => Int)
linkCount: number
@Field(() => [Contribution])
linkList: Contribution[]
}

View File

@ -52,6 +52,7 @@ import {
getUserCreations,
isContributionValid,
isStartEndDateValid,
updateCreations,
} from './util/isContributionValid'
import {
CONTRIBUTIONLINK_MEMO_MAX_CHARS,
@ -688,13 +689,3 @@ export class AdminResolver {
return new ContributionLink(dbContributionLink)
}
}
function updateCreations(creations: Decimal[], contribution: Contribution): Decimal[] {
const index = getCreationIndex(contribution.contributionDate.getMonth())
if (index < 0) {
throw new Error('You cannot create GDD for a month older than the last three months.')
}
creations[index] = creations[index].plus(contribution.amount.toString())
return creations
}

View File

@ -2,11 +2,14 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { createContribution } from '@/seeds/graphql/mutations'
import { login } from '@/seeds/graphql/queries'
import { createContribution, updateContribution } from '@/seeds/graphql/mutations'
import { listContributions, login } from '@/seeds/graphql/queries'
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
import { GraphQLError } from 'graphql'
import { userFactory } from '@/seeds/factory/user'
import { creationFactory } from '@/seeds/factory/creation'
import { creations } from '@/seeds/creation/index'
import { peterLustig } from '@/seeds/users/peter-lustig'
let mutate: any, query: any, con: any
let testEnv: any
@ -121,4 +124,114 @@ describe('ContributionResolver', () => {
})
})
})
describe('listContributions', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
query({
query: listContributions,
variables: {
currentPage: 1,
pageSize: 25,
order: 'DESC',
filterConfirmed: false,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
beforeAll(async () => {
await userFactory(testEnv, peterLustig)
await userFactory(testEnv, bibiBloxberg)
// bibi needs GDDs
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await creationFactory(testEnv, bibisCreation!)
// await userFactory(testEnv, bibiBloxberg)
await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
it('returns an empty array for unconfirmed creation filter', async () => {
await expect(
query({
query: listContributions,
variables: {
currentPage: 1,
pageSize: 25,
order: 'DESC',
filterConfirmed: true,
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listContributions: [],
},
}),
)
})
it('returns confirmed creation', async () => {
await expect(
query({
query: listContributions,
variables: {
currentPage: 1,
pageSize: 25,
order: 'DESC',
filterConfirmed: false,
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listContributions: expect.arrayContaining([
expect.objectContaining({
memo: 'Herzlich Willkommen bei Gradido!',
amount: '1000',
}),
]),
},
}),
)
})
})
})
describe('updateContribution', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: updateContribution,
variables: {
contributionId: 1,
amount: 100.0,
memo: 'Test Contribution',
creationDate: 'not-valid',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
})
})

View File

@ -1,11 +1,16 @@
import { RIGHTS } from '@/auth/RIGHTS'
import { Context, getUser } from '@/server/context'
import { backendLogger as logger } from '@/server/logger'
import { Contribution } from '@entity/Contribution'
import { Args, Authorized, Ctx, Mutation, Resolver } from 'type-graphql'
import { Contribution as dbContribution } from '@entity/Contribution'
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
import { IsNull } from '../../../../database/node_modules/typeorm'
import ContributionArgs from '../arg/ContributionArgs'
import Paginated from '../arg/Paginated'
import { Order } from '../enum/Order'
import { Contribution } from '../model/Contribution'
import { UnconfirmedContribution } from '../model/UnconfirmedContribution'
import { isContributionValid, getUserCreation } from './util/isContributionValid'
import { User } from '../model/User'
import { isContributionValid, getUserCreation, updateCreations } from './util/isContributionValid'
@Resolver()
export class ContributionResolver {
@ -21,7 +26,7 @@ export class ContributionResolver {
const creationDateObj = new Date(creationDate)
isContributionValid(creations, amount, creationDateObj)
const contribution = Contribution.create()
const contribution = dbContribution.create()
contribution.userId = user.id
contribution.amount = amount
contribution.createdAt = new Date()
@ -29,7 +34,82 @@ export class ContributionResolver {
contribution.memo = memo
logger.trace('contribution to save', contribution)
await Contribution.save(contribution)
await dbContribution.save(contribution)
return new UnconfirmedContribution(contribution, user, creations)
}
@Authorized([RIGHTS.LIST_CONTRIBUTIONS])
@Query(() => [Contribution])
async listContributions(
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
@Arg('filterConfirmed', () => Boolean)
filterConfirmed: boolean | null,
@Ctx() context: Context,
): Promise<Contribution[]> {
const user = getUser(context)
let contribution
if (filterConfirmed) {
contribution = await dbContribution.find({
where: {
userId: user.id,
confirmedBy: IsNull(),
},
order: {
createdAt: order,
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
} else {
contribution = await dbContribution.find({
where: {
userId: user.id,
},
order: {
createdAt: order,
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
}
return contribution.map((contr) => new Contribution(contr, new User(user)))
}
@Authorized([RIGHTS.UPDATE_CONTRIBUTION])
@Mutation(() => UnconfirmedContribution)
async updateContribution(
@Arg('contributionId', () => Int)
contributionId: number,
@Args() { amount, memo, creationDate }: ContributionArgs,
@Ctx() context: Context,
): Promise<UnconfirmedContribution> {
const user = getUser(context)
const contributionToUpdate = await dbContribution.findOne({
where: { id: contributionId, confirmedAt: IsNull() },
})
if (!contributionToUpdate) {
throw new Error('No contribution found to given id.')
}
if (contributionToUpdate.userId !== user.id) {
throw new Error('user of the pending contribution and send user does not correspond')
}
const creationDateObj = new Date(creationDate)
let creations = await getUserCreation(user.id)
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
creations = updateCreations(creations, contributionToUpdate)
}
// all possible cases not to be true are thrown in this function
isContributionValid(creations, amount, creationDateObj)
contributionToUpdate.amount = amount
contributionToUpdate.memo = memo
contributionToUpdate.contributionDate = new Date(creationDate)
dbContribution.save(contributionToUpdate)
const result = new UnconfirmedContribution(contributionToUpdate, user, creations)
return result
}
}

View File

@ -1,6 +1,7 @@
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
import { backendLogger as logger } from '@/server/logger'
import { getConnection } from '@dbTools/typeorm'
import { Contribution } from '@entity/Contribution'
import Decimal from 'decimal.js-light'
import { FULL_CREATION_AVAILABLE, MAX_CREATION_AMOUNT } from '../const/const'
@ -117,3 +118,13 @@ export const isStartEndDateValid = (
throw new Error(`The value of validFrom must before or equals the validTo!`)
}
}
export const updateCreations = (creations: Decimal[], contribution: Contribution): Decimal[] => {
const index = getCreationIndex(contribution.contributionDate.getMonth())
if (index < 0) {
throw new Error('You cannot create GDD for a month older than the last three months.')
}
creations[index] = creations[index].plus(contribution.amount.toString())
return creations
}

View File

@ -239,3 +239,17 @@ export const createContribution = gql`
}
}
`
export const updateContribution = gql`
mutation ($contributionId: Int!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
updateContribution(
contributionId: $contributionId
amount: $amount
memo: $memo
creationDate: $creationDate
) {
amount
memo
}
}
`

View File

@ -172,6 +172,24 @@ export const queryTransactionLink = gql`
}
`
export const listContributions = gql`
query (
$currentPage: Int = 1
$pageSize: Int = 5
$order: Order
$filterConfirmed: Boolean = false
) {
listContributions(
currentPage: $currentPage
pageSize: $pageSize
order: $order
filterConfirmed: $filterConfirmed
) {
amount
memo
}
}
`
// from admin interface
export const listUnconfirmedContributions = gql`