diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index 3d40adbf6..275242bd3 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -6,8 +6,15 @@ import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' import { cleanDB, testEnvironment } from '@test/helpers' import { userFactory } from '@/seeds/factory/user' -import { login, createContributionLink, redeemTransactionLink } from '@/seeds/graphql/mutations' +import { + login, + createContributionLink, + redeemTransactionLink, + createContribution, + updateContribution, +} from '@/seeds/graphql/mutations' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' +import { UnconfirmedContribution } from '@model/UnconfirmedContribution' import Decimal from 'decimal.js-light' import { GraphQLError } from 'graphql' @@ -32,6 +39,7 @@ describe('TransactionLinkResolver', () => { describe('redeem daily Contribution Link', () => { const now = new Date() let contributionLink: DbContributionLink | undefined + let contribution: UnconfirmedContribution | undefined beforeAll(async () => { await mutate({ @@ -79,56 +87,59 @@ describe('TransactionLinkResolver', () => { ) }) - it('allows the user to redeem the contribution link', async () => { - await expect( - mutate({ - mutation: redeemTransactionLink, - variables: { - code: 'CL-' + (contributionLink ? contributionLink.code : ''), - }, - }), - ).resolves.toMatchObject({ - data: { - redeemTransactionLink: true, - }, - errors: undefined, - }) - }) - - it('does not allow the user to redeem the contribution link a second time on the same day', async () => { - await expect( - mutate({ - mutation: redeemTransactionLink, - variables: { - code: 'CL-' + (contributionLink ? contributionLink.code : ''), - }, - }), - ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: Contribution link already redeemed today', - ), - ], - }) - }) - - describe('after one day', () => { + describe('user has pending contribution of 1000 GDD', () => { beforeAll(async () => { - jest.useFakeTimers() - /* eslint-disable-next-line @typescript-eslint/no-empty-function */ - setTimeout(() => {}, 1000 * 60 * 60 * 24) - jest.runAllTimers() await mutate({ mutation: login, - variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + const result = await mutate({ + mutation: createContribution, + variables: { + amount: new Decimal(1000), + memo: 'I was brewing potions for the community the whole month', + creationDate: now.toISOString(), + }, + }) + contribution = result.data.createContribution + }) + + it('does not allow the user to redeem the contribution link', async () => { + await expect( + mutate({ + mutation: redeemTransactionLink, + variables: { + code: 'CL-' + (contributionLink ? contributionLink.code : ''), + }, + }), + ).resolves.toMatchObject({ + errors: [ + new GraphQLError( + 'Creation from contribution link was not successful. Error: The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.', + ), + ], + }) + }) + }) + + describe('user has no pending contributions that would not allow to redeem the link', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + await mutate({ + mutation: updateContribution, + variables: { + contributionId: contribution ? contribution.id : -1, + amount: new Decimal(800), + memo: 'I was brewing potions for the community the whole month', + creationDate: now.toISOString(), + }, }) }) - afterAll(() => { - jest.useRealTimers() - }) - - it('allows the user to redeem the contribution link again', async () => { + it('allows the user to redeem the contribution link', async () => { await expect( mutate({ mutation: redeemTransactionLink, @@ -160,6 +171,56 @@ describe('TransactionLinkResolver', () => { ], }) }) + + describe('after one day', () => { + beforeAll(async () => { + jest.useFakeTimers() + /* eslint-disable-next-line @typescript-eslint/no-empty-function */ + setTimeout(() => {}, 1000 * 60 * 60 * 24) + jest.runAllTimers() + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(() => { + jest.useRealTimers() + }) + + it('allows the user to redeem the contribution link again', async () => { + await expect( + mutate({ + mutation: redeemTransactionLink, + variables: { + code: 'CL-' + (contributionLink ? contributionLink.code : ''), + }, + }), + ).resolves.toMatchObject({ + data: { + redeemTransactionLink: true, + }, + errors: undefined, + }) + }) + + it('does not allow the user to redeem the contribution link a second time on the same day', async () => { + await expect( + mutate({ + mutation: redeemTransactionLink, + variables: { + code: 'CL-' + (contributionLink ? contributionLink.code : ''), + }, + }), + ).resolves.toMatchObject({ + errors: [ + new GraphQLError( + 'Creation from contribution link was not successful. Error: Contribution link already redeemed today', + ), + ], + }) + }) + }) }) }) }) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 4ba5dcd0b..74c531c54 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -258,7 +258,7 @@ export class TransactionLinkResolver { } } - const creations = await getUserCreation(user.id, false) + const creations = await getUserCreation(user.id) logger.info('open creations', creations) validateContribution(creations, contributionLink.amount, now) const contribution = new DbContribution() diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index 9987dfae6..abf4017cb 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -1,4 +1,3 @@ -import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' import { backendLogger as logger } from '@/server/logger' import { getConnection } from '@dbTools/typeorm' import { Contribution } from '@entity/Contribution' @@ -50,27 +49,27 @@ export const getUserCreations = async ( 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` - : '' - logger.trace('getUserCreations unionString=', unionString) + const sumAmountContributionPerUserAndLast3MonthQuery = queryRunner.manager + .createQueryBuilder(Contribution, 'c') + .select('month(contribution_date)', 'month') + .addSelect('user_id', 'userId') + .addSelect('sum(amount)', 'sum') + .where(`user_id in (${ids.toString()})`) + .andWhere(`contribution_date >= ${dateFilter}`) + .andWhere('deleted_at IS NULL') + .andWhere('denied_at IS NULL') + .groupBy('month') + .addGroupBy('userId') + .orderBy('month', 'DESC') - 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 - `) - logger.trace('getUserCreations unionQuery=', unionQuery) + if (!includePending) { + sumAmountContributionPerUserAndLast3MonthQuery.andWhere('confirmed_at IS NOT NULL') + } + + const sumAmountContributionPerUserAndLast3Month = + await sumAmountContributionPerUserAndLast3MonthQuery.getRawMany() + + logger.trace(sumAmountContributionPerUserAndLast3Month) await queryRunner.release() @@ -78,9 +77,9 @@ export const getUserCreations = async ( 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, + const creation = sumAmountContributionPerUserAndLast3Month.find( + (raw: { month: string; userId: string; creation: number[] }) => + parseInt(raw.month) === month && parseInt(raw.userId) === id, ) return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0) }),