diff --git a/backend/@types/klicktipp-api/index.d.ts b/backend/@types/klicktipp-api/index.d.ts new file mode 100644 index 000000000..e34bfc6c6 --- /dev/null +++ b/backend/@types/klicktipp-api/index.d.ts @@ -0,0 +1 @@ +declare module 'klicktipp-api' diff --git a/backend/jest.config.js b/backend/jest.config.js index 66a883b51..f2e2a556d 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -6,7 +6,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 81, + lines: 85, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/backend/package.json b/backend/package.json index 22e75f4af..19427091b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -68,6 +68,7 @@ "eslint-plugin-type-graphql": "^1.0.0", "faker": "^5.5.3", "jest": "^27.2.4", + "klicktipp-api": "^1.0.2", "nodemon": "^2.0.7", "prettier": "^2.3.1", "ts-jest": "^27.0.5", diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index fe9ad563a..a291bb945 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -1,8 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { KlicktippConnector } from './klicktippConnector' +import KlicktippConnector from 'klicktipp-api' import CONFIG from '@/config' const klicktippConnector = new KlicktippConnector() diff --git a/backend/src/apis/klicktippConnector.ts b/backend/src/apis/klicktippConnector.ts deleted file mode 100644 index 0ff741604..000000000 --- a/backend/src/apis/klicktippConnector.ts +++ /dev/null @@ -1,624 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import axios, { AxiosRequestConfig, Method } from 'axios' - -export class KlicktippConnector { - private baseURL: string - private sessionName: string - private sessionId: string - private error: string - - constructor(service?: string) { - this.baseURL = service !== undefined ? service : 'https://api.klicktipp.com' - this.sessionName = '' - this.sessionId = '' - } - - /** - * Get last error - * - * @return string an error description of the last error - */ - getLastError(): string { - const result = this.error - return result - } - - /** - * login - * - * @param username The login name of the user to login. - * @param password The password of the user. - * @return TRUE on success - */ - async login(username: string, password: string): Promise { - if (!(username.length > 0 && password.length > 0)) { - throw new Error('Klicktipp Login failed: Illegal Arguments') - } - - const res = await this.httpRequest('/account/login', 'POST', { username, password }, false) - - if (!res.isAxiosError) { - this.sessionId = res.data.sessid - this.sessionName = res.data.session_name - - return true - } - - throw new Error(`Klicktipp Login failed: ${res.response.statusText}`) - } - - /** - * Logs out the user currently logged in. - * - * @return TRUE on success - */ - async logout(): Promise { - const res = await this.httpRequest('/account/logout', 'POST') - - if (!res.isAxiosError) { - this.sessionId = '' - this.sessionName = '' - - return true - } - - throw new Error(`Klicktipp Logout failed: ${res.response.statusText}`) - } - - /** - * Get all subscription processes (lists) of the logged in user. Requires to be logged in. - * - * @return A associative obeject => - */ - async subscriptionProcessIndex(): Promise { - const res = await this.httpRequest('/list', 'GET', {}, true) - - if (!res.isAxiosError) { - return res.data - } - - throw new Error(`Klicktipp Subscription process index failed: ${res.response.statusText}`) - } - - /** - * Get subscription process (list) definition. Requires to be logged in. - * - * @param listid The id of the subscription process - * - * @return An object representing the Klicktipp subscription process. - */ - async subscriptionProcessGet(listid: string): Promise { - if (!listid || listid === '') { - throw new Error('Klicktipp Illegal Arguments') - } - - // retrieve - const res = await this.httpRequest(`/subscriber/${listid}`, 'GET', {}, true) - - if (!res.isAxiosError) { - return res.data - } - - throw new Error(`Klicktipp Subscription process get failed: ${res.response.statusText}`) - } - - /** - * Get subscription process (list) redirection url for given subscription. - * - * @param listid The id of the subscription process. - * @param email The email address of the subscriber. - * - * @return A redirection url as defined in the subscription process. - */ - async subscriptionProcessRedirect(listid: string, email: string): Promise { - if (!listid || listid === '' || !email || email === '') { - throw new Error('Klicktipp Illegal Arguments') - } - - // update - const data = { listid, email } - const res = await this.httpRequest('/list/redirect', 'POST', data) - - if (!res.isAxiosError) { - return res.data - } - - throw new Error( - `Klicktipp Subscription process get redirection url failed: ${res.response.statusText}`, - ) - } - - /** - * Get all manual tags of the logged in user. Requires to be logged in. - * - * @return A associative object => - */ - async tagIndex(): Promise { - const res = await this.httpRequest('/tag', 'GET', {}, true) - - if (!res.isAxiosError) { - return res.data - } - - throw new Error(`Klicktipp Tag index failed: ${res.response.statusText}`) - } - - /** - * Get a tag definition. Requires to be logged in. - * - * @param tagid The tag id. - * - * @return An object representing the Klicktipp tag object. - */ - async tagGet(tagid: string): Promise { - if (!tagid || tagid === '') { - throw new Error('Klicktipp Illegal Arguments') - } - const res = await this.httpRequest(`/tag/${tagid}`, 'GET', {}, true) - - if (!res.isAxiosError) { - return res.data - } - - throw new Error(`Klicktipp Tag get failed: ${res.response.statusText}`) - } - - /** - * Create a new manual tag. Requires to be logged in. - * - * @param name The name of the tag. - * @param text (optional) An additional description of the tag. - * - * @return The id of the newly created tag or false if failed. - */ - async tagCreate(name: string, text?: string): Promise { - if (!name || name === '') { - throw new Error('Klicktipp Illegal Arguments') - } - const data = { - name, - text: text !== undefined ? text : '', - } - const res = await this.httpRequest('/tag', 'POST', data, true) - - if (!res.isAxiosError) { - return res.data - } - - throw new Error(`Klicktipp Tag creation failed: ${res.response.statusText}`) - } - - /** - * Updates a tag. Requires to be logged in. - * - * @param tagid The tag id used to identify which tag to modify. - * @param name (optional) The new tag name. Set empty to leave it unchanged. - * @param text (optional) The new tag description. Set empty to leave it unchanged. - * - * @return TRUE on success - */ - async tagUpdate(tagid: string, name?: string, text?: string): Promise { - if (!tagid || tagid === '' || (name === '' && text === '')) { - throw new Error('Klicktipp Illegal Arguments') - } - const data = { - name: name !== undefined ? name : '', - text: text !== undefined ? text : '', - } - - const res = await this.httpRequest(`/tag/${tagid}`, 'PUT', data, true) - - if (!res.isAxiosError) { - return true - } - - throw new Error(`Klicktipp Tag update failed: ${res.response.statusText}`) - } - - /** - * Deletes a tag. Requires to be logged in. - * - * @param tagid The user id of the user to delete. - * - * @return TRUE on success - */ - async tagDelete(tagid: string): Promise { - if (!tagid || tagid === '') { - throw new Error('Klicktipp Illegal Arguments') - } - - const res = await this.httpRequest(`/tag/${tagid}`, 'DELETE') - - if (!res.isAxiosError) { - return true - } - - throw new Error(`Klicktipp Tag deletion failed: ${res.response.statusText}`) - } - - /** - * Get all contact fields of the logged in user. Requires to be logged in. - * - * @return A associative object => - */ - async fieldIndex(): Promise { - const res = await this.httpRequest('/field', 'GET', {}, true) - - if (!res.isAxiosError) { - return res.data - } - - throw new Error(`Klicktipp Field index failed: ${res.response.statusText}`) - } - - /** - * Subscribe an email. Requires to be logged in. - * - * @param email The email address of the subscriber. - * @param listid (optional) The id subscription process. - * @param tagid (optional) The id of the manual tag the subscriber will be tagged with. - * @param fields (optional) Additional fields of the subscriber. - * - * @return An object representing the Klicktipp subscriber object. - */ - async subscribe( - email: string, - listid?: number, - tagid?: number, - fields?: any, - smsnumber?: string, - ): Promise { - if ((!email || email === '') && smsnumber === '') { - throw new Error('Illegal Arguments') - } - // subscribe - const data = { - email, - fields: fields !== undefined ? fields : {}, - smsnumber: smsnumber !== undefined ? smsnumber : '', - listid: listid !== undefined ? listid : 0, - tagid: tagid !== undefined ? tagid : 0, - } - - const res = await this.httpRequest('/subscriber', 'POST', data, true) - - if (!res.isAxiosError) { - return res.data - } - throw new Error(`Klicktipp Subscription failed: ${res.response.statusText}`) - } - - /** - * Unsubscribe an email. Requires to be logged in. - * - * @param email The email address of the subscriber. - * - * @return TRUE on success - */ - async unsubscribe(email: string): Promise { - if (!email || email === '') { - throw new Error('Klicktipp Illegal Arguments') - } - - // unsubscribe - const data = { email } - - const res = await this.httpRequest('/subscriber/unsubscribe', 'POST', data, true) - - if (!res.isAxiosError) { - return true - } - throw new Error(`Klicktipp Unsubscription failed: ${res.response.statusText}`) - } - - /** - * Tag an email. Requires to be logged in. - * - * @param email The email address of the subscriber. - * @param tagids an array of the manual tag(s) the subscriber will be tagged with. - * - * @return TRUE on success - */ - async tag(email: string, tagids: string): Promise { - if (!email || email === '' || !tagids || tagids === '') { - throw new Error('Klicktipp Illegal Arguments') - } - - // tag - const data = { - email, - tagids, - } - - const res = await this.httpRequest('/subscriber/tag', 'POST', data, true) - - if (!res.isAxiosError) { - return res.data - } - throw new Error(`Klicktipp Tagging failed: ${res.response.statusText}`) - } - - /** - * Untag an email. Requires to be logged in. - * - * @param mixed $email The email address of the subscriber. - * @param mixed $tagid The id of the manual tag that will be removed from the subscriber. - * - * @return TRUE on success. - */ - async untag(email: string, tagid: string): Promise { - if (!email || email === '' || !tagid || tagid === '') { - throw new Error('Klicktipp Illegal Arguments') - } - - // subscribe - const data = { - email, - tagid, - } - - const res = await this.httpRequest('/subscriber/untag', 'POST', data, true) - - if (!res.isAxiosError) { - return true - } - throw new Error(`Klicktipp Untagging failed: ${res.response.statusText}`) - } - - /** - * Resend an autoresponder for an email address. Requires to be logged in. - * - * @param email A valid email address - * @param autoresponder An id of the autoresponder - * - * @return TRUE on success - */ - async resend(email: string, autoresponder: string): Promise { - if (!email || email === '' || !autoresponder || autoresponder === '') { - throw new Error('Klicktipp Illegal Arguments') - } - - // resend/reset autoresponder - const data = { email, autoresponder } - - const res = await this.httpRequest('/subscriber/resend', 'POST', data, true) - - if (!res.isAxiosError) { - return true - } - throw new Error(`Klicktipp Resend failed: ${res.response.statusText}`) - } - - /** - * Get all active subscribers. Requires to be logged in. - * - * @return An array of subscriber ids. - */ - async subscriberIndex(): Promise<[string]> { - const res = await this.httpRequest('/subscriber', 'GET', undefined, true) - - if (!res.isAxiosError) { - return res.data - } - throw new Error(`Klicktipp Subscriber index failed: ${res.response.statusText}`) - } - - /** - * Get subscriber information. Requires to be logged in. - * - * @param subscriberid The subscriber id. - * - * @return An object representing the Klicktipp subscriber. - */ - async subscriberGet(subscriberid: string): Promise { - if (!subscriberid || subscriberid === '') { - throw new Error('Klicktipp Illegal Arguments') - } - - // retrieve - const res = await this.httpRequest(`/subscriber/${subscriberid}`, 'GET', {}, true) - if (!res.isAxiosError) { - return res.data - } - throw new Error(`Klicktipp Subscriber get failed: ${res.response.statusText}`) - } - - /** - * Get a subscriber id by email. Requires to be logged in. - * - * @param email The email address of the subscriber. - * - * @return The id of the subscriber. Use subscriber_get to get subscriber details. - */ - async subscriberSearch(email: string): Promise { - if (!email || email === '') { - throw new Error('Klicktipp Illegal Arguments') - } - // search - const data = { email } - const res = await this.httpRequest('/subscriber/search', 'POST', data, true) - - if (!res.isAxiosError) { - return res.data - } - throw new Error(`Klicktipp Subscriber search failed: ${res.response.statusText}`) - } - - /** - * Get all active subscribers tagged with the given tag id. Requires to be logged in. - * - * @param tagid The id of the tag. - * - * @return An array with id -> subscription date of the tagged subscribers. Use subscriber_get to get subscriber details. - */ - async subscriberTagged(tagid: string): Promise { - if (!tagid || tagid === '') { - throw new Error('Klicktipp Illegal Arguments') - } - - // search - const data = { tagid } - const res = await this.httpRequest('/subscriber/tagged', 'POST', data, true) - - if (!res.isAxiosError) { - return res.data - } - throw new Error(`Klicktipp subscriber tagged failed: ${res.response.statusText}`) - } - - /** - * Updates a subscriber. Requires to be logged in. - * - * @param subscriberid The id of the subscriber to update. - * @param fields (optional) The fields of the subscriber to update - * @param newemail (optional) The new email of the subscriber to update - * - * @return TRUE on success - */ - async subscriberUpdate( - subscriberid: string, - fields?: any, - newemail?: string, - newsmsnumber?: string, - ): Promise { - if (!subscriberid || subscriberid === '') { - throw new Error('Klicktipp Illegal Arguments') - } - - // update - const data = { - fields: fields !== undefined ? fields : {}, - newemail: newemail !== undefined ? newemail : '', - newsmsnumber: newsmsnumber !== undefined ? newsmsnumber : '', - } - const res = await this.httpRequest(`/subscriber/${subscriberid}`, 'PUT', data, true) - if (!res.isAxiosError) { - return true - } - throw new Error(`Klicktipp Subscriber update failed: ${res.response.statusText}`) - } - - /** - * Delete a subscribe. Requires to be logged in. - * - * @param subscriberid The id of the subscriber to update. - * - * @return TRUE on success. - */ - async subscriberDelete(subscriberid: string): Promise { - if (!subscriberid || subscriberid === '') { - throw new Error('Klicktipp Illegal Arguments') - } - - // delete - const res = await this.httpRequest(`/subscriber/${subscriberid}`, 'DELETE', {}, true) - - if (!res.isAxiosError) { - return true - } - throw new Error(`Klicktipp Subscriber deletion failed: ${res.response.statusText}`) - } - - /** - * Subscribe an email. Requires an api key. - * - * @param apikey The api key (listbuildng configuration). - * @param email The email address of the subscriber. - * @param fields (optional) Additional fields of the subscriber. - * - * @return A redirection url as defined in the subscription process. - */ - async signin(apikey: string, email: string, fields?: any, smsnumber?: string): Promise { - if (!apikey || apikey === '' || ((!email || email === '') && smsnumber === '')) { - throw new Error('Klicktipp Illegal Arguments') - } - - // subscribe - const data = { - apikey, - email, - fields: fields !== undefined ? fields : {}, - smsnumber: smsnumber !== undefined ? smsnumber : '', - } - - const res = await this.httpRequest('/subscriber/signin', 'POST', data) - - if (!res.isAxiosError) { - return true - } - throw new Error(`Klicktipp Subscription failed: ${res.response.statusText}`) - } - - /** - * Untag an email. Requires an api key. - * - * @param apikey The api key (listbuildng configuration). - * @param email The email address of the subscriber. - * - * @return TRUE on success - */ - async signout(apikey: string, email: string): Promise { - if (!apikey || apikey === '' || !email || email === '') { - throw new Error('Klicktipp Illegal Arguments') - } - - // untag - const data = { apikey, email } - const res = await this.httpRequest('/subscriber/signout', 'POST', data) - - if (!res.isAxiosError) { - return true - } - throw new Error(`Klicktipp Untagging failed: ${res.response.statusText}`) - } - - /** - * Unsubscribe an email. Requires an api key. - * - * @param apikey The api key (listbuildng configuration). - * @param email The email address of the subscriber. - * - * @return TRUE on success - */ - async signoff(apikey: string, email: string): Promise { - if (!apikey || apikey === '' || !email || email === '') { - throw new Error('Klicktipp Illegal Arguments') - } - - // unsubscribe - const data = { apikey, email } - const res = await this.httpRequest('/subscriber/signoff', 'POST', data) - - if (!res.isAxiosError) { - return true - } - throw new Error(`Klicktipp Unsubscription failed: ${res.response.statusText}`) - } - - async httpRequest(path: string, method?: Method, data?: any, usesession?: boolean): Promise { - if (method === undefined) { - method = 'GET' - } - const options: AxiosRequestConfig = { - baseURL: this.baseURL, - method, - url: path, - data, - headers: { - 'Content-Type': 'application/json', - Content: 'application/json', - Cookie: - usesession && this.sessionName !== '' ? `${this.sessionName}=${this.sessionId}` : '', - }, - } - - return axios(options) - .then((res) => res) - .catch((error) => error) - } -} diff --git a/backend/src/event/EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE.ts b/backend/src/event/EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE.ts new file mode 100644 index 000000000..f07d38e98 --- /dev/null +++ b/backend/src/event/EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE.ts @@ -0,0 +1,23 @@ +import { User as DbUser } from '@entity/User' +import { Contribution as DbContribution } from '@entity/Contribution' +import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage' +import { Event as DbEvent } from '@entity/Event' +import { Event, EventType } from './Event' + +export const EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE = async ( + user: DbUser, + moderator: DbUser, + contribution: DbContribution, + contributionMessage: DbContributionMessage, +): Promise => + Event( + EventType.ADMIN_CONTRIBUTION_MESSAGE_CREATE, + user, + moderator, + null, + null, + contribution, + contributionMessage, + null, + null, + ).save() diff --git a/backend/src/event/EVENT_CONTRIBUTION_MESSAGE_CREATE.ts b/backend/src/event/EVENT_CONTRIBUTION_MESSAGE_CREATE.ts new file mode 100644 index 000000000..b06685a6d --- /dev/null +++ b/backend/src/event/EVENT_CONTRIBUTION_MESSAGE_CREATE.ts @@ -0,0 +1,22 @@ +import { User as DbUser } from '@entity/User' +import { Contribution as DbContribution } from '@entity/Contribution' +import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage' +import { Event as DbEvent } from '@entity/Event' +import { Event, EventType } from './Event' + +export const EVENT_CONTRIBUTION_MESSAGE_CREATE = async ( + user: DbUser, + contribution: DbContribution, + contributionMessage: DbContributionMessage, +): Promise => + Event( + EventType.CONTRIBUTION_MESSAGE_CREATE, + user, + user, + null, + null, + contribution, + contributionMessage, + null, + null, + ).save() diff --git a/backend/src/event/Event.ts b/backend/src/event/Event.ts index 2e7cca6af..f1c4269c9 100644 --- a/backend/src/event/Event.ts +++ b/backend/src/event/Event.ts @@ -45,10 +45,12 @@ export { EVENT_ADMIN_CONTRIBUTION_UPDATE } from './EVENT_ADMIN_CONTRIBUTION_UPDA export { EVENT_ADMIN_CONTRIBUTION_LINK_CREATE } from './EVENT_ADMIN_CONTRIBUTION_LINK_CREATE' export { EVENT_ADMIN_CONTRIBUTION_LINK_DELETE } from './EVENT_ADMIN_CONTRIBUTION_LINK_DELETE' export { EVENT_ADMIN_CONTRIBUTION_LINK_UPDATE } from './EVENT_ADMIN_CONTRIBUTION_LINK_UPDATE' +export { EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE } from './EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE' export { EVENT_ADMIN_SEND_CONFIRMATION_EMAIL } from './EVENT_ADMIN_SEND_CONFIRMATION_EMAIL' export { EVENT_CONTRIBUTION_CREATE } from './EVENT_CONTRIBUTION_CREATE' export { EVENT_CONTRIBUTION_DELETE } from './EVENT_CONTRIBUTION_DELETE' export { EVENT_CONTRIBUTION_UPDATE } from './EVENT_CONTRIBUTION_UPDATE' +export { EVENT_CONTRIBUTION_MESSAGE_CREATE } from './EVENT_CONTRIBUTION_MESSAGE_CREATE' export { EVENT_LOGIN } from './EVENT_LOGIN' export { EVENT_REGISTER } from './EVENT_REGISTER' export { EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL } from './EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL' diff --git a/backend/src/event/EventType.ts b/backend/src/event/EventType.ts index b219a49ba..dda571b5a 100644 --- a/backend/src/event/EventType.ts +++ b/backend/src/event/EventType.ts @@ -9,10 +9,12 @@ export enum EventType { ADMIN_CONTRIBUTION_LINK_CREATE = 'ADMIN_CONTRIBUTION_LINK_CREATE', ADMIN_CONTRIBUTION_LINK_DELETE = 'ADMIN_CONTRIBUTION_LINK_DELETE', ADMIN_CONTRIBUTION_LINK_UPDATE = 'ADMIN_CONTRIBUTION_LINK_UPDATE', + ADMIN_CONTRIBUTION_MESSAGE_CREATE = 'ADMIN_CONTRIBUTION_MESSAGE_CREATE', ADMIN_SEND_CONFIRMATION_EMAIL = 'ADMIN_SEND_CONFIRMATION_EMAIL', CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE', CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE', CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE', + CONTRIBUTION_MESSAGE_CREATE = 'CONTRIBUTION_MESSAGE_CREATE', LOGIN = 'LOGIN', REGISTER = 'REGISTER', REDEEM_REGISTER = 'REDEEM_REGISTER', diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts index 8b5c5a0a7..3f10adae6 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts @@ -20,6 +20,8 @@ import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants' +import { EventType } from '@/event/Event' +import { Event as DbEvent } from '@entity/Event' jest.mock('@/emails/sendEmailVariants', () => { const originalModule = jest.requireActual('@/emails/sendEmailVariants') @@ -197,6 +199,18 @@ describe('ContributionMessageResolver', () => { contributionMemo: 'Test env contribution', }) }) + + it('stores the ADMIN_CONTRIBUTION_MESSAGE_CREATE event in the database', async () => { + await expect(DbEvent.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventType.ADMIN_CONTRIBUTION_MESSAGE_CREATE, + affectedUserId: expect.any(Number), + actingUserId: expect.any(Number), + involvedContributionId: result.data.createContribution.id, + involvedContributionMessageId: expect.any(Number), + }), + ) + }) }) }) }) @@ -322,6 +336,18 @@ describe('ContributionMessageResolver', () => { }), ) }) + + it('stores the CONTRIBUTION_MESSAGE_CREATE event in the database', async () => { + await expect(DbEvent.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventType.CONTRIBUTION_MESSAGE_CREATE, + affectedUserId: expect.any(Number), + actingUserId: expect.any(Number), + involvedContributionId: result.data.createContribution.id, + involvedContributionMessageId: expect.any(Number), + }), + ) + }) }) }) }) diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.ts b/backend/src/graphql/resolver/ContributionMessageResolver.ts index e9258490e..999ccc2b1 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.ts @@ -4,7 +4,8 @@ import { getConnection } from '@dbTools/typeorm' import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage' import { Contribution as DbContribution } from '@entity/Contribution' -import { UserContact } from '@entity/UserContact' +import { UserContact as DbUserContact } from '@entity/UserContact' +import { User as DbUser } from '@entity/User' import { ContributionMessage, ContributionMessageListResult } from '@model/ContributionMessage' import ContributionMessageArgs from '@arg/ContributionMessageArgs' @@ -17,6 +18,10 @@ import { RIGHTS } from '@/auth/RIGHTS' import { Context, getUser } from '@/server/context' import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants' import LogError from '@/server/LogError' +import { + EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE, + EVENT_CONTRIBUTION_MESSAGE_CREATE, +} from '@/event/Event' @Resolver() export class ContributionMessageResolver { @@ -57,6 +62,11 @@ export class ContributionMessageResolver { await queryRunner.manager.update(DbContribution, { id: contributionId }, contribution) } await queryRunner.commitTransaction() + await EVENT_CONTRIBUTION_MESSAGE_CREATE( + user, + { id: contributionMessage.contributionId } as DbContribution, + contributionMessage, + ) } catch (e) { await queryRunner.rollbackTransaction() throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e) @@ -98,7 +108,7 @@ export class ContributionMessageResolver { @Args() { contributionId, message }: ContributionMessageArgs, @Ctx() context: Context, ): Promise { - const user = getUser(context) + const moderator = getUser(context) const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() @@ -112,18 +122,18 @@ export class ContributionMessageResolver { if (!contribution) { throw new LogError('Contribution not found', contributionId) } - if (contribution.userId === user.id) { + if (contribution.userId === moderator.id) { throw new LogError('Admin can not answer on his own contribution', contributionId) } if (!contribution.user.emailContact) { - contribution.user.emailContact = await UserContact.findOneOrFail({ + contribution.user.emailContact = await DbUserContact.findOneOrFail({ where: { id: contribution.user.emailId }, }) } contributionMessage.contributionId = contributionId contributionMessage.createdAt = new Date() contributionMessage.message = message - contributionMessage.userId = user.id + contributionMessage.userId = moderator.id contributionMessage.type = ContributionMessageType.DIALOG contributionMessage.isModerator = true await queryRunner.manager.insert(DbContributionMessage, contributionMessage) @@ -142,17 +152,23 @@ export class ContributionMessageResolver { lastName: contribution.user.lastName, email: contribution.user.emailContact.email, language: contribution.user.language, - senderFirstName: user.firstName, - senderLastName: user.lastName, + senderFirstName: moderator.firstName, + senderLastName: moderator.lastName, contributionMemo: contribution.memo, }) await queryRunner.commitTransaction() + await EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE( + { id: contribution.userId } as DbUser, + moderator, + contribution, + contributionMessage, + ) } catch (e) { await queryRunner.rollbackTransaction() throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e) } finally { await queryRunner.release() } - return new ContributionMessage(contributionMessage, user) + return new ContributionMessage(contributionMessage, moderator) } } diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index d4f059832..ceb062e8c 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -2545,7 +2545,7 @@ describe('ContributionResolver', () => { ) }) - it('stores the CONTRIBUTION_CONFIRM event in the database', async () => { + it('stores the ADMIN_CONTRIBUTION_CONFIRM event in the database', async () => { await expect(DbEvent.find()).resolves.toContainEqual( expect.objectContaining({ type: EventType.ADMIN_CONTRIBUTION_CONFIRM, diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 55d90dab6..1cc097152 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -90,7 +90,6 @@ export class ContributionResolver { logger.trace('contribution to save', contribution) await DbContribution.save(contribution) - await EVENT_CONTRIBUTION_CREATE(user, contribution, amount) return new UnconfirmedContribution(contribution, user, creations) @@ -118,7 +117,6 @@ export class ContributionResolver { contribution.deletedBy = user.id contribution.deletedAt = new Date() await contribution.save() - await EVENT_CONTRIBUTION_DELETE(user, contribution, contribution.amount) const res = await contribution.softRemove() @@ -299,11 +297,8 @@ export class ContributionResolver { contribution.moderatorId = moderator.id contribution.contributionType = ContributionType.ADMIN contribution.contributionStatus = ContributionStatus.PENDING - logger.trace('contribution to save', contribution) - await DbContribution.save(contribution) - await EVENT_ADMIN_CONTRIBUTION_CREATE(emailContact.user, moderator, contribution, amount) return getUserCreation(emailContact.userId, clientTimezoneOffset) @@ -369,9 +364,7 @@ export class ContributionResolver { result.amount = amount result.memo = contributionToUpdate.memo result.date = contributionToUpdate.contributionDate - result.creation = await getUserCreation(emailContact.user.id, clientTimezoneOffset) - await EVENT_ADMIN_CONTRIBUTION_UPDATE( emailContact.user, moderator, @@ -436,7 +429,6 @@ export class ContributionResolver { contribution.deletedBy = moderator.id await contribution.save() const res = await contribution.softRemove() - await EVENT_ADMIN_CONTRIBUTION_DELETE( { id: contribution.userId } as DbUser, moderator, @@ -554,7 +546,6 @@ export class ContributionResolver { } finally { await queryRunner.release() } - await EVENT_ADMIN_CONTRIBUTION_CONFIRM(user, moderatorUser, contribution, contribution.amount) } finally { releaseLock() @@ -613,7 +604,6 @@ export class ContributionResolver { contributionToUpdate.deniedBy = moderator.id contributionToUpdate.deniedAt = new Date() const res = await contributionToUpdate.save() - await EVENT_ADMIN_CONTRIBUTION_DENY( user, moderator, diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 52241a0a6..146cd41e3 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -59,7 +59,7 @@ "@entity/*": ["../database/entity/*", "../../database/build/entity/*"] }, // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - "typeRoots": ["src/federation/@types", "node_modules/@types"], /* List of folders to include type definitions from. */ + "typeRoots": ["@types", "node_modules/@types"], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ diff --git a/backend/yarn.lock b/backend/yarn.lock index 3151557ab..e169a9ce7 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -4526,6 +4526,13 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +klicktipp-api@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/klicktipp-api/-/klicktipp-api-1.0.2.tgz#a7ba728887c4d9a1c257fa30b78cbe0be92a20ab" + integrity sha512-aQQpuznC0O2W7Oq2BxKDnuLAnGmKTMfudOQ0TAEf0TLv82KH2AsCXl0nbutJ2g1i3MH+sCyGE/r/nwnUhr4QeA== + dependencies: + axios "^0.21.1" + latest-version@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face"