import { RIGHTS } from '@/auth/RIGHTS' import { Context, getUser } from '@/server/context' import { backendLogger as logger } from '@/server/logger' import { Contribution as dbContribution } from '@entity/Contribution' import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql' import { FindOperator, IsNull, getConnection } from '@dbTools/typeorm' import ContributionArgs from '@arg/ContributionArgs' import Paginated from '@arg/Paginated' import { Order } from '@enum/Order' import { ContributionType } from '@enum/ContributionType' import { ContributionStatus } from '@enum/ContributionStatus' import { Contribution, ContributionListResult } from '@model/Contribution' import { UnconfirmedContribution } from '@model/UnconfirmedContribution' import { validateContribution, getUserCreation, updateCreations } from './util/creations' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { ContributionMessage } from '@entity/ContributionMessage' import { ContributionMessageType } from '../enum/MessageType' import { Event, EventContributionCreate, EventContributionDelete, EventContributionUpdate, } from '@/event/Event' import { eventProtocol } from '@/event/EventProtocolEmitter' @Resolver() export class ContributionResolver { @Authorized([RIGHTS.CREATE_CONTRIBUTION]) @Mutation(() => UnconfirmedContribution) async createContribution( @Args() { amount, memo, creationDate }: ContributionArgs, @Ctx() context: Context, ): Promise { 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)`) } const event = new Event() 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 = dbContribution.create() contribution.userId = user.id contribution.amount = amount contribution.createdAt = new Date() contribution.contributionDate = creationDateObj contribution.memo = memo contribution.contributionType = ContributionType.USER contribution.contributionStatus = ContributionStatus.PENDING logger.trace('contribution to save', contribution) await dbContribution.save(contribution) const eventCreateContribution = new EventContributionCreate() eventCreateContribution.userId = user.id eventCreateContribution.amount = amount eventCreateContribution.contributionId = contribution.id await eventProtocol.writeEvent(event.setEventContributionCreate(eventCreateContribution)) return new UnconfirmedContribution(contribution, user, creations) } @Authorized([RIGHTS.DELETE_CONTRIBUTION]) @Mutation(() => Boolean) async deleteContribution( @Arg('id', () => Int) id: number, @Ctx() context: Context, ): Promise { const event = new Event() 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.') } if (contribution.userId !== user.id) { logger.error('Can not delete contribution of another user') throw new Error('Can not delete contribution of another user') } if (contribution.confirmedAt) { logger.error('A confirmed contribution can not be deleted') throw new Error('A confirmed contribution can not be deleted') } contribution.contributionStatus = ContributionStatus.DELETED contribution.deletedBy = user.id contribution.deletedAt = new Date() await contribution.save() const eventDeleteContribution = new EventContributionDelete() eventDeleteContribution.userId = user.id eventDeleteContribution.contributionId = contribution.id eventDeleteContribution.amount = contribution.amount await eventProtocol.writeEvent(event.setEventContributionDelete(eventDeleteContribution)) const res = await contribution.softRemove() return !!res } @Authorized([RIGHTS.LIST_CONTRIBUTIONS]) @Query(() => ContributionListResult) async listContributions( @Args() { currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated, @Arg('filterConfirmed', () => Boolean) filterConfirmed: boolean | null, @Ctx() context: Context, ): Promise { const user = getUser(context) const where: { userId: number confirmedBy?: FindOperator | null } = { userId: user.id } if (filterConfirmed) where.confirmedBy = IsNull() const [contributions, count] = await getConnection() .createQueryBuilder() .select('c') .from(dbContribution, 'c') .leftJoinAndSelect('c.messages', 'm') .where(where) .withDeleted() .orderBy('c.createdAt', order) .limit(pageSize) .offset((currentPage - 1) * pageSize) .getManyAndCount() return new ContributionListResult( count, contributions.map((contribution) => new Contribution(contribution, user)), ) } @Authorized([RIGHTS.LIST_ALL_CONTRIBUTIONS]) @Query(() => ContributionListResult) async listAllContributions( @Args() { currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated, ): Promise { const [dbContributions, count] = await getConnection() .createQueryBuilder() .select('c') .from(dbContribution, 'c') .innerJoinAndSelect('c.user', 'u') .orderBy('c.createdAt', order) .limit(pageSize) .offset((currentPage - 1) * pageSize) .getManyAndCount() return new ContributionListResult( count, dbContributions.map((contribution) => new Contribution(contribution, contribution.user)), ) } @Authorized([RIGHTS.UPDATE_CONTRIBUTION]) @Mutation(() => UnconfirmedContribution) async updateContribution( @Arg('contributionId', () => Int) contributionId: number, @Args() { amount, memo, creationDate }: ContributionArgs, @Ctx() context: Context, ): Promise { 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)`) } const user = getUser(context) const contributionToUpdate = await dbContribution.findOne({ where: { id: contributionId, confirmedAt: IsNull() }, }) if (!contributionToUpdate) { logger.error('No contribution found to given id') throw new Error('No contribution found to given 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.contributionStatus !== ContributionStatus.IN_PROGRESS && contributionToUpdate.contributionStatus !== ContributionStatus.PENDING ) { throw new Error( `Contribution can not be updated since the state is ${contributionToUpdate.contributionStatus}`, ) } const creationDateObj = new Date(creationDate) let creations = await getUserCreation(user.id) if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) { creations = updateCreations(creations, contributionToUpdate) } else { logger.error('Currently the month of the contribution cannot change.') throw new Error('Currently the month of the contribution cannot change.') } // all possible cases not to be true are thrown in this function validateContribution(creations, amount, creationDateObj) const contributionMessage = ContributionMessage.create() contributionMessage.contributionId = contributionId contributionMessage.createdAt = contributionToUpdate.updatedAt ? contributionToUpdate.updatedAt : contributionToUpdate.createdAt let changeMessage = '' if (contributionToUpdate.memo !== memo) { // TODO: Add message to changeMessage changeMessage += 'Neuer Memo: ' + memo + '\n' } if (contributionToUpdate.amount.toFixed(6) !== amount.toFixed(6)) { // TODO: Add amount to changeMessage changeMessage += 'Neuer Betrag: ' + amount + '\n' } const isDateDifferent = contributionToUpdate.contributionDate.getTime() !== new Date(creationDate).getTime() if (isDateDifferent) { // TODO: Add contributionDate to changeMessage changeMessage += 'Neuer Beitragsdatum: ' + creationDate } contributionMessage.message = changeMessage contributionMessage.createdAt = new Date() contributionMessage.isModerator = false contributionMessage.userId = user.id contributionMessage.type = ContributionMessageType.HISTORY ContributionMessage.save(contributionMessage) contributionToUpdate.amount = amount contributionToUpdate.memo = memo contributionToUpdate.contributionDate = new Date(creationDate) contributionToUpdate.contributionStatus = ContributionStatus.PENDING contributionToUpdate.updatedAt = new Date() dbContribution.save(contributionToUpdate) const event = new Event() const eventUpdateContribution = new EventContributionUpdate() eventUpdateContribution.userId = user.id eventUpdateContribution.contributionId = contributionId eventUpdateContribution.amount = amount await eventProtocol.writeEvent(event.setEventContributionUpdate(eventUpdateContribution)) return new UnconfirmedContribution(contributionToUpdate, user, creations) } }