diff --git a/backend/src/graphql/arg/ContributionLinkArgs.ts b/backend/src/graphql/arg/ContributionLinkArgs.ts index 809ce89c4..7344a28ff 100644 --- a/backend/src/graphql/arg/ContributionLinkArgs.ts +++ b/backend/src/graphql/arg/ContributionLinkArgs.ts @@ -15,11 +15,11 @@ export default class ContributionLinkArgs { @Field(() => String) cycle: string - @Field(() => Date, { nullable: true }) - validFrom?: Date | null + @Field(() => String, { nullable: true }) + validFrom?: string | null - @Field(() => Date, { nullable: true }) - validTo?: Date | null + @Field(() => String, { nullable: true }) + validTo?: string | null @Field(() => Decimal, { nullable: true }) maxAmountPerMonth: Decimal | null diff --git a/backend/src/graphql/model/ContributionLink.ts b/backend/src/graphql/model/ContributionLink.ts index 9fb60e4aa..e4b37ad59 100644 --- a/backend/src/graphql/model/ContributionLink.ts +++ b/backend/src/graphql/model/ContributionLink.ts @@ -1,6 +1,7 @@ import { ObjectType, Field, Int } from 'type-graphql' import Decimal from 'decimal.js-light' import { ContributionLink as dbContributionLink } from '@entity/ContributionLink' +import CONFIG from '@/config' @ObjectType() export class ContributionLink { @@ -16,6 +17,8 @@ export class ContributionLink { this.maxAmountPerMonth = contributionLink.maxAmountPerMonth this.cycle = contributionLink.cycle this.maxPerCycle = contributionLink.maxPerCycle + this.code = 'CL-' + contributionLink.code + this.link = CONFIG.COMMUNITY_REDEEM_URL.replace(/{code}/g, this.code) } @Field(() => Number) @@ -33,6 +36,9 @@ export class ContributionLink { @Field(() => String) code: string + @Field(() => String) + link: string + @Field(() => Date) createdAt: Date diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index acf880efb..6b0f295c7 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -20,6 +20,7 @@ import { updatePendingCreation, deletePendingCreation, confirmPendingCreation, + createContributionLink, } from '@/seeds/graphql/mutations' import { getPendingCreations, @@ -34,6 +35,7 @@ import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' import Decimal from 'decimal.js-light' import { AdminPendingCreation } from '@entity/AdminPendingCreation' import { Transaction as DbTransaction } from '@entity/Transaction' +import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' // mock account activation email to avoid console spam jest.mock('@/mailer/sendAccountActivationEmail', () => { @@ -1593,4 +1595,120 @@ describe('AdminResolver', () => { }) }) }) + + describe('Contribution Links', () => { + const variables = { + amount: new Decimal(200), + name: 'Dokumenta 2022', + memo: 'Danke für deine Teilnahme an der Dokumenta 2022', + cycle: 'once', + validFrom: new Date(2022, 5, 18).toISOString(), + validTo: new Date(2022, 7, 14).toISOString(), + maxAmountPerMonth: new Decimal(200), + maxPerCycle: 1, + } + + describe('unauthenticated', () => { + describe('createContributionLink', () => { + it.only('returns an error', async () => { + await expect(mutate({ mutation: createContributionLink, variables })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + }) + + describe('authenticated', () => { + describe('without admin rights', () => { + beforeAll(async () => { + user = await userFactory(testEnv, bibiBloxberg) + await query({ + query: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) + + describe('createContributionLink', () => { + it.only('returns an error', async () => { + await expect(mutate({ mutation: createContributionLink, variables })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + }) + + describe('with admin rights', () => { + beforeAll(async () => { + user = await userFactory(testEnv, peterLustig) + await query({ + query: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) + + describe('createContributionLink', () => { + it.only('returns a contribution link object', async () => { + await expect(mutate({ mutation: createContributionLink, variables })).resolves.toEqual( + expect.objectContaining({ + data: { + createContributionLink: expect.objectContaining({ + amount: '200', + code: expect.stringMatching(/^CL-[0-9a-f]{24,24}$/), + link: expect.any(String), + createdAt: expect.any(String), + name: 'Dokumenta 2022', + memo: 'Danke für deine Teilnahme an der Dokumenta 2022', + validFrom: expect.any(String), + validTo: expect.any(String), + maxAmountPerMonth: '200', + cycle: 'once', + maxPerCycle: 1, + }), + }, + }), + ) + }) + + it.only('has a contribution link stored in db', async () => { + const cls = await DbContributionLink.find() + expect(cls).toHaveLength(1) + expect(cls[0]).toEqual( + expect.objectContaining({ + id: expect.any(Number), + name: 'Dokumenta 2022', + memo: 'Danke für deine Teilnahme an der Dokumenta 2022', + validFrom: new Date('2022-06-18T00:00:00.000Z'), + validTo: new Date('2022-08-14T00:00:00.000Z'), + cycle: 'once', + 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', + }), + ) + }) + }) + }) + }) + }) }) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index d99bf6c6f..d7c843611 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -42,6 +42,7 @@ import { Order } from '@enum/Order' import { communityUser } from '@/util/communityUser' import { checkOptInCode, activationLink, printTimeDuration } from './UserResolver' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' +import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import CONFIG from '@/config' // const EMAIL_OPT_IN_REGISTER = 1 @@ -484,9 +485,10 @@ export class AdminResolver { dbContributionLink.name = name dbContributionLink.memo = memo dbContributionLink.createdAt = new Date() + dbContributionLink.code = contributionLinkCode(dbContributionLink.createdAt) dbContributionLink.cycle = cycle - if (validFrom) dbContributionLink.validFrom = validFrom - if (validTo) dbContributionLink.validTo = validTo + if (validFrom) dbContributionLink.validFrom = new Date(validFrom) + if (validTo) dbContributionLink.validTo = new Date(validTo) dbContributionLink.maxAmountPerMonth = maxAmountPerMonth dbContributionLink.maxPerCycle = maxPerCycle dbContributionLink.save() diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index e66827566..355b5aa79 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -137,3 +137,39 @@ export const deletePendingCreation = gql` deletePendingCreation(id: $id) } ` + +export const createContributionLink = gql` + mutation ( + $amount: Decimal! + $name: String! + $memo: String! + $cycle: String! + $validFrom: String + $validTo: String + $maxAmountPerMonth: Decimal + $maxPerCycle: Int! = 1 + ) { + createContributionLink( + amount: $amount + name: $name + memo: $memo + cycle: $cycle + validFrom: $validFrom + validTo: $validTo + maxAmountPerMonth: $maxAmountPerMonth + maxPerCycle: $maxPerCycle + ) { + amount + name + memo + code + link + createdAt + validFrom + validTo + maxAmountPerMonth + cycle + maxPerCycle + } + } +`