diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7000100e..b935ef8f4 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: 70 token: ${{ github.token }} ########################################################################## diff --git a/backend/.env.dist b/backend/.env.dist index 62b786456..41eeeaf58 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -1,4 +1,4 @@ -CONFIG_VERSION=v6.2022-04-21 +CONFIG_VERSION=v7.2022-06-15 # Server PORT=4000 @@ -28,6 +28,7 @@ COMMUNITY_NAME=Gradido Entwicklung COMMUNITY_URL=http://localhost/ COMMUNITY_REGISTER_URL=http://localhost/register COMMUNITY_REDEEM_URL=http://localhost/redeem/{code} +COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code} COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido. # Login Server diff --git a/backend/.env.template b/backend/.env.template index 140ec67e9..284abc204 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -27,6 +27,7 @@ COMMUNITY_NAME=$COMMUNITY_NAME COMMUNITY_URL=$COMMUNITY_URL COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL COMMUNITY_REDEEM_URL=$COMMUNITY_REDEEM_URL +COMMUNITY_REDEEM_CONTRIBUTION_URL=$COMMUNITY_REDEEM_CONTRIBUTION_URL COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION # Login Server diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 8188b3daa..8ac2c78cc 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -37,4 +37,8 @@ export enum RIGHTS { UNDELETE_USER = 'UNDELETE_USER', CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST', LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN', + CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK', + LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS', + DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK', + UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK', } diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 5736e6d8a..dafcd4bf0 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -17,7 +17,7 @@ const constants = { LOG_LEVEL: process.env.LOG_LEVEL || 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v6.2022-04-21', + EXPECTED: 'v7.2022-06-15', CURRENT: '', }, } @@ -54,6 +54,8 @@ const community = { COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/', COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register', COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}', + COMMUNITY_REDEEM_CONTRIBUTION_URL: + process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}', COMMUNITY_DESCRIPTION: process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.', } diff --git a/backend/src/graphql/arg/ContributionLinkArgs.ts b/backend/src/graphql/arg/ContributionLinkArgs.ts new file mode 100644 index 000000000..7344a28ff --- /dev/null +++ b/backend/src/graphql/arg/ContributionLinkArgs.ts @@ -0,0 +1,29 @@ +import { ArgsType, Field, Int } from 'type-graphql' +import Decimal from 'decimal.js-light' + +@ArgsType() +export default class ContributionLinkArgs { + @Field(() => Decimal) + amount: Decimal + + @Field(() => String) + name: string + + @Field(() => String) + memo: string + + @Field(() => String) + cycle: string + + @Field(() => String, { nullable: true }) + validFrom?: string | null + + @Field(() => String, { nullable: true }) + validTo?: string | null + + @Field(() => Decimal, { nullable: true }) + maxAmountPerMonth: Decimal | null + + @Field(() => Int) + maxPerCycle: number +} diff --git a/backend/src/graphql/enum/ContributionCycleType.ts b/backend/src/graphql/enum/ContributionCycleType.ts new file mode 100644 index 000000000..5fe494a02 --- /dev/null +++ b/backend/src/graphql/enum/ContributionCycleType.ts @@ -0,0 +1,28 @@ +import { registerEnumType } from 'type-graphql' + +export enum ContributionCycleType { + ONCE = 'once', + HOUR = 'hour', + TWO_HOURS = 'two_hours', + FOUR_HOURS = 'four_hours', + EIGHT_HOURS = 'eight_hours', + HALF_DAY = 'half_day', + DAY = 'day', + TWO_DAYS = 'two_days', + THREE_DAYS = 'three_days', + FOUR_DAYS = 'four_days', + FIVE_DAYS = 'five_days', + SIX_DAYS = 'six_days', + WEEK = 'week', + TWO_WEEKS = 'two_weeks', + MONTH = 'month', + TWO_MONTH = 'two_month', + QUARTER = 'quarter', + HALF_YEAR = 'half_year', + YEAR = 'year', +} + +registerEnumType(ContributionCycleType, { + name: 'ContributionCycleType', // this one is mandatory + description: 'Name of the Type of the ContributionCycle', // this one is optional +}) diff --git a/backend/src/graphql/model/ContributionLink.ts b/backend/src/graphql/model/ContributionLink.ts new file mode 100644 index 000000000..9fe9eccd6 --- /dev/null +++ b/backend/src/graphql/model/ContributionLink.ts @@ -0,0 +1,62 @@ +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 { + constructor(contributionLink: dbContributionLink) { + this.id = contributionLink.id + this.amount = contributionLink.amount + this.name = contributionLink.name + this.memo = contributionLink.memo + this.createdAt = contributionLink.createdAt + this.deletedAt = contributionLink.deletedAt + this.validFrom = contributionLink.validFrom + this.validTo = contributionLink.validTo + this.maxAmountPerMonth = contributionLink.maxAmountPerMonth + this.cycle = contributionLink.cycle + this.maxPerCycle = contributionLink.maxPerCycle + this.code = contributionLink.code + this.link = CONFIG.COMMUNITY_REDEEM_CONTRIBUTION_URL.replace(/{code}/g, this.code) + } + + @Field(() => Number) + id: number + + @Field(() => Decimal) + amount: Decimal + + @Field(() => String) + name: string + + @Field(() => String) + memo: string + + @Field(() => String) + code: string + + @Field(() => String) + link: string + + @Field(() => Date) + createdAt: Date + + @Field(() => Date, { nullable: true }) + deletedAt: Date | null + + @Field(() => Date, { nullable: true }) + validFrom: Date | null + + @Field(() => Date, { nullable: true }) + validTo: Date | null + + @Field(() => Decimal, { nullable: true }) + maxAmountPerMonth: Decimal | null + + @Field(() => String) + cycle: string + + @Field(() => Int) + maxPerCycle: number +} diff --git a/backend/src/graphql/model/ContributionLinkList.ts b/backend/src/graphql/model/ContributionLinkList.ts new file mode 100644 index 000000000..412d0bf7b --- /dev/null +++ b/backend/src/graphql/model/ContributionLinkList.ts @@ -0,0 +1,11 @@ +import { ObjectType, Field } from 'type-graphql' +import { ContributionLink } from '@model/ContributionLink' + +@ObjectType() +export class ContributionLinkList { + @Field(() => [ContributionLink]) + links: ContributionLink[] + + @Field(() => Number) + count: number +} diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index acf880efb..7417b529e 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -20,12 +20,16 @@ import { updatePendingCreation, deletePendingCreation, confirmPendingCreation, + createContributionLink, + deleteContributionLink, + updateContributionLink, } from '@/seeds/graphql/mutations' import { getPendingCreations, login, searchUsers, listTransactionLinksAdmin, + listContributionLinks, } from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' import { User } from '@entity/User' @@ -34,6 +38,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 +1598,361 @@ 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('returns an error', async () => { + await expect(mutate({ mutation: createContributionLink, variables })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('listContributionLinks', () => { + it('returns an error', async () => { + await expect(query({ query: listContributionLinks })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('updateContributionLink', () => { + it('returns an error', async () => { + await expect( + mutate({ + mutation: updateContributionLink, + variables: { + ...variables, + id: -1, + amount: new Decimal(400), + name: 'Dokumenta 2023', + memo: 'Danke für deine Teilnahme an der Dokumenta 2023', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('deleteContributionLink', () => { + it('returns an error', async () => { + await expect( + mutate({ mutation: deleteContributionLink, variables: { id: -1 } }), + ).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('returns an error', async () => { + await expect(mutate({ mutation: createContributionLink, variables })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('listContributionLinks', () => { + it('returns an error', async () => { + await expect(query({ query: listContributionLinks })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('updateContributionLink', () => { + it('returns an error', async () => { + await expect( + mutate({ + mutation: updateContributionLink, + variables: { + ...variables, + id: -1, + amount: new Decimal(400), + name: 'Dokumenta 2023', + memo: 'Danke für deine Teilnahme an der Dokumenta 2023', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('deleteContributionLink', () => { + it('returns an error', async () => { + await expect( + mutate({ mutation: deleteContributionLink, variables: { id: -1 } }), + ).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('returns a contribution link object', async () => { + await expect(mutate({ mutation: createContributionLink, variables })).resolves.toEqual( + expect.objectContaining({ + data: { + createContributionLink: expect.objectContaining({ + id: expect.any(Number), + amount: '200', + code: expect.stringMatching(/^[0-9a-f]{24,24}$/), + link: expect.stringMatching(/^.*?\/CL-[0-9a-f]{24,24}$/), + 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('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', + }), + ) + }) + }) + + describe('listContributionLinks', () => { + describe('one link in DB', () => { + it('returns the link and count 1', async () => { + await expect(query({ query: listContributionLinks })).resolves.toEqual( + expect.objectContaining({ + data: { + listContributionLinks: { + links: expect.arrayContaining([ + expect.objectContaining({ + amount: '200', + code: expect.stringMatching(/^[0-9a-f]{24,24}$/), + link: expect.stringMatching(/^.*?\/CL-[0-9a-f]{24,24}$/), + 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, + }), + ]), + count: 1, + }, + }, + }), + ) + }) + }) + }) + + describe('updateContributionLink', () => { + describe('no valid id', () => { + it('returns an error', async () => { + await expect( + mutate({ + mutation: updateContributionLink, + variables: { + ...variables, + id: -1, + amount: new Decimal(400), + name: 'Dokumenta 2023', + memo: 'Danke für deine Teilnahme an der Dokumenta 2023', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Contribution Link not found to given id.')], + }), + ) + }) + }) + + describe('valid id', () => { + let linkId: number + beforeAll(async () => { + const links = await query({ query: listContributionLinks }) + linkId = links.data.listContributionLinks.links[0].id + }) + + it('returns updated contribution link object', async () => { + await expect( + mutate({ + mutation: updateContributionLink, + variables: { + ...variables, + id: linkId, + amount: new Decimal(400), + name: 'Dokumenta 2023', + memo: 'Danke für deine Teilnahme an der Dokumenta 2023', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + updateContributionLink: { + id: linkId, + amount: '400', + code: expect.stringMatching(/^[0-9a-f]{24,24}$/), + link: expect.stringMatching(/^.*?\/CL-[0-9a-f]{24,24}$/), + createdAt: expect.any(String), + name: 'Dokumenta 2023', + memo: 'Danke für deine Teilnahme an der Dokumenta 2023', + validFrom: expect.any(String), + validTo: expect.any(String), + maxAmountPerMonth: '200', + cycle: 'once', + maxPerCycle: 1, + }, + }, + }), + ) + }) + + it('updated the DB record', async () => { + await expect(DbContributionLink.findOne(linkId)).resolves.toEqual( + expect.objectContaining({ + id: linkId, + name: 'Dokumenta 2023', + memo: 'Danke für deine Teilnahme an der Dokumenta 2023', + // amount: '400', + }), + ) + }) + }) + }) + + describe('deleteContributionLink', () => { + describe('no valid id', () => { + it('returns an error', async () => { + await expect( + mutate({ mutation: deleteContributionLink, variables: { id: -1 } }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Contribution Link not found to given id.')], + }), + ) + }) + }) + + describe('valid id', () => { + let linkId: number + beforeAll(async () => { + const links = await query({ query: listContributionLinks }) + linkId = links.data.listContributionLinks.links[0].id + }) + + it('returns a date string', async () => { + await expect( + mutate({ mutation: deleteContributionLink, variables: { id: linkId } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + deleteContributionLink: expect.any(String), + }, + }), + ) + }) + + it('does not list this contribution link anymore', async () => { + await expect(query({ query: listContributionLinks })).resolves.toEqual( + expect.objectContaining({ + data: { + listContributionLinks: { + links: [], + count: 0, + }, + }, + }), + ) + }) + }) + }) + }) + }) + }) }) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 4c94e48c8..785a7de02 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -1,4 +1,5 @@ import { Context, getUser } from '@/server/context' +import { backendLogger as logger } from '@/server/logger' import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx, Int } from 'type-graphql' import { getCustomRepository, @@ -14,12 +15,16 @@ import { UserAdmin, SearchUsersResult } from '@model/UserAdmin' import { PendingCreation } from '@model/PendingCreation' import { CreatePendingCreations } from '@model/CreatePendingCreations' import { UpdatePendingCreation } from '@model/UpdatePendingCreation' +import { ContributionLink } from '@model/ContributionLink' +import { ContributionLinkList } from '@model/ContributionLinkList' import { RIGHTS } from '@/auth/RIGHTS' import { UserRepository } from '@repository/User' import CreatePendingCreationArgs from '@arg/CreatePendingCreationArgs' import UpdatePendingCreationArgs from '@arg/UpdatePendingCreationArgs' import SearchUsersArgs from '@arg/SearchUsersArgs' +import ContributionLinkArgs from '@arg/ContributionLinkArgs' import { Transaction as DbTransaction } from '@entity/Transaction' +import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' import { Transaction } from '@model/Transaction' import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' @@ -39,6 +44,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 @@ -460,6 +466,99 @@ export class AdminResolver { linkList: transactionLinks.map((tl) => new TransactionLink(tl, new User(user))), } } + + @Authorized([RIGHTS.CREATE_CONTRIBUTION_LINK]) + @Mutation(() => ContributionLink) + async createContributionLink( + @Args() + { + amount, + name, + memo, + cycle, + validFrom, + validTo, + maxAmountPerMonth, + maxPerCycle, + }: ContributionLinkArgs, + ): Promise { + const dbContributionLink = new DbContributionLink() + dbContributionLink.amount = amount + dbContributionLink.name = name + dbContributionLink.memo = memo + dbContributionLink.createdAt = new Date() + dbContributionLink.code = contributionLinkCode(dbContributionLink.createdAt) + dbContributionLink.cycle = cycle + if (validFrom) dbContributionLink.validFrom = new Date(validFrom) + if (validTo) dbContributionLink.validTo = new Date(validTo) + dbContributionLink.maxAmountPerMonth = maxAmountPerMonth + dbContributionLink.maxPerCycle = maxPerCycle + await dbContributionLink.save() + return new ContributionLink(dbContributionLink) + } + + @Authorized([RIGHTS.LIST_CONTRIBUTION_LINKS]) + @Query(() => ContributionLinkList) + async listContributionLinks( + @Args() + { currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated, + ): Promise { + const [links, count] = await DbContributionLink.findAndCount({ + order: { createdAt: order }, + skip: (currentPage - 1) * pageSize, + take: pageSize, + }) + return { + links: links.map((link: DbContributionLink) => new ContributionLink(link)), + count, + } + } + + @Authorized([RIGHTS.DELETE_CONTRIBUTION_LINK]) + @Mutation(() => Date, { nullable: true }) + async deleteContributionLink(@Arg('id', () => Int) id: number): Promise { + const contributionLink = await DbContributionLink.findOne(id) + if (!contributionLink) { + logger.error(`Contribution Link not found to given id: ${id}`) + throw new Error('Contribution Link not found to given id.') + } + await contributionLink.softRemove() + const newContributionLink = await DbContributionLink.findOne({ id }, { withDeleted: true }) + return newContributionLink ? newContributionLink.deletedAt : null + } + + @Authorized([RIGHTS.UPDATE_CONTRIBUTION_LINK]) + @Mutation(() => ContributionLink) + async updateContributionLink( + @Args() + { + amount, + name, + memo, + cycle, + validFrom, + validTo, + maxAmountPerMonth, + maxPerCycle, + }: ContributionLinkArgs, + @Arg('id', () => Int) id: number, + ): Promise { + const dbContributionLink = await DbContributionLink.findOne(id) + if (!dbContributionLink) { + logger.error(`Contribution Link not found to given id: ${id}`) + throw new Error('Contribution Link not found to given id.') + } + dbContributionLink.amount = amount + dbContributionLink.name = name + dbContributionLink.memo = memo + dbContributionLink.cycle = cycle + if (validFrom) dbContributionLink.validFrom = new Date(validFrom) + if (validTo) dbContributionLink.validTo = new Date(validTo) + dbContributionLink.maxAmountPerMonth = maxAmountPerMonth + dbContributionLink.maxPerCycle = maxPerCycle + await dbContributionLink.save() + return new ContributionLink(dbContributionLink) + } } interface CreationMap { diff --git a/backend/src/seeds/contributionLink/ContributionLinkInterface.ts b/backend/src/seeds/contributionLink/ContributionLinkInterface.ts new file mode 100644 index 000000000..15ba4b72d --- /dev/null +++ b/backend/src/seeds/contributionLink/ContributionLinkInterface.ts @@ -0,0 +1,7 @@ +export interface ContributionLinkInterface { + amount: number + name: string + memo: string + validFrom?: Date + validTo?: Date +} diff --git a/backend/src/seeds/contributionLink/index.ts b/backend/src/seeds/contributionLink/index.ts new file mode 100644 index 000000000..41d28eb60 --- /dev/null +++ b/backend/src/seeds/contributionLink/index.ts @@ -0,0 +1,18 @@ +import { ContributionLinkInterface } from './ContributionLinkInterface' + +export const contributionLinks: ContributionLinkInterface[] = [ + { + name: 'Dokumenta 2017', + memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2017', + amount: 200, + validFrom: new Date(2017, 3, 8), + validTo: new Date(2017, 6, 16), + }, + { + name: 'Dokumenta 2022', + memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022', + amount: 200, + validFrom: new Date(2022, 5, 18), + validTo: new Date(2022, 8, 25), + }, +] diff --git a/backend/src/seeds/factory/contributionLink.ts b/backend/src/seeds/factory/contributionLink.ts new file mode 100644 index 000000000..7e34b9d20 --- /dev/null +++ b/backend/src/seeds/factory/contributionLink.ts @@ -0,0 +1,27 @@ +import { ApolloServerTestClient } from 'apollo-server-testing' +import { createContributionLink } from '@/seeds/graphql/mutations' +import { login } from '@/seeds/graphql/queries' +import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface' + +export const contributionLinkFactory = async ( + client: ApolloServerTestClient, + contributionLink: ContributionLinkInterface, +): Promise => { + const { mutate, query } = client + + // login as admin + await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } }) + + const variables = { + amount: contributionLink.amount, + memo: contributionLink.memo, + name: contributionLink.name, + cycle: 'ONCE', + maxPerCycle: 1, + maxAmountPerMonth: 200, + validFrom: contributionLink.validFrom ? contributionLink.validFrom.toISOString() : undefined, + validTo: contributionLink.validTo ? contributionLink.validTo.toISOString() : undefined, + } + + await mutate({ mutation: createContributionLink, variables }) +} diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index e66827566..253f78e2a 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -137,3 +137,85 @@ 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 + ) { + id + amount + name + memo + code + link + createdAt + validFrom + validTo + maxAmountPerMonth + cycle + maxPerCycle + } + } +` + +export const updateContributionLink = gql` + mutation ( + $amount: Decimal! + $name: String! + $memo: String! + $cycle: String! + $validFrom: String + $validTo: String + $maxAmountPerMonth: Decimal + $maxPerCycle: Int! = 1 + $id: Int! + ) { + updateContributionLink( + amount: $amount + name: $name + memo: $memo + cycle: $cycle + validFrom: $validFrom + validTo: $validTo + maxAmountPerMonth: $maxAmountPerMonth + maxPerCycle: $maxPerCycle + id: $id + ) { + id + amount + name + memo + code + link + createdAt + validFrom + validTo + maxAmountPerMonth + cycle + maxPerCycle + } + } +` + +export const deleteContributionLink = gql` + mutation ($id: Int!) { + deleteContributionLink(id: $id) + } +` diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 03ee3b53e..9a0e00be3 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -217,3 +217,25 @@ export const listTransactionLinksAdmin = gql` } } ` + +export const listContributionLinks = gql` + query ($pageSize: Int = 25, $currentPage: Int = 1, $order: Order) { + listContributionLinks(pageSize: $pageSize, currentPage: $currentPage, order: $order) { + links { + id + amount + name + memo + code + link + createdAt + validFrom + validTo + maxAmountPerMonth + cycle + maxPerCycle + } + count + } + } +` diff --git a/backend/src/seeds/index.ts b/backend/src/seeds/index.ts index 710f255ee..8e9a4e2d8 100644 --- a/backend/src/seeds/index.ts +++ b/backend/src/seeds/index.ts @@ -9,9 +9,11 @@ import { name, internet, datatype } from 'faker' import { users } from './users/index' import { creations } from './creation/index' import { transactionLinks } from './transactionLink/index' +import { contributionLinks } from './contributionLink/index' import { userFactory } from './factory/user' import { creationFactory } from './factory/creation' import { transactionLinkFactory } from './factory/transactionLink' +import { contributionLinkFactory } from './factory/contributionLink' import { entities } from '@entity/index' import CONFIG from '@/config' @@ -77,6 +79,11 @@ const run = async () => { await transactionLinkFactory(seedClient, transactionLinks[i]) } + // create Contribution Links + for (let i = 0; i < contributionLinks.length; i++) { + await contributionLinkFactory(seedClient, contributionLinks[i]) + } + await con.close() } diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index a1751a859..d9e159382 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -22,10 +22,11 @@ COMMUNITY_NAME="Gradido Development Stage1" COMMUNITY_URL=https://stage1.gradido.net/ COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code} +COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code} COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community" # backend -BACKEND_CONFIG_VERSION=v6.2022-04-21 +BACKEND_CONFIG_VERSION=v7.2022-06-15 JWT_EXPIRES_IN=30m GDT_API_URL=https://gdt.gradido.net