diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7000100e..34ebeff11 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -528,7 +528,7 @@ jobs: report_name: Coverage Backend type: lcov result_path: ./backend/coverage/lcov.info - min_coverage: 68 + min_coverage: 74 token: ${{ github.token }} ########################################################################## diff --git a/backend/src/graphql/enum/ContributionCycleType.ts b/backend/src/graphql/enum/ContributionCycleType.ts index 5fe494a02..a3c55aa68 100644 --- a/backend/src/graphql/enum/ContributionCycleType.ts +++ b/backend/src/graphql/enum/ContributionCycleType.ts @@ -1,13 +1,14 @@ import { registerEnumType } from 'type-graphql' +// lowercase values are not implemented yet export enum ContributionCycleType { - ONCE = 'once', + ONCE = 'ONCE', HOUR = 'hour', TWO_HOURS = 'two_hours', FOUR_HOURS = 'four_hours', EIGHT_HOURS = 'eight_hours', HALF_DAY = 'half_day', - DAY = 'day', + DAILY = 'DAILY', TWO_DAYS = 'two_days', THREE_DAYS = 'three_days', FOUR_DAYS = 'four_days', diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index 5a1a39dca..e527eeb8a 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -1,4 +1,120 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + import { transactionLinkCode } from './TransactionLinkResolver' +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 { createContributionLink, redeemTransactionLink } from '@/seeds/graphql/mutations' +import { login } from '@/seeds/graphql/queries' +import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' +import Decimal from 'decimal.js-light' +import { GraphQLError } from 'graphql' + +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() + await userFactory(testEnv, bibiBloxberg) + await userFactory(testEnv, peterLustig) +}) + +afterAll(async () => { + await cleanDB() + await con.close() +}) + +describe('TransactionLinkResolver', () => { + describe('redeem daily Contribution Link', () => { + const now = new Date() + let contributionLink: DbContributionLink | undefined + + beforeAll(async () => { + await query({ + query: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + await mutate({ + mutation: createContributionLink, + variables: { + amount: new Decimal(5), + name: 'Daily Contribution Link', + memo: 'Thank you for contribute daily to the community', + cycle: 'DAILY', + validFrom: new Date(now.getFullYear(), 0, 1).toISOString(), + validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(), + maxAmountPerMonth: new Decimal(200), + maxPerCycle: 1, + }, + }) + }) + + it('has a daily contribution link in the database', async () => { + const cls = await DbContributionLink.find() + expect(cls).toHaveLength(1) + contributionLink = cls[0] + expect(contributionLink).toEqual( + expect.objectContaining({ + id: expect.any(Number), + name: 'Daily Contribution Link', + memo: 'Thank you for contribute daily to the community', + validFrom: new Date(now.getFullYear(), 0, 1), + validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 0), + cycle: 'DAILY', + maxPerCycle: 1, + totalMaxCountOfContribution: null, + maxAccountBalance: null, + minGapHours: null, + createdAt: expect.any(Date), + deletedAt: null, + code: expect.stringMatching(/^[0-9a-f]{24,24}$/), + linkEnabled: true, + // amount: '200', + // maxAmountPerMonth: '200', + }), + ) + }) + + 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', 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('transactionLinkCode', () => { const date = new Date() diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index c9acbace3..e6b28a0f8 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -1,6 +1,6 @@ import { backendLogger as logger } from '@/server/logger' import { Context, getUser } from '@/server/context' -import { getConnection } from '@dbTools/typeorm' +import { getConnection, Between } from '@dbTools/typeorm' import { Resolver, Args, @@ -34,6 +34,7 @@ import { getUserCreation, validateContribution } from './util/creations' import { Decay } from '@model/Decay' import Decimal from 'decimal.js-light' import { TransactionTypeId } from '@enum/TransactionTypeId' +import { ContributionCycleType } from '@enum/ContributionCycleType' const QueryLinkResult = createUnionType({ name: 'QueryLinkResult', // the name of the GraphQL union @@ -204,23 +205,55 @@ export class TransactionLinkResolver { throw new Error('Contribution link is depricated') } } - if (contributionLink.cycle !== 'ONCE') { - logger.error('contribution link has unknown cycle', contributionLink.cycle) - throw new Error('Contribution link has unknown cycle') - } - // Test ONCE rule - const alreadyRedeemed = await queryRunner.manager - .createQueryBuilder() - .select('contribution') - .from(DbContribution, 'contribution') - .where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', { - linkId: contributionLink.id, - id: user.id, - }) - .getOne() - if (alreadyRedeemed) { - logger.error('contribution link with rule ONCE already redeemed by user with id', user.id) - throw new Error('Contribution link already redeemed') + let alreadyRedeemed: DbContribution | undefined + switch (contributionLink.cycle) { + case ContributionCycleType.ONCE: { + alreadyRedeemed = await queryRunner.manager + .createQueryBuilder() + .select('contribution') + .from(DbContribution, 'contribution') + .where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', { + linkId: contributionLink.id, + id: user.id, + }) + .getOne() + if (alreadyRedeemed) { + logger.error( + 'contribution link with rule ONCE already redeemed by user with id', + user.id, + ) + throw new Error('Contribution link already redeemed') + } + break + } + case ContributionCycleType.DAILY: { + const start = new Date() + start.setHours(0, 0, 0, 0) + const end = new Date() + end.setHours(23, 59, 59, 999) + alreadyRedeemed = await queryRunner.manager + .createQueryBuilder() + .select('contribution') + .from(DbContribution, 'contribution') + .where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', { + linkId: contributionLink.id, + id: user.id, + contributionDate: Between(start, end), + }) + .getOne() + if (alreadyRedeemed) { + logger.error( + 'contribution link with rule DAILY already redeemed by user with id', + user.id, + ) + throw new Error('Contribution link already redeemed today') + } + break + } + default: { + logger.error('contribution link has unknown cycle', contributionLink.cycle) + throw new Error('Contribution link has unknown cycle') + } } const creations = await getUserCreation(user.id, false) diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index e5f290645..99dd0e7bd 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -289,3 +289,9 @@ export const adminCreateContributionMessage = gql` } } ` + +export const redeemTransactionLink = gql` + mutation ($code: String!) { + redeemTransactionLink(code: $code) + } +` diff --git a/backend/yarn.lock b/backend/yarn.lock index dd84e2ce5..1e5647b6f 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1668,9 +1668,9 @@ camelcase@^6.2.0: integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== caniuse-lite@^1.0.30001264: - version "1.0.30001325" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001325.tgz" - integrity sha512-sB1bZHjseSjDtijV1Hb7PB2Zd58Kyx+n/9EotvZ4Qcz2K3d0lWB8dB4nb8wN/TsOGFq3UuAm0zQZNQ4SoR7TrQ== + version "1.0.30001418" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz" + integrity sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg== chalk@^2.0.0: version "2.4.2"