mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into docu-env-vars
This commit is contained in:
commit
332de5082b
@ -25,6 +25,7 @@ export enum RIGHTS {
|
|||||||
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
|
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
|
||||||
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
|
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
|
||||||
GDT_BALANCE = 'GDT_BALANCE',
|
GDT_BALANCE = 'GDT_BALANCE',
|
||||||
|
CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION',
|
||||||
// Admin
|
// Admin
|
||||||
SEARCH_USERS = 'SEARCH_USERS',
|
SEARCH_USERS = 'SEARCH_USERS',
|
||||||
SET_USER_ROLE = 'SET_USER_ROLE',
|
SET_USER_ROLE = 'SET_USER_ROLE',
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export const ROLE_USER = new Role('user', [
|
|||||||
RIGHTS.REDEEM_TRANSACTION_LINK,
|
RIGHTS.REDEEM_TRANSACTION_LINK,
|
||||||
RIGHTS.LIST_TRANSACTION_LINKS,
|
RIGHTS.LIST_TRANSACTION_LINKS,
|
||||||
RIGHTS.GDT_BALANCE,
|
RIGHTS.GDT_BALANCE,
|
||||||
|
RIGHTS.CREATE_CONTRIBUTION,
|
||||||
])
|
])
|
||||||
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
||||||
|
|
||||||
|
|||||||
15
backend/src/graphql/arg/ContributionArgs.ts
Normal file
15
backend/src/graphql/arg/ContributionArgs.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { ArgsType, Field, InputType } from 'type-graphql'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
@ArgsType()
|
||||||
|
export default class ContributionArgs {
|
||||||
|
@Field(() => Decimal)
|
||||||
|
amount: Decimal
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
memo: string
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
creationDate: string
|
||||||
|
}
|
||||||
@ -1,8 +1,22 @@
|
|||||||
import { ObjectType, Field, Int } from 'type-graphql'
|
import { ObjectType, Field, Int } from 'type-graphql'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { Contribution } from '@entity/Contribution'
|
||||||
|
import { User } from '@entity/User'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class UnconfirmedContribution {
|
export class UnconfirmedContribution {
|
||||||
|
constructor(contribution: Contribution, user: User, creations: Decimal[]) {
|
||||||
|
this.id = contribution.id
|
||||||
|
this.userId = contribution.userId
|
||||||
|
this.amount = contribution.amount
|
||||||
|
this.memo = contribution.memo
|
||||||
|
this.date = contribution.contributionDate
|
||||||
|
this.firstName = user ? user.firstName : ''
|
||||||
|
this.lastName = user ? user.lastName : ''
|
||||||
|
this.email = user ? user.email : ''
|
||||||
|
this.creation = creations
|
||||||
|
}
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
firstName: string
|
firstName: string
|
||||||
|
|
||||||
@ -27,8 +41,8 @@ export class UnconfirmedContribution {
|
|||||||
@Field(() => Decimal)
|
@Field(() => Decimal)
|
||||||
amount: Decimal
|
amount: Decimal
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Number, { nullable: true })
|
||||||
moderator: number
|
moderator: number | null
|
||||||
|
|
||||||
@Field(() => [Decimal])
|
@Field(() => [Decimal])
|
||||||
creation: Decimal[]
|
creation: Decimal[]
|
||||||
|
|||||||
@ -46,15 +46,23 @@ import { checkOptInCode, activationLink, printTimeDuration } from './UserResolve
|
|||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
|
import {
|
||||||
|
getCreationIndex,
|
||||||
|
getUserCreation,
|
||||||
|
getUserCreations,
|
||||||
|
validateContribution,
|
||||||
|
isStartEndDateValid,
|
||||||
|
} from './util/creations'
|
||||||
|
import {
|
||||||
|
CONTRIBUTIONLINK_MEMO_MAX_CHARS,
|
||||||
|
CONTRIBUTIONLINK_MEMO_MIN_CHARS,
|
||||||
|
CONTRIBUTIONLINK_NAME_MAX_CHARS,
|
||||||
|
CONTRIBUTIONLINK_NAME_MIN_CHARS,
|
||||||
|
FULL_CREATION_AVAILABLE,
|
||||||
|
} from './const/const'
|
||||||
|
|
||||||
// const EMAIL_OPT_IN_REGISTER = 1
|
// const EMAIL_OPT_IN_REGISTER = 1
|
||||||
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
||||||
const MAX_CREATION_AMOUNT = new Decimal(1000)
|
|
||||||
const FULL_CREATION_AVAILABLE = [MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT]
|
|
||||||
const CONTRIBUTIONLINK_NAME_MAX_CHARS = 100
|
|
||||||
const CONTRIBUTIONLINK_NAME_MIN_CHARS = 5
|
|
||||||
const CONTRIBUTIONLINK_MEMO_MAX_CHARS = 255
|
|
||||||
const CONTRIBUTIONLINK_MEMO_MIN_CHARS = 5
|
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class AdminResolver {
|
export class AdminResolver {
|
||||||
@ -244,7 +252,7 @@ export class AdminResolver {
|
|||||||
const creations = await getUserCreation(user.id)
|
const creations = await getUserCreation(user.id)
|
||||||
logger.trace('creations', creations)
|
logger.trace('creations', creations)
|
||||||
const creationDateObj = new Date(creationDate)
|
const creationDateObj = new Date(creationDate)
|
||||||
if (isContributionValid(creations, amount, creationDateObj)) {
|
validateContribution(creations, amount, creationDateObj)
|
||||||
const contribution = Contribution.create()
|
const contribution = Contribution.create()
|
||||||
contribution.userId = user.id
|
contribution.userId = user.id
|
||||||
contribution.amount = amount
|
contribution.amount = amount
|
||||||
@ -255,7 +263,6 @@ export class AdminResolver {
|
|||||||
|
|
||||||
logger.trace('contribution to save', contribution)
|
logger.trace('contribution to save', contribution)
|
||||||
await Contribution.save(contribution)
|
await Contribution.save(contribution)
|
||||||
}
|
|
||||||
return getUserCreation(user.id)
|
return getUserCreation(user.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,7 +328,7 @@ export class AdminResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// all possible cases not to be true are thrown in this function
|
// all possible cases not to be true are thrown in this function
|
||||||
isContributionValid(creations, amount, creationDateObj)
|
validateContribution(creations, amount, creationDateObj)
|
||||||
contributionToUpdate.amount = amount
|
contributionToUpdate.amount = amount
|
||||||
contributionToUpdate.memo = memo
|
contributionToUpdate.memo = memo
|
||||||
contributionToUpdate.contributionDate = new Date(creationDate)
|
contributionToUpdate.contributionDate = new Date(creationDate)
|
||||||
@ -398,9 +405,7 @@ export class AdminResolver {
|
|||||||
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a contribution.')
|
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a contribution.')
|
||||||
|
|
||||||
const creations = await getUserCreation(contribution.userId, false)
|
const creations = await getUserCreation(contribution.userId, false)
|
||||||
if (!isContributionValid(creations, contribution.amount, contribution.contributionDate)) {
|
validateContribution(creations, contribution.amount, contribution.contributionDate)
|
||||||
throw new Error('Creation is not valid!!')
|
|
||||||
}
|
|
||||||
|
|
||||||
const receivedCallDate = new Date()
|
const receivedCallDate = new Date()
|
||||||
|
|
||||||
@ -684,64 +689,6 @@ export class AdminResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreationMap {
|
|
||||||
id: number
|
|
||||||
creations: Decimal[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
|
||||||
logger.trace('getUserCreation', id, includePending)
|
|
||||||
const creations = await getUserCreations([id], includePending)
|
|
||||||
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getUserCreations(ids: number[], includePending = true): Promise<CreationMap[]> {
|
|
||||||
logger.trace('getUserCreations:', ids, includePending)
|
|
||||||
const months = getCreationMonths()
|
|
||||||
logger.trace('getUserCreations months', months)
|
|
||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
|
||||||
await queryRunner.connect()
|
|
||||||
|
|
||||||
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
|
||||||
logger.trace('getUserCreations dateFilter', dateFilter)
|
|
||||||
|
|
||||||
const unionString = includePending
|
|
||||||
? `
|
|
||||||
UNION
|
|
||||||
SELECT contribution_date AS date, amount AS amount, user_id AS userId FROM contributions
|
|
||||||
WHERE user_id IN (${ids.toString()})
|
|
||||||
AND contribution_date >= ${dateFilter}
|
|
||||||
AND confirmed_at IS NULL AND deleted_at IS NULL`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
const unionQuery = await queryRunner.manager.query(`
|
|
||||||
SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM
|
|
||||||
(SELECT creation_date AS date, amount AS amount, user_id AS userId FROM transactions
|
|
||||||
WHERE user_id IN (${ids.toString()})
|
|
||||||
AND type_id = ${TransactionTypeId.CREATION}
|
|
||||||
AND creation_date >= ${dateFilter}
|
|
||||||
${unionString}) AS result
|
|
||||||
GROUP BY month, userId
|
|
||||||
ORDER BY date DESC
|
|
||||||
`)
|
|
||||||
|
|
||||||
await queryRunner.release()
|
|
||||||
|
|
||||||
return ids.map((id) => {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
creations: months.map((month) => {
|
|
||||||
const creation = unionQuery.find(
|
|
||||||
(raw: { month: string; id: string; creation: number[] }) =>
|
|
||||||
parseInt(raw.month) === month && parseInt(raw.id) === id,
|
|
||||||
)
|
|
||||||
return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0)
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCreations(creations: Decimal[], contribution: Contribution): Decimal[] {
|
function updateCreations(creations: Decimal[], contribution: Contribution): Decimal[] {
|
||||||
const index = getCreationIndex(contribution.contributionDate.getMonth())
|
const index = getCreationIndex(contribution.contributionDate.getMonth())
|
||||||
|
|
||||||
@ -751,58 +698,3 @@ function updateCreations(creations: Decimal[], contribution: Contribution): Deci
|
|||||||
creations[index] = creations[index].plus(contribution.amount.toString())
|
creations[index] = creations[index].plus(contribution.amount.toString())
|
||||||
return creations
|
return creations
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isContributionValid = (
|
|
||||||
creations: Decimal[],
|
|
||||||
amount: Decimal,
|
|
||||||
creationDate: Date,
|
|
||||||
): boolean => {
|
|
||||||
logger.trace('isContributionValid', creations, amount, creationDate)
|
|
||||||
const index = getCreationIndex(creationDate.getMonth())
|
|
||||||
|
|
||||||
if (index < 0) {
|
|
||||||
throw new Error('No information for available creations for the given date')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (amount.greaterThan(creations[index].toString())) {
|
|
||||||
throw new Error(
|
|
||||||
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const isStartEndDateValid = (
|
|
||||||
startDate: string | null | undefined,
|
|
||||||
endDate: string | null | undefined,
|
|
||||||
): void => {
|
|
||||||
if (!startDate) {
|
|
||||||
logger.error('Start-Date is not initialized. A Start-Date must be set!')
|
|
||||||
throw new Error('Start-Date is not initialized. A Start-Date must be set!')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!endDate) {
|
|
||||||
logger.error('End-Date is not initialized. An End-Date must be set!')
|
|
||||||
throw new Error('End-Date is not initialized. An End-Date must be set!')
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if endDate is before startDate
|
|
||||||
if (new Date(endDate).getTime() - new Date(startDate).getTime() < 0) {
|
|
||||||
logger.error(`The value of validFrom must before or equals the validTo!`)
|
|
||||||
throw new Error(`The value of validFrom must before or equals the validTo!`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCreationMonths = (): number[] => {
|
|
||||||
const now = new Date(Date.now())
|
|
||||||
return [
|
|
||||||
now.getMonth() + 1,
|
|
||||||
new Date(now.getFullYear(), now.getMonth() - 1, 1).getMonth() + 1,
|
|
||||||
new Date(now.getFullYear(), now.getMonth() - 2, 1).getMonth() + 1,
|
|
||||||
].reverse()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCreationIndex = (month: number): number => {
|
|
||||||
return getCreationMonths().findIndex((el) => el === month + 1)
|
|
||||||
}
|
|
||||||
|
|||||||
124
backend/src/graphql/resolver/ContributionResolver.test.ts
Normal file
124
backend/src/graphql/resolver/ContributionResolver.test.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* 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 { cleanDB, resetToken, testEnvironment } from '@test/helpers'
|
||||||
|
import { GraphQLError } from 'graphql'
|
||||||
|
import { userFactory } from '@/seeds/factory/user'
|
||||||
|
|
||||||
|
let mutate: any, query: any, con: any
|
||||||
|
let testEnv: any
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
testEnv = await testEnvironment()
|
||||||
|
mutate = testEnv.mutate
|
||||||
|
query = testEnv.query
|
||||||
|
con = testEnv.con
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
await con.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ContributionResolver', () => {
|
||||||
|
describe('createContribution', () => {
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('returns an error', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: createContribution,
|
||||||
|
variables: { amount: 100.0, memo: 'Test Contribution', creationDate: 'not-valid' },
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('401 Unauthorized')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated with valid user', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
|
await query({
|
||||||
|
query: login,
|
||||||
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
resetToken()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('input not valid', () => {
|
||||||
|
it('throws error when creationDate not-valid', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: createContribution,
|
||||||
|
variables: {
|
||||||
|
amount: 100.0,
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
creationDate: 'not-valid',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
new GraphQLError('No information for available creations for the given date'),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws error when creationDate 3 month behind', async () => {
|
||||||
|
const date = new Date()
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: createContribution,
|
||||||
|
variables: {
|
||||||
|
amount: 100.0,
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
creationDate: date.setMonth(date.getMonth() - 3).toString(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
new GraphQLError('No information for available creations for the given date'),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('valid input', () => {
|
||||||
|
it('creates contribution', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: createContribution,
|
||||||
|
variables: {
|
||||||
|
amount: 100.0,
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
creationDate: new Date().toString(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
createContribution: {
|
||||||
|
amount: '100',
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
35
backend/src/graphql/resolver/ContributionResolver.ts
Normal file
35
backend/src/graphql/resolver/ContributionResolver.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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 ContributionArgs from '../arg/ContributionArgs'
|
||||||
|
import { UnconfirmedContribution } from '../model/UnconfirmedContribution'
|
||||||
|
import { validateContribution, getUserCreation } from './util/creations'
|
||||||
|
|
||||||
|
@Resolver()
|
||||||
|
export class ContributionResolver {
|
||||||
|
@Authorized([RIGHTS.CREATE_CONTRIBUTION])
|
||||||
|
@Mutation(() => UnconfirmedContribution)
|
||||||
|
async createContribution(
|
||||||
|
@Args() { amount, memo, creationDate }: ContributionArgs,
|
||||||
|
@Ctx() context: Context,
|
||||||
|
): Promise<UnconfirmedContribution> {
|
||||||
|
const user = getUser(context)
|
||||||
|
const creations = await getUserCreation(user.id)
|
||||||
|
logger.trace('creations', creations)
|
||||||
|
const creationDateObj = new Date(creationDate)
|
||||||
|
validateContribution(creations, amount, creationDateObj)
|
||||||
|
|
||||||
|
const contribution = Contribution.create()
|
||||||
|
contribution.userId = user.id
|
||||||
|
contribution.amount = amount
|
||||||
|
contribution.createdAt = new Date()
|
||||||
|
contribution.contributionDate = creationDateObj
|
||||||
|
contribution.memo = memo
|
||||||
|
|
||||||
|
logger.trace('contribution to save', contribution)
|
||||||
|
await Contribution.save(contribution)
|
||||||
|
return new UnconfirmedContribution(contribution, user, creations)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,7 +28,7 @@ import { executeTransaction } from './TransactionResolver'
|
|||||||
import { Order } from '@enum/Order'
|
import { Order } from '@enum/Order'
|
||||||
import { Contribution as DbContribution } from '@entity/Contribution'
|
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||||
import { getUserCreation, isContributionValid } from './AdminResolver'
|
import { getUserCreation, validateContribution } from './util/creations'
|
||||||
import { Decay } from '@model/Decay'
|
import { Decay } from '@model/Decay'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||||
@ -223,13 +223,7 @@ export class TransactionLinkResolver {
|
|||||||
|
|
||||||
const creations = await getUserCreation(user.id, false)
|
const creations = await getUserCreation(user.id, false)
|
||||||
logger.info('open creations', creations)
|
logger.info('open creations', creations)
|
||||||
if (!isContributionValid(creations, contributionLink.amount, now)) {
|
validateContribution(creations, contributionLink.amount, now)
|
||||||
logger.error(
|
|
||||||
'Amount of Contribution link exceeds available amount for this month',
|
|
||||||
contributionLink.amount,
|
|
||||||
)
|
|
||||||
throw new Error('Amount of Contribution link exceeds available amount')
|
|
||||||
}
|
|
||||||
const contribution = new DbContribution()
|
const contribution = new DbContribution()
|
||||||
contribution.userId = user.id
|
contribution.userId = user.id
|
||||||
contribution.createdAt = now
|
contribution.createdAt = now
|
||||||
|
|||||||
12
backend/src/graphql/resolver/const/const.ts
Normal file
12
backend/src/graphql/resolver/const/const.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
|
export const MAX_CREATION_AMOUNT = new Decimal(1000)
|
||||||
|
export const FULL_CREATION_AVAILABLE = [
|
||||||
|
MAX_CREATION_AMOUNT,
|
||||||
|
MAX_CREATION_AMOUNT,
|
||||||
|
MAX_CREATION_AMOUNT,
|
||||||
|
]
|
||||||
|
export const CONTRIBUTIONLINK_NAME_MAX_CHARS = 100
|
||||||
|
export const CONTRIBUTIONLINK_NAME_MIN_CHARS = 5
|
||||||
|
export const CONTRIBUTIONLINK_MEMO_MAX_CHARS = 255
|
||||||
|
export const CONTRIBUTIONLINK_MEMO_MIN_CHARS = 5
|
||||||
119
backend/src/graphql/resolver/util/creations.ts
Normal file
119
backend/src/graphql/resolver/util/creations.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
|
||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
import { getConnection } from '@dbTools/typeorm'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { FULL_CREATION_AVAILABLE, MAX_CREATION_AMOUNT } from '../const/const'
|
||||||
|
|
||||||
|
interface CreationMap {
|
||||||
|
id: number
|
||||||
|
creations: Decimal[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateContribution = (
|
||||||
|
creations: Decimal[],
|
||||||
|
amount: Decimal,
|
||||||
|
creationDate: Date,
|
||||||
|
): void => {
|
||||||
|
logger.trace('isContributionValid', creations, amount, creationDate)
|
||||||
|
const index = getCreationIndex(creationDate.getMonth())
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
throw new Error('No information for available creations for the given date')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount.greaterThan(creations[index].toString())) {
|
||||||
|
throw new Error(
|
||||||
|
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserCreations = async (
|
||||||
|
ids: number[],
|
||||||
|
includePending = true,
|
||||||
|
): Promise<CreationMap[]> => {
|
||||||
|
logger.trace('getUserCreations:', ids, includePending)
|
||||||
|
const months = getCreationMonths()
|
||||||
|
logger.trace('getUserCreations months', months)
|
||||||
|
|
||||||
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
|
await queryRunner.connect()
|
||||||
|
|
||||||
|
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
||||||
|
logger.trace('getUserCreations dateFilter', dateFilter)
|
||||||
|
|
||||||
|
const unionString = includePending
|
||||||
|
? `
|
||||||
|
UNION
|
||||||
|
SELECT contribution_date AS date, amount AS amount, user_id AS userId FROM contributions
|
||||||
|
WHERE user_id IN (${ids.toString()})
|
||||||
|
AND contribution_date >= ${dateFilter}
|
||||||
|
AND confirmed_at IS NULL AND deleted_at IS NULL`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const unionQuery = await queryRunner.manager.query(`
|
||||||
|
SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM
|
||||||
|
(SELECT creation_date AS date, amount AS amount, user_id AS userId FROM transactions
|
||||||
|
WHERE user_id IN (${ids.toString()})
|
||||||
|
AND type_id = ${TransactionTypeId.CREATION}
|
||||||
|
AND creation_date >= ${dateFilter}
|
||||||
|
${unionString}) AS result
|
||||||
|
GROUP BY month, userId
|
||||||
|
ORDER BY date DESC
|
||||||
|
`)
|
||||||
|
|
||||||
|
await queryRunner.release()
|
||||||
|
|
||||||
|
return ids.map((id) => {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
creations: months.map((month) => {
|
||||||
|
const creation = unionQuery.find(
|
||||||
|
(raw: { month: string; id: string; creation: number[] }) =>
|
||||||
|
parseInt(raw.month) === month && parseInt(raw.id) === id,
|
||||||
|
)
|
||||||
|
return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
||||||
|
logger.trace('getUserCreation', id, includePending)
|
||||||
|
const creations = await getUserCreations([id], includePending)
|
||||||
|
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCreationMonths = (): number[] => {
|
||||||
|
const now = new Date(Date.now())
|
||||||
|
return [
|
||||||
|
now.getMonth() + 1,
|
||||||
|
new Date(now.getFullYear(), now.getMonth() - 1, 1).getMonth() + 1,
|
||||||
|
new Date(now.getFullYear(), now.getMonth() - 2, 1).getMonth() + 1,
|
||||||
|
].reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCreationIndex = (month: number): number => {
|
||||||
|
return getCreationMonths().findIndex((el) => el === month + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isStartEndDateValid = (
|
||||||
|
startDate: string | null | undefined,
|
||||||
|
endDate: string | null | undefined,
|
||||||
|
): void => {
|
||||||
|
if (!startDate) {
|
||||||
|
logger.error('Start-Date is not initialized. A Start-Date must be set!')
|
||||||
|
throw new Error('Start-Date is not initialized. A Start-Date must be set!')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!endDate) {
|
||||||
|
logger.error('End-Date is not initialized. An End-Date must be set!')
|
||||||
|
throw new Error('End-Date is not initialized. An End-Date must be set!')
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if endDate is before startDate
|
||||||
|
if (new Date(endDate).getTime() - new Date(startDate).getTime() < 0) {
|
||||||
|
logger.error(`The value of validFrom must before or equals the validTo!`)
|
||||||
|
throw new Error(`The value of validFrom must before or equals the validTo!`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -230,3 +230,12 @@ export const deleteContributionLink = gql`
|
|||||||
deleteContributionLink(id: $id)
|
deleteContributionLink(id: $id)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const createContribution = gql`
|
||||||
|
mutation ($amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||||
|
createContribution(amount: $amount, memo: $memo, creationDate: $creationDate) {
|
||||||
|
amount
|
||||||
|
memo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user