diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index c9543902e..f31dfc93d 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0050-add_messageId_to_event_protocol', + DB_VERSION: '0051-add_delete_by_to_contributions', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 8d9e4bed0..281f2b99b 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -17,6 +17,7 @@ import { setUserRole, deleteUser, unDeleteUser, + createContribution, adminCreateContribution, adminCreateContributions, adminUpdateContribution, @@ -77,6 +78,7 @@ afterAll(async () => { let admin: User let user: User let creation: Contribution | void +let result: any describe('AdminResolver', () => { describe('set user role', () => { @@ -1365,6 +1367,38 @@ describe('AdminResolver', () => { }) }) + describe('admin deletes own user contribution', () => { + beforeAll(async () => { + await query({ + query: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + result = await mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test env contribution', + creationDate: new Date().toString(), + }, + }) + }) + + it('throws an error', async () => { + await expect( + mutate({ + mutation: adminDeleteContribution, + variables: { + id: result.data.createContribution.id, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Own contribution can not be deleted as admin')], + }), + ) + }) + }) + describe('creation id does exist', () => { it('returns true', async () => { await expect( diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 2193fe7a3..4fd85dc0f 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -400,13 +400,24 @@ export class AdminResolver { @Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION]) @Mutation(() => Boolean) - async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise { + async adminDeleteContribution( + @Arg('id', () => Int) id: number, + @Ctx() context: Context, + ): Promise { const contribution = await DbContribution.findOne(id) if (!contribution) { logger.error(`Contribution not found for given id: ${id}`) throw new Error('Contribution not found for given id.') } + const moderator = getUser(context) + if ( + contribution.contributionType === ContributionType.USER && + contribution.userId === moderator.id + ) { + throw new Error('Own contribution can not be deleted as admin') + } contribution.contributionStatus = ContributionStatus.DELETED + contribution.deletedBy = moderator.id await contribution.save() const res = await contribution.softRemove() return !!res diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 0741533cb..aec7bc44d 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -91,6 +91,7 @@ export class ContributionResolver { } contribution.contributionStatus = ContributionStatus.DELETED + contribution.deletedBy = user.id contribution.deletedAt = new Date() await contribution.save() diff --git a/backend/src/mailer/sendAddedContributionMessageEmail.test.ts b/backend/src/mailer/sendAddedContributionMessageEmail.test.ts index 1151a0abc..bed8f6214 100644 --- a/backend/src/mailer/sendAddedContributionMessageEmail.test.ts +++ b/backend/src/mailer/sendAddedContributionMessageEmail.test.ts @@ -26,7 +26,7 @@ describe('sendAddedContributionMessageEmail', () => { it('calls sendEMail', () => { expect(sendEMail).toBeCalledWith({ to: `Bibi Bloxberg `, - subject: 'Gradido Frage zur Schöpfung', + subject: 'Rückfrage zu Deinem Gemeinwohl-Beitrag', text: expect.stringContaining('Hallo Bibi Bloxberg') && expect.stringContaining('Peter Lustig') && diff --git a/backend/src/mailer/text/contributionMessageReceived.ts b/backend/src/mailer/text/contributionMessageReceived.ts index b0c9c4d30..af1cabb9f 100644 --- a/backend/src/mailer/text/contributionMessageReceived.ts +++ b/backend/src/mailer/text/contributionMessageReceived.ts @@ -1,6 +1,6 @@ export const contributionMessageReceived = { de: { - subject: 'Gradido Frage zur Schöpfung', + subject: 'Rückfrage zu Deinem Gemeinwohl-Beitrag', text: (data: { senderFirstName: string senderLastName: string diff --git a/database/entity/0051-add_delete_by_to_contributions/Contribution.ts b/database/entity/0051-add_delete_by_to_contributions/Contribution.ts new file mode 100644 index 000000000..32c6f32a3 --- /dev/null +++ b/database/entity/0051-add_delete_by_to_contributions/Contribution.ts @@ -0,0 +1,92 @@ +import Decimal from 'decimal.js-light' +import { + BaseEntity, + Column, + Entity, + PrimaryGeneratedColumn, + DeleteDateColumn, + JoinColumn, + ManyToOne, + OneToMany, +} from 'typeorm' +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' +import { User } from '../User' +import { ContributionMessage } from '../ContributionMessage' + +@Entity('contributions') +export class Contribution extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ unsigned: true, nullable: false, name: 'user_id' }) + userId: number + + @ManyToOne(() => User, (user) => user.contributions) + @JoinColumn({ name: 'user_id' }) + user: User + + @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' }) + createdAt: Date + + @Column({ type: 'datetime', nullable: false, name: 'contribution_date' }) + contributionDate: Date + + @Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' }) + memo: string + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + amount: Decimal + + @Column({ unsigned: true, nullable: true, name: 'moderator_id' }) + moderatorId: number + + @Column({ unsigned: true, nullable: true, name: 'contribution_link_id' }) + contributionLinkId: number + + @Column({ unsigned: true, nullable: true, name: 'confirmed_by' }) + confirmedBy: number + + @Column({ nullable: true, name: 'confirmed_at' }) + confirmedAt: Date + + @Column({ unsigned: true, nullable: true, name: 'denied_by' }) + deniedBy: number + + @Column({ nullable: true, name: 'denied_at' }) + deniedAt: Date + + @Column({ + name: 'contribution_type', + length: 12, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + contributionType: string + + @Column({ + name: 'contribution_status', + length: 12, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + contributionStatus: string + + @Column({ unsigned: true, nullable: true, name: 'transaction_id' }) + transactionId: number + + @DeleteDateColumn({ name: 'deleted_at' }) + deletedAt: Date | null + + @DeleteDateColumn({ unsigned: true, nullable: true, name: 'deleted_by' }) + deletedBy: number + + @OneToMany(() => ContributionMessage, (message) => message.contribution) + @JoinColumn({ name: 'contribution_id' }) + messages?: ContributionMessage[] +} diff --git a/database/entity/Contribution.ts b/database/entity/Contribution.ts index f6530f00b..4b1dfc9c8 100644 --- a/database/entity/Contribution.ts +++ b/database/entity/Contribution.ts @@ -1 +1 @@ -export { Contribution } from './0047-messages_tables/Contribution' +export { Contribution } from './0051-add_delete_by_to_contributions/Contribution' diff --git a/database/migrations/0051-add_delete_by_to_contributions.ts b/database/migrations/0051-add_delete_by_to_contributions.ts new file mode 100644 index 000000000..21d0eda97 --- /dev/null +++ b/database/migrations/0051-add_delete_by_to_contributions.ts @@ -0,0 +1,12 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn( + `ALTER TABLE \`contributions\` ADD COLUMN \`deleted_by\` int(10) unsigned DEFAULT NULL;`, + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(`ALTER TABLE \`contributions\` DROP COLUMN \`deleted_by\`;`) +} diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index a9ad47911..a9aee274d 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -34,7 +34,7 @@ "contribution": { "activity": "Tätigkeit", "alert": { - "answerQuestion": "Bitte beantworte die Nachfrage", + "answerQuestion": "Bitte beantworte die Rückfrage!", "communityNoteList": "Hier findest du alle eingereichten und bestätigten Beiträge von allen Mitgliedern aus dieser Gemeinschaft.", "confirm": "bestätigt", "in_progress": "Es gibt eine Rückfrage der Moderatoren.", diff --git a/frontend/src/pages/Community.vue b/frontend/src/pages/Community.vue index c98bfff2d..786307405 100644 --- a/frontend/src/pages/Community.vue +++ b/frontend/src/pages/Community.vue @@ -231,8 +231,6 @@ export default { this.items = listContributions.contributionList if (this.items.find((item) => item.state === 'IN_PROGRESS')) { this.tabIndex = 1 - } else { - this.tabIndex = 0 } }) .catch((err) => {