diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da8521a76..7819a0703 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -360,6 +360,25 @@ jobs: - name: backend | Lint run: docker run --rm gradido/backend:test yarn run lint + ############################################################################## + # JOB: LOCALES BACKEND ####################################################### + ############################################################################## + locales_backend: + name: Locales - Backend + runs-on: ubuntu-latest + needs: [build_test_backend] + steps: + ########################################################################## + # CHECKOUT CODE ########################################################## + ########################################################################## + - name: Checkout code + uses: actions/checkout@v3 + ########################################################################## + # LOCALES BACKEND ##################################################### + ########################################################################## + - name: Backend | Locales + run: cd backend && yarn && yarn locales + ############################################################################## # JOB: LINT DATABASE UP ###################################################### ############################################################################## @@ -526,7 +545,7 @@ jobs: report_name: Coverage Backend type: lcov result_path: ./backend/coverage/lcov.info - min_coverage: 78 + min_coverage: 80 token: ${{ github.token }} ########################################################################## diff --git a/CHANGELOG.md b/CHANGELOG.md index 358e4670a..4bfc66e39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,57 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [1.18.2](https://github.com/gradido/gradido/compare/1.18.1...1.18.2) + +- fix(admin): deny contribution button to left [`#2699`](https://github.com/gradido/gradido/pull/2699) + +#### [1.18.1](https://github.com/gradido/gradido/compare/1.18.0...1.18.1) + +> 10 February 2023 + +- chore(release): version 1.18.1 [`#2698`](https://github.com/gradido/gradido/pull/2698) +- fix(frontend): fix is last month for empty form date [`#2697`](https://github.com/gradido/gradido/pull/2697) +- fix(frontend): community link [`#2696`](https://github.com/gradido/gradido/pull/2696) + +#### [1.18.0](https://github.com/gradido/gradido/compare/1.17.1...1.18.0) + +> 9 February 2023 + +- feat(release): version 1.18.0 [`#2690`](https://github.com/gradido/gradido/pull/2690) +- refactor(frontend): toast by automatically logged out [`#2681`](https://github.com/gradido/gradido/pull/2681) +- refactor(frontend): change text for gdd_per_link.choose-amount [`#2638`](https://github.com/gradido/gradido/pull/2638) +- fix(backend): emails for deny and delete contribution [`#2688`](https://github.com/gradido/gradido/pull/2688) +- refactor(other): remove config version from `.env.dist` [`#2686`](https://github.com/gradido/gradido/pull/2686) +- refactor(backend): use LogError on contributionMessageResolver [`#2663`](https://github.com/gradido/gradido/pull/2663) +- refactor(backend): get last transaction by only one function [`#2668`](https://github.com/gradido/gradido/pull/2668) +- refactor(other): don't rebuild modul if unit test file has been changed [`#2667`](https://github.com/gradido/gradido/pull/2667) +- refactor(backend): use LogError on contributionLinkResolver [`#2662`](https://github.com/gradido/gradido/pull/2662) +- refactor(backend): remove event protocol config switch [`#2670`](https://github.com/gradido/gradido/pull/2670) +- refactor(backend): event protocol [`#2652`](https://github.com/gradido/gradido/pull/2652) +- refactor(frontend): sidebar becomes smaller when critical phase [`#2649`](https://github.com/gradido/gradido/pull/2649) +- refactor(backend): use LogError on sendEMailTranslated [`#2656`](https://github.com/gradido/gradido/pull/2656) +- refactor(backend): log error class [`#2640`](https://github.com/gradido/gradido/pull/2640) +- feat(backend): add filterState parameter to listAllContributions query [`#2619`](https://github.com/gradido/gradido/pull/2619) +- refactor(frontend): there is no message when a month is fully created [`#2626`](https://github.com/gradido/gradido/pull/2626) +- refactor(frontend): better text alignment on send via link [`#2637`](https://github.com/gradido/gradido/pull/2637) +- refactor(frontend): text changed as indicated in the issues [`#2642`](https://github.com/gradido/gradido/pull/2642) +- refactor(frontend): when you click on create, you will be directed to the form [`#2645`](https://github.com/gradido/gradido/pull/2645) +- feat(backend): federation: separated dht-hub features in new dht-node modul [`#2510`](https://github.com/gradido/gradido/pull/2510) +- refactor(backend): refine assembly of error message in user resolver [`#2636`](https://github.com/gradido/gradido/pull/2636) +- fix(backend): unit tests creations for 31st day [`#2641`](https://github.com/gradido/gradido/pull/2641) +- fix(workflow): properly lint pr - prevent requirement to restart linting [`#2635`](https://github.com/gradido/gradido/pull/2635) +- feat(frontend): 'yes'-button shows which dialog is currently open with a different color [`#2629`](https://github.com/gradido/gradido/pull/2629) +- feat(backend): federation implement multiple apollo graphql endpoints [`#2459`](https://github.com/gradido/gradido/pull/2459) +- refactor(frontend): add legend to all contribution tab, and add tests. [`#2625`](https://github.com/gradido/gradido/pull/2625) +- feat(frontend): unit tests community page [`#2587`](https://github.com/gradido/gradido/pull/2587) +- feat(backend): deny contributions [`#2461`](https://github.com/gradido/gradido/pull/2461) +- refactor(admin): update yarn.lock file of admin. [`#2579`](https://github.com/gradido/gradido/pull/2579) + #### [1.17.1](https://github.com/gradido/gradido/compare/1.17.0...1.17.1) +> 20 January 2023 + +- chore(release): v1.17.1 [`#2588`](https://github.com/gradido/gradido/pull/2588) - refactor(frontend): change contribution memo add word-break [`#2583`](https://github.com/gradido/gradido/pull/2583) - refactor(admin): add text-break on all table memo fields [`#2584`](https://github.com/gradido/gradido/pull/2584) - fix(frontend): throw proper frontend warning errors [`#2586`](https://github.com/gradido/gradido/pull/2586) diff --git a/admin/.env.dist b/admin/.env.dist index d7044669a..66c84dda8 100644 --- a/admin/.env.dist +++ b/admin/.env.dist @@ -1,5 +1,3 @@ -CONFIG_VERSION=v1.2022-03-18 - GRAPHQL_URI=http://localhost:4000/graphql WALLET_AUTH_URL=http://localhost/authenticate?token={token} WALLET_URL=http://localhost/login diff --git a/admin/package.json b/admin/package.json index 8270c4da6..941a9bf69 100644 --- a/admin/package.json +++ b/admin/package.json @@ -3,7 +3,7 @@ "description": "Administraion Interface for Gradido", "main": "index.js", "author": "Moriz Wahl", - "version": "1.17.1", + "version": "1.18.2", "license": "Apache-2.0", "private": false, "scripts": { @@ -86,5 +86,10 @@ "> 1%", "last 2 versions", "not ie <= 10" - ] + ], + "nodemonConfig": { + "ignore": [ + "**/*.spec.js" + ] + } } diff --git a/admin/src/components/ContributionLink/ContributionLink.spec.js b/admin/src/components/ContributionLink/ContributionLink.spec.js index b72a0347c..e0f09f9fd 100644 --- a/admin/src/components/ContributionLink/ContributionLink.spec.js +++ b/admin/src/components/ContributionLink/ContributionLink.spec.js @@ -42,14 +42,30 @@ describe('ContributionLink', () => { expect(wrapper.find('div.contribution-link').exists()).toBe(true) }) - it('emits toggle::collapse new Contribution', async () => { - wrapper.vm.editContributionLinkData() - expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy() + describe('function editContributionLinkData', () => { + beforeEach(() => { + wrapper.vm.editContributionLinkData() + }) + it('emits toggle::collapse new Contribution', async () => { + await expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy() + }) }) - it('emits toggle::collapse close Contribution-Form ', async () => { - wrapper.vm.closeContributionForm() - expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy() + describe('function closeContributionForm', () => { + beforeEach(async () => { + await wrapper.setData({ visible: true }) + wrapper.vm.closeContributionForm() + }) + + it('emits toggle::collapse close Contribution-Form ', async () => { + await expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy() + }) + it('editContributionLink is false', async () => { + await expect(wrapper.vm.editContributionLink).toBe(false) + }) + it('contributionLinkData is empty', async () => { + await expect(wrapper.vm.contributionLinkData).toEqual({}) + }) }) }) }) diff --git a/admin/src/components/CreationTransactionList.spec.js b/admin/src/components/CreationTransactionList.spec.js index ff9607424..9613942f8 100644 --- a/admin/src/components/CreationTransactionList.spec.js +++ b/admin/src/components/CreationTransactionList.spec.js @@ -88,5 +88,16 @@ describe('CreationTransactionList', () => { expect(toastErrorSpy).toBeCalledWith('OUCH!') }) }) + + describe('watch currentPage', () => { + beforeEach(async () => { + jest.clearAllMocks() + await wrapper.setData({ currentPage: 2 }) + }) + + it('returns the string in normal order if reversed property is not true', () => { + expect(wrapper.vm.currentPage).toBe(2) + }) + }) }) }) diff --git a/admin/src/components/NavBar.spec.js b/admin/src/components/NavBar.spec.js index 139172c30..96b7cba9c 100644 --- a/admin/src/components/NavBar.spec.js +++ b/admin/src/components/NavBar.spec.js @@ -46,39 +46,31 @@ describe('NavBar', () => { }) describe('Navbar Menu', () => { - it('has a link to overview', () => { - expect(wrapper.findAll('.nav-item').at(0).find('a').attributes('href')).toBe('/') - }) - it('has a link to /user', () => { - expect(wrapper.findAll('.nav-item').at(1).find('a').attributes('href')).toBe('/user') - }) - - it('has a link to /creation', () => { - expect(wrapper.findAll('.nav-item').at(2).find('a').attributes('href')).toBe('/creation') + expect(wrapper.findAll('.nav-item').at(0).find('a').attributes('href')).toBe('/user') }) it('has a link to /creation-confirm', () => { - expect(wrapper.findAll('.nav-item').at(3).find('a').attributes('href')).toBe( + expect(wrapper.findAll('.nav-item').at(1).find('a').attributes('href')).toBe( '/creation-confirm', ) }) it('has a link to /contribution-links', () => { - expect(wrapper.findAll('.nav-item').at(4).find('a').attributes('href')).toBe( + expect(wrapper.findAll('.nav-item').at(2).find('a').attributes('href')).toBe( '/contribution-links', ) }) it('has a link to /statistic', () => { - expect(wrapper.findAll('.nav-item').at(5).find('a').attributes('href')).toBe('/statistic') + expect(wrapper.findAll('.nav-item').at(3).find('a').attributes('href')).toBe('/statistic') }) }) describe('wallet', () => { const assignLocationSpy = jest.fn() beforeEach(async () => { - await wrapper.findAll('.nav-item').at(6).find('a').trigger('click') + await wrapper.findAll('.nav-item').at(5).find('a').trigger('click') }) it.skip('changes window location to wallet', () => { @@ -97,7 +89,7 @@ describe('NavBar', () => { window.location = { assign: windowLocationMock, } - await wrapper.findAll('.nav-item').at(7).find('a').trigger('click') + await wrapper.findAll('.nav-item').at(5).find('a').trigger('click') }) it('redirects to /logout', () => { diff --git a/admin/src/components/NavBar.vue b/admin/src/components/NavBar.vue index 6bed8e6e4..f8bc2b280 100644 --- a/admin/src/components/NavBar.vue +++ b/admin/src/components/NavBar.vue @@ -9,9 +9,7 @@ - {{ $t('navbar.overview') }} {{ $t('navbar.user_search') }} - {{ $t('navbar.multi_creation') }} -
- - - -
- - diff --git a/admin/src/components/Tables/SelectedUsersTable.vue b/admin/src/components/Tables/SelectedUsersTable.vue deleted file mode 100644 index 810f8dac8..000000000 --- a/admin/src/components/Tables/SelectedUsersTable.vue +++ /dev/null @@ -1,26 +0,0 @@ - - diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json index 9612e3247..4f4a0c5bc 100644 --- a/admin/src/locales/de.json +++ b/admin/src/locales/de.json @@ -32,7 +32,6 @@ "creation": "Schöpfung", "creationList": "Schöpfungsliste", "creation_form": { - "creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.", "creation_for": "Aktives Grundeinkommen für", "enter_text": "Text eintragen", "form": "Schöpfungsformular", @@ -87,7 +86,6 @@ "lastname": "Nachname", "math": { "equals": "=", - "exclaim": "!", "pipe": "|", "plus": "+" }, @@ -95,15 +93,12 @@ "request": "Die Anfrage wurde gesendet." }, "moderator": "Moderator", - "multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.", "name": "Name", "navbar": { "automaticContributions": "Automatische Beiträge", "logout": "Abmelden", - "multi_creation": "Mehrfachschöpfung", "my-account": "Mein Konto", "open_creation": "Offene Schöpfungen", - "overview": "Übersicht", "statistic": "Statistik", "user_search": "Nutzersuche" }, @@ -132,9 +127,7 @@ } }, "redeemed": "eingelöst", - "remove": "Entfernen", "removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.", - "remove_all": "alle Nutzer entfernen", "save": "Speichern", "statistic": { "activeUsers": "Aktive Mitglieder", diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json index f9598d006..566273415 100644 --- a/admin/src/locales/en.json +++ b/admin/src/locales/en.json @@ -32,7 +32,6 @@ "creation": "Creation", "creationList": "Creation list", "creation_form": { - "creation_failed": "Could not create pending creation for {email}", "creation_for": "Active Basic Income for", "enter_text": "Enter text", "form": "Creation form", @@ -87,7 +86,6 @@ "lastname": "Lastname", "math": { "equals": "=", - "exclaim": "!", "pipe": "|", "plus": "+" }, @@ -95,15 +93,12 @@ "request": "Request has been sent." }, "moderator": "Moderator", - "multiple_creation_text": "Please select one or more members for which you would like to perform creations.", "name": "Name", "navbar": { "automaticContributions": "Automatic Contributions", "logout": "Logout", - "multi_creation": "Multiple creation", "my-account": "My Account", "open_creation": "Open creations", - "overview": "Overview", "statistic": "Statistic", "user_search": "User search" }, @@ -132,9 +127,7 @@ } }, "redeemed": "redeemed", - "remove": "Remove", "removeNotSelf": "As an admin/moderator, you cannot delete yourself.", - "remove_all": "Remove all users", "save": "Speichern", "statistic": { "activeUsers": "Active members", diff --git a/admin/src/locales/index.test.js b/admin/src/locales/index.test.js new file mode 100644 index 000000000..1abcadbec --- /dev/null +++ b/admin/src/locales/index.test.js @@ -0,0 +1,18 @@ +import locales from './index.js' + +describe('locales', () => { + it('should contain 2 locales', () => { + expect(locales).toHaveLength(2) + }) + + it('should contain a German locale', () => { + expect(locales).toContainEqual( + expect.objectContaining({ + name: 'Deutsch', + code: 'de', + iso: 'de-DE', + enabled: true, + }), + ) + }) +}) diff --git a/admin/src/pages/ContributionLinks.spec.js b/admin/src/pages/ContributionLinks.spec.js index fb60a99cf..3d91fdbe9 100644 --- a/admin/src/pages/ContributionLinks.spec.js +++ b/admin/src/pages/ContributionLinks.spec.js @@ -1,6 +1,7 @@ import { mount } from '@vue/test-utils' import ContributionLinks from './ContributionLinks.vue' import { listContributionLinks } from '@/graphql/listContributionLinks.js' +import { toastErrorSpy } from '../../test/testSetup' const localVue = global.localVue @@ -46,13 +47,31 @@ describe('ContributionLinks', () => { beforeEach(() => { wrapper = Wrapper() }) + describe('apollo returns', () => { + it('calls listContributionLinks', () => { + expect(apolloQueryMock).toBeCalledWith( + expect.objectContaining({ + query: listContributionLinks, + }), + ) + }) + }) - it('calls listContributionLinks', () => { - expect(apolloQueryMock).toBeCalledWith( - expect.objectContaining({ - query: listContributionLinks, - }), - ) + describe.skip('query transaction with error', () => { + beforeEach(() => { + apolloQueryMock.mockRejectedValue({ message: 'OUCH!' }) + wrapper = Wrapper() + }) + + it('calls the API', () => { + expect(apolloQueryMock).toBeCalled() + }) + + it('toast error', () => { + expect(toastErrorSpy).toBeCalledWith( + 'listContributionLinks has no result, use default data', + ) + }) }) }) }) diff --git a/admin/src/pages/Creation.spec.js b/admin/src/pages/Creation.spec.js deleted file mode 100644 index 9524fc5d6..000000000 --- a/admin/src/pages/Creation.spec.js +++ /dev/null @@ -1,337 +0,0 @@ -import { mount } from '@vue/test-utils' -import Creation from './Creation.vue' -import { toastErrorSpy } from '../../test/testSetup' - -const localVue = global.localVue - -const apolloQueryMock = jest.fn().mockResolvedValue({ - data: { - searchUsers: { - userCount: 2, - userList: [ - { - userId: 1, - firstName: 'Bibi', - lastName: 'Bloxberg', - email: 'bibi@bloxberg.de', - creation: [200, 400, 600], - emailChecked: true, - }, - { - userId: 2, - firstName: 'Benjamin', - lastName: 'Blümchen', - email: 'benjamin@bluemchen.de', - creation: [800, 600, 400], - emailChecked: true, - }, - ], - }, - }, -}) - -const storeCommitMock = jest.fn() - -const mocks = { - $t: jest.fn((t, options) => (options ? [t, options] : t)), - $d: jest.fn((d) => d), - $apollo: { - query: apolloQueryMock, - }, - $store: { - commit: storeCommitMock, - state: { - userSelectedInMassCreation: [], - }, - }, -} - -describe('Creation', () => { - let wrapper - - const Wrapper = () => { - return mount(Creation, { localVue, mocks }) - } - - describe('mount', () => { - beforeEach(() => { - jest.clearAllMocks() - wrapper = Wrapper() - }) - - it('has a DIV element with the class.creation', () => { - expect(wrapper.find('div.creation').exists()).toBeTruthy() - }) - - describe('apollo returns user array', () => { - it('calls the searchUser query', () => { - expect(apolloQueryMock).toBeCalledWith( - expect.objectContaining({ - variables: { - searchText: '', - currentPage: 1, - pageSize: 25, - filters: { - byActivated: true, - byDeleted: false, - }, - }, - }), - ) - }) - - it('has two rows in the left table', () => { - expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(2) - }) - - it('has nwo rows in the right table', () => { - expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0) - }) - - it('has correct data in first row ', () => { - expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain('Bibi') - expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain( - 'Bloxberg', - ) - expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain( - '200 | 400 | 600', - ) - expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain( - 'bibi@bloxberg.de', - ) - }) - - it('has correct data in second row ', () => { - expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain( - 'Benjamin', - ) - expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain( - 'Blümchen', - ) - expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain( - '800 | 600 | 400', - ) - expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain( - 'benjamin@bluemchen.de', - ) - }) - }) - - describe('push item', () => { - beforeEach(() => { - wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).find('button').trigger('click') - }) - - it('has one item in left table', () => { - expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(1) - }) - - it('has one item in right table', () => { - expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(1) - }) - - it('has the correct user in left table', () => { - expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain( - 'bibi@bloxberg.de', - ) - }) - - it('has the correct user in right table', () => { - expect(wrapper.findAll('table').at(1).findAll('tbody > tr').at(0).text()).toContain( - 'benjamin@bluemchen.de', - ) - }) - - it('updates userSelectedInMassCreation in store', () => { - expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [ - { - userId: 2, - firstName: 'Benjamin', - lastName: 'Blümchen', - email: 'benjamin@bluemchen.de', - creation: [800, 600, 400], - showDetails: false, - emailChecked: true, - }, - ]) - }) - - describe('remove item', () => { - beforeEach(async () => { - await wrapper - .findAll('table') - .at(1) - .findAll('tbody > tr') - .at(0) - .find('button') - .trigger('click') - }) - - it('has two items in left table', () => { - expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(2) - }) - - it('has the removed user in first row', () => { - expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain( - 'benjamin@bluemchen.de', - ) - }) - - it('has no items in right table', () => { - expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0) - }) - - it('commits empty array as userSelectedInMassCreation', () => { - expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', []) - }) - }) - - describe('remove all bookmarks', () => { - beforeEach(async () => { - jest.clearAllMocks() - await wrapper.find('button.btn-light').trigger('click') - }) - - it('has no items in right table', () => { - expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0) - }) - - it('commits empty array to userSelectedInMassCreation', () => { - expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', []) - }) - - it('calls searchUsers', () => { - expect(apolloQueryMock).toBeCalled() - }) - }) - }) - - describe('store has items in userSelectedInMassCreation', () => { - beforeEach(() => { - mocks.$store.state.userSelectedInMassCreation = [ - { - userId: 2, - firstName: 'Benjamin', - lastName: 'Blümchen', - email: 'benjamin@bluemchen.de', - creation: [800, 600, 400], - showDetails: false, - emailChecked: true, - }, - ] - wrapper = Wrapper() - }) - - it('has one item in left table', () => { - expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(1) - }) - - it('has one item in right table', () => { - expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(1) - }) - - it('has the stored user in second row', () => { - expect(wrapper.findAll('table').at(1).findAll('tbody > tr').at(0).text()).toContain( - 'benjamin@bluemchen.de', - ) - }) - }) - - describe('failed creations', () => { - beforeEach(async () => { - await wrapper - .findComponent({ name: 'CreationFormular' }) - .vm.$emit('toast-failed-creations', ['bibi@bloxberg.de', 'benjamin@bluemchen.de']) - }) - - it('toasts two error messages', () => { - expect(toastErrorSpy).toBeCalledWith([ - 'creation_form.creation_failed', - { email: 'bibi@bloxberg.de' }, - ]) - expect(toastErrorSpy).toBeCalledWith([ - 'creation_form.creation_failed', - { email: 'benjamin@bluemchen.de' }, - ]) - }) - }) - - describe('watchers', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - describe('search criteria', () => { - beforeEach(async () => { - await wrapper.setData({ criteria: 'XX' }) - }) - - it('calls API when criteria changes', async () => { - expect(apolloQueryMock).toBeCalledWith( - expect.objectContaining({ - variables: { - searchText: 'XX', - currentPage: 1, - pageSize: 25, - filters: { - byActivated: true, - byDeleted: false, - }, - }, - }), - ) - }) - - describe('reset search criteria', () => { - it('calls the API', async () => { - jest.clearAllMocks() - await wrapper.find('.test-click-clear-criteria').trigger('click') - expect(apolloQueryMock).toBeCalledWith( - expect.objectContaining({ - variables: { - searchText: '', - currentPage: 1, - pageSize: 25, - filters: { - byActivated: true, - byDeleted: false, - }, - }, - }), - ) - }) - }) - }) - - it('calls API when currentPage changes', async () => { - await wrapper.setData({ currentPage: 2 }) - expect(apolloQueryMock).toBeCalledWith( - expect.objectContaining({ - variables: { - searchText: '', - currentPage: 2, - pageSize: 25, - filters: { - byActivated: true, - byDeleted: false, - }, - }, - }), - ) - }) - }) - - describe('apollo returns error', () => { - beforeEach(() => { - apolloQueryMock.mockRejectedValue({ - message: 'Ouch', - }) - wrapper = Wrapper() - }) - - it('toasts an error message', () => { - expect(toastErrorSpy).toBeCalledWith('Ouch') - }) - }) - }) -}) diff --git a/admin/src/pages/Creation.vue b/admin/src/pages/Creation.vue deleted file mode 100644 index 26d44fd3e..000000000 --- a/admin/src/pages/Creation.vue +++ /dev/null @@ -1,200 +0,0 @@ - - diff --git a/admin/src/pages/CreationConfirm.spec.js b/admin/src/pages/CreationConfirm.spec.js index d47233ded..99dbda219 100644 --- a/admin/src/pages/CreationConfirm.spec.js +++ b/admin/src/pages/CreationConfirm.spec.js @@ -259,7 +259,7 @@ describe('CreationConfirm', () => { describe('deny creation', () => { beforeEach(async () => { - await wrapper.findAll('tr').at(1).findAll('button').at(2).trigger('click') + await wrapper.findAll('tr').at(1).findAll('button').at(1).trigger('click') }) it('opens the overlay', () => { diff --git a/admin/src/pages/CreationConfirm.vue b/admin/src/pages/CreationConfirm.vue index e87dfc247..c6576e5ba 100644 --- a/admin/src/pages/CreationConfirm.vue +++ b/admin/src/pages/CreationConfirm.vue @@ -129,6 +129,7 @@ export default { fields() { return [ { key: 'bookmark', label: this.$t('delete') }, + { key: 'deny', label: this.$t('deny') }, { key: 'email', label: this.$t('e_mail') }, { key: 'firstName', label: this.$t('firstname') }, { key: 'lastName', label: this.$t('lastname') }, @@ -149,7 +150,6 @@ export default { }, { key: 'moderator', label: this.$t('moderator') }, { key: 'editCreation', label: this.$t('edit') }, - { key: 'deny', label: this.$t('deny') }, { key: 'confirm', label: this.$t('save') }, ] }, diff --git a/admin/src/router/router.test.js b/admin/src/router/router.test.js index fdc4b0b83..ad1ad1245 100644 --- a/admin/src/router/router.test.js +++ b/admin/src/router/router.test.js @@ -45,7 +45,7 @@ describe('router', () => { describe('routes', () => { it('has nine routes defined', () => { - expect(routes).toHaveLength(9) + expect(routes).toHaveLength(8) }) it('has "/overview" as default', async () => { @@ -67,13 +67,6 @@ describe('router', () => { }) }) - describe('creation', () => { - it('loads the "Creation" component', async () => { - const component = await routes.find((r) => r.path === '/creation').component() - expect(component.default.name).toBe('Creation') - }) - }) - describe('creation-confirm', () => { it('loads the "CreationConfirm" component', async () => { const component = await routes.find((r) => r.path === '/creation-confirm').component() diff --git a/admin/src/router/routes.js b/admin/src/router/routes.js index e365a6e40..b01466cfc 100644 --- a/admin/src/router/routes.js +++ b/admin/src/router/routes.js @@ -19,10 +19,6 @@ const routes = [ path: '/user', component: () => import('@/pages/UserSearch.vue'), }, - { - path: '/creation', - component: () => import('@/pages/Creation.vue'), - }, { path: '/creation-confirm', component: () => import('@/pages/CreationConfirm.vue'), diff --git a/backend/.env.dist b/backend/.env.dist index ba5b89fa9..8cf89ff0c 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -1,5 +1,3 @@ -CONFIG_VERSION=v14.2022-12-22 - # Server PORT=4000 JWT_SECRET=secret123 @@ -55,9 +53,6 @@ EMAIL_CODE_REQUEST_TIME=10 # Webhook WEBHOOK_ELOPAGE_SECRET=secret -# EventProtocol -EVENT_PROTOCOL_DISABLED=false - # SET LOG LEVEL AS NEEDED IN YOUR .ENV # POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal # LOG_LEVEL=info diff --git a/backend/.env.template b/backend/.env.template index f73b87353..e75798325 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -54,9 +54,6 @@ EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME # Webhook WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET -# EventProtocol -EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED - # Federation FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED diff --git a/backend/package.json b/backend/package.json index bfcd61d5b..497c4d82d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "gradido-backend", - "version": "1.17.1", + "version": "1.18.2", "description": "Gradido unified backend providing an API-Service for Gradido Transactions", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/backend", @@ -15,7 +15,8 @@ "lint": "eslint --max-warnings=0 --ext .js,.ts .", "test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles", "seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts", - "klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts" + "klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts", + "locales": "scripts/sort.sh" }, "dependencies": { "@hyperswarm/dht": "^6.2.0", @@ -72,5 +73,10 @@ "ts-node": "^10.0.0", "tsconfig-paths": "^3.14.0", "typescript": "^4.3.4" + }, + "nodemonConfig": { + "ignore": [ + "**/*.test.ts" + ] } } diff --git a/backend/scripts/sort.sh b/backend/scripts/sort.sh new file mode 100755 index 000000000..e5c5c41c6 --- /dev/null +++ b/backend/scripts/sort.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +ROOT_DIR=$(dirname "$0")/.. + +tmp=$(mktemp) +exit_code=0 + +for locale_file in $ROOT_DIR/src/locales/*.json +do + jq -f $(dirname "$0")/sort_filter.jq $locale_file > "$tmp" + if [ "$*" == "--fix" ] + then + mv "$tmp" $locale_file + else + if diff -q "$tmp" $locale_file > /dev/null ; + then + : # all good + else + exit_code=$? + echo "$(basename -- $locale_file) is not sorted by keys" + fi + fi +done + +exit $exit_code diff --git a/backend/scripts/sort_filter.jq b/backend/scripts/sort_filter.jq new file mode 100644 index 000000000..9d108f8f0 --- /dev/null +++ b/backend/scripts/sort_filter.jq @@ -0,0 +1,13 @@ +def walk(f): + . as $in + | if type == "object" then + reduce keys_unsorted[] as $key + ( {}; . + { ($key): ($in[$key] | walk(f)) } ) | f + elif type == "array" then map( walk(f) ) | f + else f + end; + +def keys_sort_by(f): + to_entries | sort_by(.key|f ) | from_entries; + +walk(if type == "object" then keys_sort_by(ascii_upcase) else . end) \ No newline at end of file diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index d010b4ab0..4cca9e0e2 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: 'v14.2022-12-22', + EXPECTED: 'v15.2023-02-07', CURRENT: '', }, } @@ -99,11 +99,6 @@ const webhook = { WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret', } -const eventProtocol = { - // global switch to enable writing of EventProtocol-Entries - EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false, -} - // This is needed by graphql-directive-auth process.env.APP_SECRET = server.JWT_SECRET @@ -139,7 +134,6 @@ const CONFIG = { ...email, ...loginServer, ...webhook, - ...eventProtocol, ...federation, } diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index ddbc387a1..7e499feb9 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -10,6 +10,7 @@ import { sendAccountMultiRegistrationEmail, sendContributionConfirmedEmail, sendContributionDeniedEmail, + sendContributionDeletedEmail, sendResetPasswordEmail, sendTransactionLinkRedeemedEmail, sendTransactionReceivedEmail, @@ -438,6 +439,84 @@ describe('sendEmailVariants', () => { }) }) + describe('sendContributionDeletedEmail', () => { + beforeAll(async () => { + result = await sendContributionDeletedEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'contributionDeleted', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL, + communityURL: CONFIG.COMMUNITY_URL, + }, + }) + }) + + it('has expected result', () => { + expect(result).toMatchObject({ + envelope: { + from: 'info@gradido.net', + to: ['peter@lustig.de'], + }, + message: expect.any(String), + originalMessage: expect.objectContaining({ + to: 'Peter Lustig ', + from: 'Gradido (do not answer) ', + attachments: [], + subject: 'Gradido: Your common good contribution was deleted', + html: expect.any(String), + text: expect.stringContaining('GRADIDO: YOUR COMMON GOOD CONTRIBUTION WAS DELETED'), + }), + }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain( + 'Gradido: Your common good contribution was deleted', + ) + expect(result.originalMessage.html).toContain( + '>Gradido: Your common good contribution was deleted', + ) + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'Your public good contribution “My contribution.” was deleted by Bibi Bloxberg.', + ) + expect(result.originalMessage.html).toContain( + 'To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!', + ) + expect(result.originalMessage.html).toContain( + `Link to your account: ${CONFIG.EMAIL_LINK_OVERVIEW}`, + ) + expect(result.originalMessage.html).toContain('Please do not reply to this email!') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') + expect(result.originalMessage.html).toContain('—————') + expect(result.originalMessage.html).toContain( + '
Gradido-Akademie Logo

Gradido-Akademie
Institut für Wirtschaftsbionik
Pfarrweg 2
74653 Künzelsau
Deutschland
support@supportmail.com
http://localhost/', + ) + }) + }) + }) + describe('sendResetPasswordEmail', () => { beforeAll(async () => { result = await sendResetPasswordEmail({ diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index 681ee56af..4e3881829 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -103,6 +103,32 @@ export const sendContributionConfirmedEmail = (data: { }) } +export const sendContributionDeletedEmail = (data: { + firstName: string + lastName: string + email: string + language: string + senderFirstName: string + senderLastName: string + contributionMemo: string +}): Promise | null> => { + return sendEmailTranslated({ + receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, + template: 'contributionDeleted', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + senderFirstName: data.senderFirstName, + senderLastName: data.senderLastName, + contributionMemo: data.contributionMemo, + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL, + communityURL: CONFIG.COMMUNITY_URL, + }, + }) +} + export const sendContributionDeniedEmail = (data: { firstName: string lastName: string diff --git a/backend/src/emails/templates/contributionDeleted/html.pug b/backend/src/emails/templates/contributionDeleted/html.pug new file mode 100644 index 000000000..d6b3ea207 --- /dev/null +++ b/backend/src/emails/templates/contributionDeleted/html.pug @@ -0,0 +1,16 @@ +doctype html +html(lang=locale) + head + title= t('emails.contributionDeleted.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.contributionDeleted.subject') + #container.col + include ../hello.pug + p= t('emails.contributionDeleted.commonGoodContributionDeleted', { senderFirstName, senderLastName, contributionMemo }) + p= t('emails.contributionDeleted.toSeeContributionsAndMessages') + p + = t('emails.general.linkToYourAccount') + = " " + a(href=overviewURL) #{overviewURL} + p= t('emails.general.pleaseDoNotReply') + include ../greatingFormularImprint.pug diff --git a/backend/src/emails/templates/contributionDeleted/subject.pug b/backend/src/emails/templates/contributionDeleted/subject.pug new file mode 100644 index 000000000..024588472 --- /dev/null +++ b/backend/src/emails/templates/contributionDeleted/subject.pug @@ -0,0 +1 @@ += t('emails.contributionDeleted.subject') diff --git a/backend/src/event/Event.ts b/backend/src/event/Event.ts index 09a31d4e0..8e65d85f2 100644 --- a/backend/src/event/Event.ts +++ b/backend/src/event/Event.ts @@ -1,517 +1,212 @@ -import { EventProtocol } from '@entity/EventProtocol' -import decimal from 'decimal.js-light' +import { EventProtocol as DbEvent } from '@entity/EventProtocol' +import Decimal from 'decimal.js-light' import { EventProtocolType } from './EventProtocolType' -export class EventBasic { - type: string - createdAt: Date -} -export class EventBasicUserId extends EventBasic { - userId: number +export const Event = ( + type: EventProtocolType, + userId: number, + xUserId: number | null = null, + xCommunityId: number | null = null, + transactionId: number | null = null, + contributionId: number | null = null, + amount: Decimal | null = null, + messageId: number | null = null, +): DbEvent => { + const event = new DbEvent() + event.type = type + event.userId = userId + event.xUserId = xUserId + event.xCommunityId = xCommunityId + event.transactionId = transactionId + event.contributionId = contributionId + event.amount = amount + event.messageId = messageId + return event } -export class EventBasicTx extends EventBasicUserId { - transactionId: number - amount: decimal -} - -export class EventBasicTxX extends EventBasicTx { - xUserId: number - xCommunityId: number -} - -export class EventBasicCt extends EventBasicUserId { - contributionId: number - amount: decimal -} - -export class EventBasicCtX extends EventBasicCt { - xUserId: number - xCommunityId: number -} - -export class EventBasicRedeem extends EventBasicUserId { - transactionId?: number - contributionId?: number -} - -export class EventBasicCtMsg extends EventBasicCt { - messageId: number -} - -export class EventVisitGradido extends EventBasic {} -export class EventRegister extends EventBasicUserId {} -export class EventRedeemRegister extends EventBasicRedeem {} -export class EventVerifyRedeem extends EventBasicRedeem {} -export class EventInactiveAccount extends EventBasicUserId {} -export class EventSendConfirmationEmail extends EventBasicUserId {} -export class EventSendAccountMultiRegistrationEmail extends EventBasicUserId {} -export class EventSendForgotPasswordEmail extends EventBasicUserId {} -export class EventSendTransactionSendEmail extends EventBasicTxX {} -export class EventSendTransactionReceiveEmail extends EventBasicTxX {} -export class EventSendTransactionLinkRedeemEmail extends EventBasicTxX {} -export class EventSendAddedContributionEmail extends EventBasicCt {} -export class EventSendContributionConfirmEmail extends EventBasicCt {} -export class EventConfirmationEmail extends EventBasicUserId {} -export class EventRegisterEmailKlicktipp extends EventBasicUserId {} -export class EventLogin extends EventBasicUserId {} -export class EventLogout extends EventBasicUserId {} -export class EventRedeemLogin extends EventBasicRedeem {} -export class EventActivateAccount extends EventBasicUserId {} -export class EventPasswordChange extends EventBasicUserId {} -export class EventTransactionSend extends EventBasicTxX {} -export class EventTransactionSendRedeem extends EventBasicTxX {} -export class EventTransactionRepeateRedeem extends EventBasicTxX {} -export class EventTransactionCreation extends EventBasicTx {} -export class EventTransactionReceive extends EventBasicTxX {} -export class EventTransactionReceiveRedeem extends EventBasicTxX {} -export class EventContributionCreate extends EventBasicCt {} -export class EventAdminContributionCreate extends EventBasicCt {} -export class EventAdminContributionDelete extends EventBasicCt {} -export class EventAdminContributionUpdate extends EventBasicCt {} -export class EventUserCreateContributionMessage extends EventBasicCtMsg {} -export class EventAdminCreateContributionMessage extends EventBasicCtMsg {} -export class EventContributionDelete extends EventBasicCt {} -export class EventContributionUpdate extends EventBasicCt {} -export class EventContributionConfirm extends EventBasicCtX {} -export class EventContributionDeny extends EventBasicCtX {} -export class EventContributionLinkDefine extends EventBasicCt {} -export class EventContributionLinkActivateRedeem extends EventBasicCt {} -export class EventDeleteUser extends EventBasicUserId {} -export class EventUndeleteUser extends EventBasicUserId {} -export class EventChangeUserRole extends EventBasicUserId {} -export class EventAdminUpdateContribution extends EventBasicCt {} -export class EventAdminDeleteContribution extends EventBasicCt {} -export class EventCreateContributionLink extends EventBasicCt {} -export class EventDeleteContributionLink extends EventBasicCt {} -export class EventUpdateContributionLink extends EventBasicCt {} - -export class Event { - constructor() - constructor(event?: EventProtocol) { - if (event) { - this.id = event.id - this.type = event.type - this.createdAt = event.createdAt - this.userId = event.userId - this.xUserId = event.xUserId - this.xCommunityId = event.xCommunityId - this.transactionId = event.transactionId - this.contributionId = event.contributionId - this.amount = event.amount - } - } - - public setEventBasic(): Event { - this.type = EventProtocolType.BASIC - this.createdAt = new Date() - - return this - } - - public setEventVisitGradido(): Event { - this.setEventBasic() - this.type = EventProtocolType.VISIT_GRADIDO - - return this - } - - public setEventRegister(ev: EventRegister): Event { - this.setByBasicUser(ev.userId) - this.type = EventProtocolType.REGISTER - - return this - } - - public setEventRedeemRegister(ev: EventRedeemRegister): Event { - this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId) - this.type = EventProtocolType.REDEEM_REGISTER - - return this - } - - public setEventVerifyRedeem(ev: EventVerifyRedeem): Event { - this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId) - this.type = EventProtocolType.VERIFY_REDEEM - - return this - } - - public setEventInactiveAccount(ev: EventInactiveAccount): Event { - this.setByBasicUser(ev.userId) - this.type = EventProtocolType.INACTIVE_ACCOUNT - - return this - } - - public setEventSendConfirmationEmail(ev: EventSendConfirmationEmail): Event { - this.setByBasicUser(ev.userId) - this.type = EventProtocolType.SEND_CONFIRMATION_EMAIL - - return this - } - - public setEventSendAccountMultiRegistrationEmail( - ev: EventSendAccountMultiRegistrationEmail, - ): Event { - this.setByBasicUser(ev.userId) - this.type = EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL - - return this - } - - public setEventSendForgotPasswordEmail(ev: EventSendForgotPasswordEmail): Event { - this.setByBasicUser(ev.userId) - this.type = EventProtocolType.SEND_FORGOT_PASSWORD_EMAIL - - return this - } - - public setEventSendTransactionSendEmail(ev: EventSendTransactionSendEmail): Event { - this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) - this.type = EventProtocolType.SEND_TRANSACTION_SEND_EMAIL - - return this - } - - public setEventSendTransactionReceiveEmail(ev: EventSendTransactionReceiveEmail): Event { - this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) - this.type = EventProtocolType.SEND_TRANSACTION_RECEIVE_EMAIL - - return this - } - - public setEventSendTransactionLinkRedeemEmail(ev: EventSendTransactionLinkRedeemEmail): Event { - this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) - this.type = EventProtocolType.SEND_TRANSACTION_LINK_REDEEM_EMAIL - - return this - } - - public setEventSendAddedContributionEmail(ev: EventSendAddedContributionEmail): Event { - this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) - this.type = EventProtocolType.SEND_ADDED_CONTRIBUTION_EMAIL - - return this - } - - public setEventSendContributionConfirmEmail(ev: EventSendContributionConfirmEmail): Event { - this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) - this.type = EventProtocolType.SEND_CONTRIBUTION_CONFIRM_EMAIL - - return this - } - - public setEventConfirmationEmail(ev: EventConfirmationEmail): Event { - this.setByBasicUser(ev.userId) - this.type = EventProtocolType.CONFIRM_EMAIL - - return this - } - - public setEventRegisterEmailKlicktipp(ev: EventRegisterEmailKlicktipp): Event { - this.setByBasicUser(ev.userId) - this.type = EventProtocolType.REGISTER_EMAIL_KLICKTIPP - - return this - } - - public setEventLogin(ev: EventLogin): Event { - this.setByBasicUser(ev.userId) - this.type = EventProtocolType.LOGIN - - return this - } - - public setEventLogout(ev: EventLogout): Event { - this.setByBasicUser(ev.userId) - this.type = EventProtocolType.LOGOUT - - return this - } - - public setEventRedeemLogin(ev: EventRedeemLogin): Event { - this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId) - this.type = EventProtocolType.REDEEM_LOGIN - - return this - } - - public setEventActivateAccount(ev: EventActivateAccount): Event { - this.setByBasicUser(ev.userId) - this.type = EventProtocolType.ACTIVATE_ACCOUNT - - return this - } - - public setEventPasswordChange(ev: EventPasswordChange): Event { - this.setByBasicUser(ev.userId) - this.type = EventProtocolType.PASSWORD_CHANGE - - return this - } - - public setEventTransactionSend(ev: EventTransactionSend): Event { - this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) - this.type = EventProtocolType.TRANSACTION_SEND - - return this - } - - public setEventTransactionSendRedeem(ev: EventTransactionSendRedeem): Event { - this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) - this.type = EventProtocolType.TRANSACTION_SEND_REDEEM - - return this - } - - public setEventTransactionRepeateRedeem(ev: EventTransactionRepeateRedeem): Event { - this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) - this.type = EventProtocolType.TRANSACTION_REPEATE_REDEEM - - return this - } - - public setEventTransactionCreation(ev: EventTransactionCreation): Event { - this.setByBasicTx(ev.userId, ev.transactionId, ev.amount) - this.type = EventProtocolType.TRANSACTION_CREATION - - return this - } - - public setEventTransactionReceive(ev: EventTransactionReceive): Event { - this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) - this.type = EventProtocolType.TRANSACTION_RECEIVE - - return this - } - - public setEventTransactionReceiveRedeem(ev: EventTransactionReceiveRedeem): Event { - this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) - this.type = EventProtocolType.TRANSACTION_RECEIVE_REDEEM - - return this - } - - public setEventContributionCreate(ev: EventContributionCreate): Event { - this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) - this.type = EventProtocolType.CONTRIBUTION_CREATE - - return this - } - - public setEventAdminContributionCreate(ev: EventAdminContributionCreate): Event { - this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) - this.type = EventProtocolType.ADMIN_CONTRIBUTION_CREATE - - return this - } - - public setEventAdminContributionDelete(ev: EventAdminContributionDelete): Event { - this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) - this.type = EventProtocolType.ADMIN_CONTRIBUTION_DELETE - - return this - } - - public setEventAdminContributionUpdate(ev: EventAdminContributionUpdate): Event { - this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) - this.type = EventProtocolType.ADMIN_CONTRIBUTION_UPDATE - - return this - } - - public setEventUserCreateContributionMessage(ev: EventUserCreateContributionMessage): Event { - this.setByBasicCtMsg(ev.userId, ev.contributionId, ev.amount, ev.messageId) - this.type = EventProtocolType.USER_CREATE_CONTRIBUTION_MESSAGE - - return this - } - - public setEventAdminCreateContributionMessage(ev: EventAdminCreateContributionMessage): Event { - this.setByBasicCtMsg(ev.userId, ev.contributionId, ev.amount, ev.messageId) - this.type = EventProtocolType.ADMIN_CREATE_CONTRIBUTION_MESSAGE - - return this - } - - public setEventContributionDelete(ev: EventContributionDelete): Event { - this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) - this.type = EventProtocolType.CONTRIBUTION_DELETE - - return this - } - - public setEventContributionUpdate(ev: EventContributionUpdate): Event { - this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) - this.type = EventProtocolType.CONTRIBUTION_UPDATE - - return this - } - - public setEventContributionConfirm(ev: EventContributionConfirm): Event { - this.setByBasicCtX(ev.userId, ev.contributionId, ev.amount, ev.xUserId, ev.xCommunityId) - this.type = EventProtocolType.CONTRIBUTION_CONFIRM - - return this - } - - public setEventContributionDeny(ev: EventContributionDeny): Event { - this.setByBasicCtX(ev.userId, ev.contributionId, ev.amount, ev.xUserId, ev.xCommunityId) - this.type = EventProtocolType.CONTRIBUTION_DENY - - return this - } - - public setEventContributionLinkDefine(ev: EventContributionLinkDefine): Event { - this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) - this.type = EventProtocolType.CONTRIBUTION_LINK_DEFINE - - return this - } - - public setEventContributionLinkActivateRedeem(ev: EventContributionLinkActivateRedeem): Event { - this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) - this.type = EventProtocolType.CONTRIBUTION_LINK_ACTIVATE_REDEEM - - return this - } - - public setEventDeleteUser(ev: EventDeleteUser): Event { - this.setByBasicUser(ev.userId) - this.type = EventProtocolType.DELETE_USER - - return this - } - - public setEventUndeleteUser(ev: EventUndeleteUser): Event { - this.setByBasicUser(ev.userId) - this.type = EventProtocolType.UNDELETE_USER - - return this - } - - public setEventChangeUserRole(ev: EventChangeUserRole): Event { - this.setByBasicUser(ev.userId) - this.type = EventProtocolType.CHANGE_USER_ROLE - - return this - } - - public setEventAdminUpdateContribution(ev: EventAdminUpdateContribution): Event { - this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) - this.type = EventProtocolType.ADMIN_UPDATE_CONTRIBUTION - - return this - } - - public setEventAdminDeleteContribution(ev: EventAdminDeleteContribution): Event { - this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) - this.type = EventProtocolType.ADMIN_DELETE_CONTRIBUTION - - return this - } - - public setEventCreateContributionLink(ev: EventCreateContributionLink): Event { - this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) - this.type = EventProtocolType.CREATE_CONTRIBUTION_LINK - - return this - } - - public setEventDeleteContributionLink(ev: EventDeleteContributionLink): Event { - this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) - this.type = EventProtocolType.DELETE_CONTRIBUTION_LINK - - return this - } - - public setEventUpdateContributionLink(ev: EventUpdateContributionLink): Event { - this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) - this.type = EventProtocolType.UPDATE_CONTRIBUTION_LINK - - return this - } - - setByBasicUser(userId: number): Event { - this.setEventBasic() - this.userId = userId - - return this - } - - setByBasicTx(userId: number, transactionId: number, amount: decimal): Event { - this.setByBasicUser(userId) - this.transactionId = transactionId - this.amount = amount - - return this - } - - setByBasicTxX( - userId: number, - transactionId: number, - amount: decimal, - xUserId: number, - xCommunityId: number, - ): Event { - this.setByBasicTx(userId, transactionId, amount) - this.xUserId = xUserId - this.xCommunityId = xCommunityId - - return this - } - - setByBasicCt(userId: number, contributionId: number, amount: decimal): Event { - this.setByBasicUser(userId) - this.contributionId = contributionId - this.amount = amount - - return this - } - - setByBasicCtMsg( - userId: number, - contributionId: number, - amount: decimal, - messageId: number, - ): Event { - this.setByBasicCt(userId, contributionId, amount) - this.messageId = messageId - - return this - } - - setByBasicCtX( - userId: number, - contributionId: number, - amount: decimal, - xUserId: number, - xCommunityId: number, - ): Event { - this.setByBasicCt(userId, contributionId, amount) - this.xUserId = xUserId - this.xCommunityId = xCommunityId - - return this - } - - setByBasicRedeem(userId: number, transactionId?: number, contributionId?: number): Event { - this.setByBasicUser(userId) - if (transactionId) this.transactionId = transactionId - if (contributionId) this.contributionId = contributionId - - return this - } - - id: number - type: string - createdAt: Date - userId: number - xUserId?: number - xCommunityId?: number - transactionId?: number - contributionId?: number - amount?: decimal - messageId?: number -} +export const EVENT_CONTRIBUTION_CREATE = async ( + userId: number, + contributionId: number, + amount: Decimal, +): Promise => + Event( + EventProtocolType.CONTRIBUTION_CREATE, + userId, + null, + null, + null, + contributionId, + amount, + ).save() + +export const EVENT_CONTRIBUTION_DELETE = async ( + userId: number, + contributionId: number, + amount: Decimal, +): Promise => + Event( + EventProtocolType.CONTRIBUTION_DELETE, + userId, + null, + null, + null, + contributionId, + amount, + ).save() + +export const EVENT_CONTRIBUTION_UPDATE = async ( + userId: number, + contributionId: number, + amount: Decimal, +): Promise => + Event( + EventProtocolType.CONTRIBUTION_UPDATE, + userId, + null, + null, + null, + contributionId, + amount, + ).save() + +export const EVENT_ADMIN_CONTRIBUTION_CREATE = async ( + userId: number, + contributionId: number, + amount: Decimal, +): Promise => + Event( + EventProtocolType.ADMIN_CONTRIBUTION_CREATE, + userId, + null, + null, + null, + contributionId, + amount, + ).save() + +export const EVENT_ADMIN_CONTRIBUTION_UPDATE = async ( + userId: number, + contributionId: number, + amount: Decimal, +): Promise => + Event( + EventProtocolType.ADMIN_CONTRIBUTION_UPDATE, + userId, + null, + null, + null, + contributionId, + amount, + ).save() + +export const EVENT_ADMIN_CONTRIBUTION_DELETE = async ( + userId: number, + contributionId: number, + amount: Decimal, +): Promise => + Event( + EventProtocolType.ADMIN_CONTRIBUTION_DELETE, + userId, + null, + null, + null, + contributionId, + amount, + ).save() + +export const EVENT_CONTRIBUTION_CONFIRM = async ( + userId: number, + contributionId: number, + amount: Decimal, +): Promise => + Event( + EventProtocolType.CONTRIBUTION_CONFIRM, + userId, + null, + null, + null, + contributionId, + amount, + ).save() + +export const EVENT_ADMIN_CONTRIBUTION_DENY = async ( + userId: number, + xUserId: number, + contributionId: number, + amount: Decimal, +): Promise => + Event( + EventProtocolType.ADMIN_CONTRIBUTION_DENY, + userId, + xUserId, + null, + null, + contributionId, + amount, + ).save() + +export const EVENT_TRANSACTION_SEND = async ( + userId: number, + xUserId: number, + transactionId: number, + amount: Decimal, +): Promise => + Event( + EventProtocolType.TRANSACTION_SEND, + userId, + xUserId, + null, + transactionId, + null, + amount, + ).save() + +export const EVENT_TRANSACTION_RECEIVE = async ( + userId: number, + xUserId: number, + transactionId: number, + amount: Decimal, +): Promise => + Event( + EventProtocolType.TRANSACTION_RECEIVE, + userId, + xUserId, + null, + transactionId, + null, + amount, + ).save() + +export const EVENT_LOGIN = async (userId: number): Promise => + Event(EventProtocolType.LOGIN, userId, null, null, null, null, null, null).save() + +export const EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = async ( + userId: number, +): Promise => Event(EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL, userId).save() + +export const EVENT_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise => + Event(EventProtocolType.SEND_CONFIRMATION_EMAIL, userId).save() + +export const EVENT_ADMIN_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise => + Event(EventProtocolType.ADMIN_SEND_CONFIRMATION_EMAIL, userId).save() + +/* export const EVENT_REDEEM_REGISTER = async ( + userId: number, + transactionId: number | null = null, + contributionId: number | null = null, +): Promise => + Event( + EventProtocolType.REDEEM_REGISTER, + userId, + null, + null, + transactionId, + contributionId, + ).save() +*/ + +export const EVENT_REGISTER = async (userId: number): Promise => + Event(EventProtocolType.REGISTER, userId).save() + +export const EVENT_ACTIVATE_ACCOUNT = async (userId: number): Promise => + Event(EventProtocolType.ACTIVATE_ACCOUNT, userId).save() diff --git a/backend/src/event/EventProtocolEmitter.ts b/backend/src/event/EventProtocolEmitter.ts deleted file mode 100644 index b4b22bce1..000000000 --- a/backend/src/event/EventProtocolEmitter.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Event } from '@/event/Event' -import { backendLogger as logger } from '@/server/logger' -import { EventProtocol } from '@entity/EventProtocol' -import CONFIG from '@/config' - -class EventProtocolEmitter { - /* }extends EventEmitter { */ - private events: Event[] - - /* - public addEvent(event: Event) { - this.events.push(event) - } - - public getEvents(): Event[] { - return this.events - } - */ - - public isDisabled() { - logger.info(`EventProtocol - isDisabled=${CONFIG.EVENT_PROTOCOL_DISABLED}`) - return CONFIG.EVENT_PROTOCOL_DISABLED === true - } - - public async writeEvent(event: Event): Promise { - if (!eventProtocol.isDisabled()) { - logger.info(`writeEvent(${JSON.stringify(event)})`) - const dbEvent = new EventProtocol() - dbEvent.type = event.type - dbEvent.createdAt = event.createdAt - dbEvent.userId = event.userId - if (event.xUserId) dbEvent.xUserId = event.xUserId - if (event.xCommunityId) dbEvent.xCommunityId = event.xCommunityId - if (event.contributionId) dbEvent.contributionId = event.contributionId - if (event.transactionId) dbEvent.transactionId = event.transactionId - if (event.amount) dbEvent.amount = event.amount - await dbEvent.save() - } - } -} -export const eventProtocol = new EventProtocolEmitter() diff --git a/backend/src/event/EventProtocolType.ts b/backend/src/event/EventProtocolType.ts index b7c2f0151..3a4c914c1 100644 --- a/backend/src/event/EventProtocolType.ts +++ b/backend/src/event/EventProtocolType.ts @@ -1,49 +1,50 @@ export enum EventProtocolType { - BASIC = 'BASIC', - VISIT_GRADIDO = 'VISIT_GRADIDO', + // VISIT_GRADIDO = 'VISIT_GRADIDO', REGISTER = 'REGISTER', REDEEM_REGISTER = 'REDEEM_REGISTER', - VERIFY_REDEEM = 'VERIFY_REDEEM', - INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT', + // VERIFY_REDEEM = 'VERIFY_REDEEM', + // INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT', SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL', + ADMIN_SEND_CONFIRMATION_EMAIL = 'ADMIN_SEND_CONFIRMATION_EMAIL', SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTIREGISTRATION_EMAIL', - CONFIRM_EMAIL = 'CONFIRM_EMAIL', - REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP', + // CONFIRM_EMAIL = 'CONFIRM_EMAIL', + // REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP', LOGIN = 'LOGIN', - LOGOUT = 'LOGOUT', - REDEEM_LOGIN = 'REDEEM_LOGIN', + // LOGOUT = 'LOGOUT', + // REDEEM_LOGIN = 'REDEEM_LOGIN', ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT', - SEND_FORGOT_PASSWORD_EMAIL = 'SEND_FORGOT_PASSWORD_EMAIL', - PASSWORD_CHANGE = 'PASSWORD_CHANGE', - SEND_TRANSACTION_SEND_EMAIL = 'SEND_TRANSACTION_SEND_EMAIL', - SEND_TRANSACTION_RECEIVE_EMAIL = 'SEND_TRANSACTION_RECEIVE_EMAIL', + // SEND_FORGOT_PASSWORD_EMAIL = 'SEND_FORGOT_PASSWORD_EMAIL', + // PASSWORD_CHANGE = 'PASSWORD_CHANGE', + // SEND_TRANSACTION_SEND_EMAIL = 'SEND_TRANSACTION_SEND_EMAIL', + // SEND_TRANSACTION_RECEIVE_EMAIL = 'SEND_TRANSACTION_RECEIVE_EMAIL', TRANSACTION_SEND = 'TRANSACTION_SEND', - TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM', - TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM', - TRANSACTION_CREATION = 'TRANSACTION_CREATION', + // TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM', + // TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM', + // TRANSACTION_CREATION = 'TRANSACTION_CREATION', TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE', - TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM', - SEND_TRANSACTION_LINK_REDEEM_EMAIL = 'SEND_TRANSACTION_LINK_REDEEM_EMAIL', - SEND_ADDED_CONTRIBUTION_EMAIL = 'SEND_ADDED_CONTRIBUTION_EMAIL', - SEND_CONTRIBUTION_CONFIRM_EMAIL = 'SEND_CONTRIBUTION_CONFIRM_EMAIL', + // TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM', + // SEND_TRANSACTION_LINK_REDEEM_EMAIL = 'SEND_TRANSACTION_LINK_REDEEM_EMAIL', + // SEND_ADDED_CONTRIBUTION_EMAIL = 'SEND_ADDED_CONTRIBUTION_EMAIL', + // SEND_CONTRIBUTION_CONFIRM_EMAIL = 'SEND_CONTRIBUTION_CONFIRM_EMAIL', CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE', CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM', - CONTRIBUTION_DENY = 'CONTRIBUTION_DENY', - CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE', - CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM', + // CONTRIBUTION_DENY = 'CONTRIBUTION_DENY', + // CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE', + // CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM', CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE', CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE', ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE', ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE', + ADMIN_CONTRIBUTION_DENY = 'ADMIN_CONTRIBUTION_DENY', ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE', - USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE', - ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE', - DELETE_USER = 'DELETE_USER', - UNDELETE_USER = 'UNDELETE_USER', - CHANGE_USER_ROLE = 'CHANGE_USER_ROLE', - ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION', - ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION', - CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK', - DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK', - UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK', + // USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE', + // ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE', + // DELETE_USER = 'DELETE_USER', + // UNDELETE_USER = 'UNDELETE_USER', + // CHANGE_USER_ROLE = 'CHANGE_USER_ROLE', + // ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION', + // ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION', + // CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK', + // DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK', + // UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK', } diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index 26f9cd656..65cccf4d4 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -15,6 +15,8 @@ import { calculateDecay } from '@/util/decay' import { RIGHTS } from '@/auth/RIGHTS' import { GdtResolver } from './GdtResolver' +import { getLastTransaction } from './util/getLastTransaction' + @Resolver() export class BalanceResolver { @Authorized([RIGHTS.BALANCE]) @@ -32,7 +34,7 @@ export class BalanceResolver { const lastTransaction = context.lastTransaction ? context.lastTransaction - : await dbTransaction.findOne({ userId: user.id }, { order: { id: 'DESC' } }) + : await getLastTransaction(user.id) logger.debug(`lastTransaction=${lastTransaction}`) diff --git a/backend/src/graphql/resolver/ContributionLinkResolver.test.ts b/backend/src/graphql/resolver/ContributionLinkResolver.test.ts index 2a17f0556..62f273829 100644 --- a/backend/src/graphql/resolver/ContributionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionLinkResolver.test.ts @@ -313,27 +313,6 @@ describe('Contribution Links', () => { ) }) - it('returns an error if name is an empty string', async () => { - jest.clearAllMocks() - await expect( - mutate({ - mutation: createContributionLink, - variables: { - ...variables, - name: '', - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('The name must be initialized')], - }), - ) - }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('The name must be initialized') - }) - it('returns an error if name is shorter than 5 characters', async () => { jest.clearAllMocks() await expect( @@ -376,27 +355,6 @@ describe('Contribution Links', () => { expect(logger.error).toBeCalledWith('The value of name is too long', 101) }) - it('returns an error if memo is an empty string', async () => { - jest.clearAllMocks() - await expect( - mutate({ - mutation: createContributionLink, - variables: { - ...variables, - memo: '', - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('The memo must be initialized')], - }), - ) - }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('The memo must be initialized') - }) - it('returns an error if memo is shorter than 5 characters', async () => { jest.clearAllMocks() await expect( diff --git a/backend/src/graphql/resolver/ContributionLinkResolver.ts b/backend/src/graphql/resolver/ContributionLinkResolver.ts index 6a7a71391..39f202848 100644 --- a/backend/src/graphql/resolver/ContributionLinkResolver.ts +++ b/backend/src/graphql/resolver/ContributionLinkResolver.ts @@ -40,20 +40,12 @@ export class ContributionLinkResolver { }: ContributionLinkArgs, ): Promise { isStartEndDateValid(validFrom, validTo) - // TODO: this should be enforced by the schema. - if (!name) { - throw new LogError('The name must be initialized') - } if (name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS) { throw new LogError('The value of name is too short', name.length) } if (name.length > CONTRIBUTIONLINK_NAME_MAX_CHARS) { throw new LogError('The value of name is too long', name.length) } - // TODO: this should be enforced by the schema. - if (!memo) { - throw new LogError('The memo must be initialized') - } if (memo.length < MEMO_MIN_CHARS) { throw new LogError('The value of memo is too short', memo.length) } diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts index 36d78c382..f3e5e865d 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts @@ -99,14 +99,18 @@ describe('ContributionMessageResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('ContributionMessage was not sent successfully')], + errors: [ + new GraphQLError( + 'ContributionMessage was not sent successfully: Error: Contribution not found', + ), + ], }), ) }) it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'ContributionMessage was not sent successfully', + 'ContributionMessage was not sent successfully: Error: Contribution not found', new Error('Contribution not found'), ) }) @@ -135,14 +139,18 @@ describe('ContributionMessageResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('ContributionMessage was not sent successfully')], + errors: [ + new GraphQLError( + 'ContributionMessage was not sent successfully: Error: Admin can not answer on his own contribution', + ), + ], }), ) }) it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'ContributionMessage was not sent successfully', + 'ContributionMessage was not sent successfully: Error: Admin can not answer on his own contribution', new Error('Admin can not answer on his own contribution'), ) }) @@ -229,14 +237,18 @@ describe('ContributionMessageResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('ContributionMessage was not sent successfully')], + errors: [ + new GraphQLError( + 'ContributionMessage was not sent successfully: Error: Contribution not found', + ), + ], }), ) }) it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'ContributionMessage was not sent successfully', + 'ContributionMessage was not sent successfully: Error: Contribution not found', new Error('Contribution not found'), ) }) @@ -257,14 +269,18 @@ describe('ContributionMessageResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('ContributionMessage was not sent successfully')], + errors: [ + new GraphQLError( + 'ContributionMessage was not sent successfully: Error: Can not send message to contribution of another user', + ), + ], }), ) }) it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'ContributionMessage was not sent successfully', + 'ContributionMessage was not sent successfully: Error: Can not send message to contribution of another user', new Error('Can not send message to contribution of another user'), ) }) diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.ts b/backend/src/graphql/resolver/ContributionMessageResolver.ts index 3e6f86e53..4248946b1 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.ts @@ -54,7 +54,7 @@ export class ContributionMessageResolver { await queryRunner.commitTransaction() } catch (e) { await queryRunner.rollbackTransaction() - throw new LogError('ContributionMessage was not sent successfully', e) + throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e) } finally { await queryRunner.release() } @@ -144,7 +144,7 @@ export class ContributionMessageResolver { await queryRunner.commitTransaction() } catch (e) { await queryRunner.rollbackTransaction() - throw new LogError('ContributionMessage was not sent successfully', e) + throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e) } finally { await queryRunner.release() } diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 93d7d36d0..652d4cd7f 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -22,11 +22,7 @@ import { listContributions, listUnconfirmedContributions, } from '@/seeds/graphql/queries' -import { - // sendAccountActivationEmail, - sendContributionConfirmedEmail, - // sendContributionRejectedEmail, -} from '@/emails/sendEmailVariants' +import { sendContributionConfirmedEmail } from '@/emails/sendEmailVariants' import { cleanDB, resetToken, @@ -48,7 +44,6 @@ import { logger, i18n as localization } from '@test/testSetup' import { UserInputError } from 'apollo-server-express' import { ContributionStatus } from '../enum/ContributionStatus' -// mock account activation email to avoid console spam // mock account activation email to avoid console spam jest.mock('@/emails/sendEmailVariants', () => { const originalModule = jest.requireActual('@/emails/sendEmailVariants') @@ -249,7 +244,7 @@ describe('ContributionResolver', () => { ) }) - it('stores the create contribution event in the database', async () => { + it('stores the CONTRIBUTION_CREATE event in the database', async () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.CONTRIBUTION_CREATE, @@ -536,7 +531,6 @@ describe('ContributionResolver', () => { }) }) - // TODO: why is this here - this is a different call `adminUpdateContribution` not `updateContribution` describe('admin tries to update a user contribution', () => { it('throws an error', async () => { jest.clearAllMocks() @@ -704,7 +698,7 @@ describe('ContributionResolver', () => { ) }) - it('stores the update contribution event in the database', async () => { + it('stores the CONTRIBUTION_UPDATE event in the database', async () => { bibi = await query({ query: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, @@ -1271,7 +1265,7 @@ describe('ContributionResolver', () => { ).resolves.toBeTruthy() }) - it('stores the delete contribution event in the database', async () => { + it('stores the CONTRIBUTION_DELETE event in the database', async () => { const contribution = await mutate({ mutation: createContribution, variables: { @@ -1790,7 +1784,7 @@ describe('ContributionResolver', () => { ) }) - it('stores the admin create contribution event in the database', async () => { + it('stores the ADMIN_CONTRIBUTION_CREATE event in the database', async () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.ADMIN_CONTRIBUTION_CREATE, @@ -2060,7 +2054,7 @@ describe('ContributionResolver', () => { ) }) - it('stores the admin update contribution event in the database', async () => { + it('stores the ADMIN_CONTRIBUTION_UPDATE event in the database', async () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE, @@ -2100,7 +2094,7 @@ describe('ContributionResolver', () => { ) }) - it('stores the admin update contribution event in the database', async () => { + it('stores the ADMIN_CONTRIBUTION_UPDATE event in the database', async () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE, @@ -2244,7 +2238,7 @@ describe('ContributionResolver', () => { ) }) - it('stores the admin delete contribution event in the database', async () => { + it('stores the ADMIN_CONTRIBUTION_DELETE event in the database', async () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.ADMIN_CONTRIBUTION_DELETE, @@ -2386,7 +2380,7 @@ describe('ContributionResolver', () => { ) }) - it('stores the contribution confirm event in the database', async () => { + it('stores the CONTRIBUTION_CONFIRM event in the database', async () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.CONTRIBUTION_CONFIRM, @@ -2418,7 +2412,7 @@ describe('ContributionResolver', () => { }) }) - it('stores the send confirmation email event in the database', async () => { + it('stores the SEND_CONFIRMATION_EMAIL event in the database', async () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.SEND_CONFIRMATION_EMAIL, diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index c68c34f99..f070fade5 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -37,24 +37,26 @@ import { } from './util/creations' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, FULL_CREATION_AVAILABLE } from './const/const' import { - Event, - EventContributionCreate, - EventContributionDelete, - EventContributionUpdate, - EventContributionConfirm, - EventAdminContributionCreate, - EventAdminContributionDelete, - EventAdminContributionUpdate, + EVENT_CONTRIBUTION_CREATE, + EVENT_CONTRIBUTION_DELETE, + EVENT_CONTRIBUTION_UPDATE, + EVENT_ADMIN_CONTRIBUTION_CREATE, + EVENT_ADMIN_CONTRIBUTION_UPDATE, + EVENT_ADMIN_CONTRIBUTION_DELETE, + EVENT_CONTRIBUTION_CONFIRM, + EVENT_ADMIN_CONTRIBUTION_DENY, } from '@/event/Event' -import { eventProtocol } from '@/event/EventProtocolEmitter' import { calculateDecay } from '@/util/decay' import { sendContributionConfirmedEmail, + sendContributionDeletedEmail, sendContributionDeniedEmail, } from '@/emails/sendEmailVariants' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import LogError from '@/server/LogError' +import { getLastTransaction } from './util/getLastTransaction' + @Resolver() export class ContributionResolver { @Authorized([RIGHTS.CREATE_CONTRIBUTION]) @@ -71,8 +73,6 @@ export class ContributionResolver { throw new LogError('Memo text is too long', memo.length) } - const event = new Event() - const user = getUser(context) const creations = await getUserCreation(user.id, clientTimezoneOffset) logger.trace('creations', creations) @@ -91,11 +91,7 @@ export class ContributionResolver { logger.trace('contribution to save', contribution) await DbContribution.save(contribution) - const eventCreateContribution = new EventContributionCreate() - eventCreateContribution.userId = user.id - eventCreateContribution.amount = amount - eventCreateContribution.contributionId = contribution.id - await eventProtocol.writeEvent(event.setEventContributionCreate(eventCreateContribution)) + await EVENT_CONTRIBUTION_CREATE(user.id, contribution.id, amount) return new UnconfirmedContribution(contribution, user, creations) } @@ -106,7 +102,6 @@ export class ContributionResolver { @Arg('id', () => Int) id: number, @Ctx() context: Context, ): Promise { - const event = new Event() const user = getUser(context) const contribution = await DbContribution.findOne(id) if (!contribution) { @@ -124,11 +119,7 @@ export class ContributionResolver { contribution.deletedAt = new Date() await contribution.save() - const eventDeleteContribution = new EventContributionDelete() - eventDeleteContribution.userId = user.id - eventDeleteContribution.contributionId = contribution.id - eventDeleteContribution.amount = contribution.amount - await eventProtocol.writeEvent(event.setEventContributionDelete(eventDeleteContribution)) + await EVENT_CONTRIBUTION_DELETE(user.id, contribution.id, contribution.amount) const res = await contribution.softRemove() return !!res @@ -275,13 +266,7 @@ export class ContributionResolver { contributionToUpdate.updatedAt = new Date() DbContribution.save(contributionToUpdate) - const event = new Event() - - const eventUpdateContribution = new EventContributionUpdate() - eventUpdateContribution.userId = user.id - eventUpdateContribution.contributionId = contributionId - eventUpdateContribution.amount = amount - await eventProtocol.writeEvent(event.setEventContributionUpdate(eventUpdateContribution)) + await EVENT_CONTRIBUTION_UPDATE(user.id, contributionId, amount) return new UnconfirmedContribution(contributionToUpdate, user, creations) } @@ -317,7 +302,6 @@ export class ContributionResolver { ) } - const event = new Event() const moderator = getUser(context) logger.trace('moderator: ', moderator.id) const creations = await getUserCreation(emailContact.userId, clientTimezoneOffset) @@ -339,13 +323,7 @@ export class ContributionResolver { await DbContribution.save(contribution) - const eventAdminCreateContribution = new EventAdminContributionCreate() - eventAdminCreateContribution.userId = moderator.id - eventAdminCreateContribution.amount = amount - eventAdminCreateContribution.contributionId = contribution.id - await eventProtocol.writeEvent( - event.setEventAdminContributionCreate(eventAdminCreateContribution), - ) + await EVENT_ADMIN_CONTRIBUTION_CREATE(moderator.id, contribution.id, amount) return getUserCreation(emailContact.userId, clientTimezoneOffset) } @@ -440,14 +418,7 @@ export class ContributionResolver { result.creation = await getUserCreation(emailContact.user.id, clientTimezoneOffset) - const event = new Event() - const eventAdminContributionUpdate = new EventAdminContributionUpdate() - eventAdminContributionUpdate.userId = emailContact.user.id - eventAdminContributionUpdate.amount = amount - eventAdminContributionUpdate.contributionId = contributionToUpdate.id - await eventProtocol.writeEvent( - event.setEventAdminContributionUpdate(eventAdminContributionUpdate), - ) + await EVENT_ADMIN_CONTRIBUTION_UPDATE(emailContact.user.id, contributionToUpdate.id, amount) return result } @@ -518,15 +489,9 @@ export class ContributionResolver { await contribution.save() const res = await contribution.softRemove() - const event = new Event() - const eventAdminContributionDelete = new EventAdminContributionDelete() - eventAdminContributionDelete.userId = contribution.userId - eventAdminContributionDelete.amount = contribution.amount - eventAdminContributionDelete.contributionId = contribution.id - await eventProtocol.writeEvent( - event.setEventAdminContributionDelete(eventAdminContributionDelete), - ) - sendContributionDeniedEmail({ + await EVENT_ADMIN_CONTRIBUTION_DELETE(contribution.userId, contribution.id, contribution.amount) + + sendContributionDeletedEmail({ firstName: user.firstName, lastName: user.lastName, email: user.emailContact.email, @@ -582,16 +547,11 @@ export class ContributionResolver { const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED') - try { - const lastTransaction = await queryRunner.manager - .createQueryBuilder() - .select('transaction') - .from(DbTransaction, 'transaction') - .where('transaction.userId = :id', { id: contribution.userId }) - .orderBy('transaction.id', 'DESC') - .getOne() - logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined') + const lastTransaction = await getLastTransaction(contribution.userId) + logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined') + + try { let newBalance = new Decimal(0) let decay: Decay | null = null if (lastTransaction) { @@ -642,12 +602,7 @@ export class ContributionResolver { await queryRunner.release() } - const event = new Event() - const eventContributionConfirm = new EventContributionConfirm() - eventContributionConfirm.userId = user.id - eventContributionConfirm.amount = contribution.amount - eventContributionConfirm.contributionId = contribution.id - await eventProtocol.writeEvent(event.setEventContributionConfirm(eventContributionConfirm)) + await EVENT_CONTRIBUTION_CONFIRM(user.id, contribution.id, contribution.amount) } finally { releaseLock() } @@ -737,6 +692,13 @@ export class ContributionResolver { contributionToUpdate.deniedAt = new Date() const res = await contributionToUpdate.save() + await EVENT_ADMIN_CONTRIBUTION_DENY( + contributionToUpdate.userId, + moderator.id, + contributionToUpdate.id, + contributionToUpdate.amount, + ) + sendContributionDeniedEmail({ firstName: user.firstName, lastName: user.lastName, diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index 0666efc8e..c77a0bf64 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -116,6 +116,11 @@ describe('TransactionLinkResolver', () => { }) describe('redeemTransactionLink', () => { + afterAll(async () => { + await cleanDB() + resetToken() + }) + describe('contributionLink', () => { describe('input not valid', () => { beforeAll(async () => { @@ -487,8 +492,7 @@ describe('TransactionLinkResolver', () => { pageSize: 5, } - // TODO: there is a test not cleaning up after itself! Fix it! - beforeAll(async () => { + afterAll(async () => { await cleanDB() resetToken() }) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 696c51d97..4647dde60 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -34,6 +34,8 @@ import QueryLinkResult from '@union/QueryLinkResult' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import LogError from '@/server/LogError' +import { getLastTransaction } from './util/getLastTransaction' + // TODO: do not export, test it inside the resolver export const transactionLinkCode = (date: Date): string => { const time = date.getTime().toString(16) @@ -262,13 +264,7 @@ export class TransactionLinkResolver { await queryRunner.manager.insert(DbContribution, contribution) - const lastTransaction = await queryRunner.manager - .createQueryBuilder() - .select('transaction') - .from(DbTransaction, 'transaction') - .where('transaction.userId = :id', { id: user.id }) - .orderBy('transaction.id', 'DESC') - .getOne() + const lastTransaction = await getLastTransaction(user.id) let newBalance = new Decimal(0) let decay: Decay | null = null diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 404a76094..6751aa6ad 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -330,7 +330,7 @@ describe('send coins', () => { ) }) - it('stores the send transaction event in the database', async () => { + it('stores the TRANSACTION_SEND event in the database', async () => { // Find the exact transaction (sent one is the one with user[1] as user) const transaction = await Transaction.find({ userId: user[1].id, @@ -347,7 +347,7 @@ describe('send coins', () => { ) }) - it('stores the receive event in the database', async () => { + it('stores the TRANSACTION_RECEIVE event in the database', async () => { // Find the exact transaction (received one is the one with user[0] as user) const transaction = await Transaction.find({ userId: user[0].id, diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index c4fbe8c10..9d5a1d38c 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -29,8 +29,7 @@ import { sendTransactionLinkRedeemedEmail, sendTransactionReceivedEmail, } from '@/emails/sendEmailVariants' -import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event' -import { eventProtocol } from '@/event/EventProtocolEmitter' +import { EVENT_TRANSACTION_RECEIVE, EVENT_TRANSACTION_SEND } from '@/event/Event' import { BalanceResolver } from './BalanceResolver' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' @@ -39,6 +38,8 @@ import { findUserByEmail } from './UserResolver' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import LogError from '@/server/LogError' +import { getLastTransaction } from './util/getLastTransaction' + export const executeTransaction = async ( amount: Decimal, memo: string, @@ -136,20 +137,18 @@ export const executeTransaction = async ( await queryRunner.commitTransaction() logger.info(`commit Transaction successful...`) - const eventTransactionSend = new EventTransactionSend() - eventTransactionSend.userId = transactionSend.userId - eventTransactionSend.xUserId = transactionSend.linkedUserId - eventTransactionSend.transactionId = transactionSend.id - eventTransactionSend.amount = transactionSend.amount.mul(-1) - await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend)) + await EVENT_TRANSACTION_SEND( + transactionSend.userId, + transactionSend.linkedUserId, + transactionSend.id, + transactionSend.amount.mul(-1), + ) - const eventTransactionReceive = new EventTransactionReceive() - eventTransactionReceive.userId = transactionReceive.userId - eventTransactionReceive.xUserId = transactionReceive.linkedUserId - eventTransactionReceive.transactionId = transactionReceive.id - eventTransactionReceive.amount = transactionReceive.amount - await eventProtocol.writeEvent( - new Event().setEventTransactionReceive(eventTransactionReceive), + await EVENT_TRANSACTION_RECEIVE( + transactionReceive.userId, + transactionReceive.linkedUserId, + transactionReceive.id, + transactionReceive.amount, ) } catch (e) { await queryRunner.rollbackTransaction() @@ -204,10 +203,7 @@ export class TransactionResolver { logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`) // find current balance - const lastTransaction = await dbTransaction.findOne( - { userId: user.id }, - { order: { id: 'DESC' }, relations: ['contribution'] }, - ) + const lastTransaction = await getLastTransaction(user.id, ['contribution']) logger.debug(`lastTransaction=${lastTransaction}`) const balanceResolver = new BalanceResolver() diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 17eddca94..19eb04b34 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -19,6 +19,7 @@ import { setUserRole, deleteUser, unDeleteUser, + sendActivationEmail, } from '@/seeds/graphql/mutations' import { verifyLogin, queryOptIn, searchAdminUsers, searchUsers } from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' @@ -175,6 +176,19 @@ describe('UserResolver', () => { }) }) }) + + it('stores the REGISTER event in the database', async () => { + const userConatct = await UserContact.findOneOrFail( + { email: 'peter@lustig.de' }, + { relations: ['user'] }, + ) + expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.REGISTER, + userId: userConatct.user.id, + }), + ) + }) }) describe('account activation email', () => { @@ -196,7 +210,7 @@ describe('UserResolver', () => { }) }) - it('stores the send confirmation event in the database', () => { + it('stores the SEND_CONFIRMATION_EMAIL event in the database', () => { expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.SEND_CONFIRMATION_EMAIL, @@ -206,7 +220,7 @@ describe('UserResolver', () => { }) }) - describe('email already exists', () => { + describe('user already exists', () => { let mutation: User beforeAll(async () => { mutation = await mutate({ mutation: createUser, variables }) @@ -236,6 +250,19 @@ describe('UserResolver', () => { }), ) }) + + it('stores the SEND_ACCOUNT_MULTIREGISTRATION_EMAIL event in the database', async () => { + const userConatct = await UserContact.findOneOrFail( + { email: 'peter@lustig.de' }, + { relations: ['user'] }, + ) + expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL, + userId: userConatct.user.id, + }), + ) + }) }) describe('unknown language', () => { @@ -328,7 +355,7 @@ describe('UserResolver', () => { ) }) - it('stores the account activated event in the database', () => { + it('stores the ACTIVATE_ACCOUNT event in the database', () => { expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.ACTIVATE_ACCOUNT, @@ -337,7 +364,7 @@ describe('UserResolver', () => { ) }) - it('stores the redeem register event in the database', () => { + it('stores the REDEEM_REGISTER event in the database', () => { expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.REDEEM_REGISTER, @@ -421,7 +448,7 @@ describe('UserResolver', () => { ) }) - it('stores the redeem register event in the database', async () => { + it('stores the REDEEM_REGISTER event in the database', async () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.REDEEM_REGISTER, @@ -647,6 +674,19 @@ describe('UserResolver', () => { it('sets the token in the header', () => { expect(headerPushMock).toBeCalledWith({ key: 'token', value: expect.any(String) }) }) + + it('stores the LOGIN event in the database', async () => { + const userConatct = await UserContact.findOneOrFail( + { email: 'bibi@bloxberg.de' }, + { relations: ['user'] }, + ) + expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.LOGIN, + userId: userConatct.user.id, + }), + ) + }) }) describe('user is in database and wrong password', () => { @@ -887,7 +927,7 @@ describe('UserResolver', () => { ) }) - it('stores the login event in the database', () => { + it('stores the LOGIN event in the database', () => { expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.LOGIN, @@ -1668,6 +1708,157 @@ describe('UserResolver', () => { }) }) + /// + + describe('sendActivationEmail', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + mutate({ mutation: sendActivationEmail, variables: { email: 'bibi@bloxberg.de' } }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated', () => { + describe('without admin rights', () => { + beforeAll(async () => { + user = await userFactory(testEnv, bibiBloxberg) + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) + + it('returns an error', async () => { + await expect( + mutate({ mutation: sendActivationEmail, variables: { email: 'bibi@bloxberg.de' } }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('with admin rights', () => { + beforeAll(async () => { + admin = await userFactory(testEnv, peterLustig) + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) + + describe('user does not exist', () => { + it('throws an error', async () => { + jest.clearAllMocks() + await expect( + mutate({ mutation: sendActivationEmail, variables: { email: 'INVALID' } }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('No user with this credentials')], + }), + ) + }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('No user with this credentials', 'invalid') + }) + }) + + describe('user is deleted', () => { + it('throws an error', async () => { + jest.clearAllMocks() + await userFactory(testEnv, stephenHawking) + await expect( + mutate({ mutation: sendActivationEmail, variables: { email: 'stephen@hawking.uk' } }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('User with given email contact is deleted')], + }), + ) + }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'User with given email contact is deleted', + 'stephen@hawking.uk', + ) + }) + }) + + describe('sendActivationEmail with success', () => { + beforeAll(async () => { + user = await userFactory(testEnv, bibiBloxberg) + }) + + it('returns true', async () => { + const result = await mutate({ + mutation: sendActivationEmail, + variables: { email: 'bibi@bloxberg.de' }, + }) + expect(result).toEqual( + expect.objectContaining({ + data: { + sendActivationEmail: true, + }, + }), + ) + }) + + it('sends an account activation email', async () => { + const userConatct = await UserContact.findOneOrFail( + { email: 'bibi@bloxberg.de' }, + { relations: ['user'] }, + ) + const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace( + /{optin}/g, + userConatct.emailVerificationCode.toString(), + ).replace(/{code}/g, '') + expect(sendAccountActivationEmail).toBeCalledWith({ + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + language: 'de', + activationLink, + timeDurationObject: expect.objectContaining({ + hours: expect.any(Number), + minutes: expect.any(Number), + }), + }) + }) + + it('stores the ADMIN_SEND_CONFIRMATION_EMAIL event in the database', async () => { + const userConatct = await UserContact.findOneOrFail( + { email: 'bibi@bloxberg.de' }, + { relations: ['user'] }, + ) + expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.ADMIN_SEND_CONFIRMATION_EMAIL, + userId: userConatct.user.id, + }), + ) + }) + }) + }) + }) + }) + describe('unDelete user', () => { describe('unauthenticated', () => { it('returns an error', async () => { diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index d92958009..f9617b0df 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -48,15 +48,14 @@ import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddle import { klicktippSignIn } from '@/apis/KlicktippController' import { RIGHTS } from '@/auth/RIGHTS' import { hasElopageBuys } from '@/util/hasElopageBuys' -import { eventProtocol } from '@/event/EventProtocolEmitter' import { Event, - EventLogin, - EventRedeemRegister, - EventRegister, - EventSendAccountMultiRegistrationEmail, - EventSendConfirmationEmail, - EventActivateAccount, + EVENT_LOGIN, + EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL, + EVENT_SEND_CONFIRMATION_EMAIL, + EVENT_REGISTER, + EVENT_ACTIVATE_ACCOUNT, + EVENT_ADMIN_SEND_CONFIRMATION_EMAIL, } from '@/event/Event' import { getUserCreations } from './util/creations' import { isValidPassword } from '@/password/EncryptorUtils' @@ -64,6 +63,7 @@ import { FULL_CREATION_AVAILABLE } from './const/const' import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' import LogError from '@/server/LogError' +import { EventProtocolType } from '@/event/EventProtocolType' // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') @@ -177,9 +177,8 @@ export class UserResolver { key: 'token', value: encode(dbUser.gradidoID), }) - const ev = new EventLogin() - ev.userId = user.id - eventProtocol.writeEvent(new Event().setEventLogin(ev)) + + await EVENT_LOGIN(user.id) logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`) return user } @@ -211,7 +210,6 @@ export class UserResolver { ) // TODO: wrong default value (should be null), how does graphql work here? Is it an required field? // default int publisher_id = 0; - const event = new Event() // Validate Language (no throw) if (!language || !isLanguage(language)) { @@ -249,11 +247,9 @@ export class UserResolver { email, language: foundUser.language, // use language of the emails owner for sending }) - const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail() - eventSendAccountMultiRegistrationEmail.userId = foundUser.id - eventProtocol.writeEvent( - event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail), - ) + + await EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL(foundUser.id) + logger.info( `sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`, ) @@ -270,10 +266,7 @@ export class UserResolver { const gradidoID = await newGradidoID() - const eventRegister = new EventRegister() - const eventRedeemRegister = new EventRedeemRegister() - const eventSendConfirmEmail = new EventSendConfirmationEmail() - + const eventRegisterRedeem = Event(EventProtocolType.REDEEM_REGISTER, 0) let dbUser = new DbUser() dbUser.gradidoID = gradidoID dbUser.firstName = firstName @@ -290,14 +283,14 @@ export class UserResolver { logger.info('redeemCode found contributionLink=' + contributionLink) if (contributionLink) { dbUser.contributionLinkId = contributionLink.id - eventRedeemRegister.contributionId = contributionLink.id + eventRegisterRedeem.contributionId = contributionLink.id } } else { const transactionLink = await DbTransactionLink.findOne({ code: redeemCode }) logger.info('redeemCode found transactionLink=' + transactionLink) if (transactionLink) { dbUser.referrerId = transactionLink.userId - eventRedeemRegister.transactionId = transactionLink.id + eventRegisterRedeem.transactionId = transactionLink.id } } } @@ -335,8 +328,8 @@ export class UserResolver { timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), }) logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`) - eventSendConfirmEmail.userId = dbUser.id - eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail)) + + await EVENT_SEND_CONFIRMATION_EMAIL(dbUser.id) if (!emailSent) { logger.debug(`Account confirmation link: ${activationLink}`) @@ -353,11 +346,10 @@ export class UserResolver { logger.info('createUser() successful...') if (redeemCode) { - eventRedeemRegister.userId = dbUser.id - await eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister)) + eventRegisterRedeem.userId = dbUser.id + await eventRegisterRedeem.save() } else { - eventRegister.userId = dbUser.id - await eventProtocol.writeEvent(event.setEventRegister(eventRegister)) + await EVENT_REGISTER(dbUser.id) } return new User(dbUser) @@ -460,8 +452,6 @@ export class UserResolver { await queryRunner.connect() await queryRunner.startTransaction('REPEATABLE READ') - const event = new Event() - try { // Save user await queryRunner.manager.save(user).catch((error) => { @@ -475,9 +465,7 @@ export class UserResolver { await queryRunner.commitTransaction() logger.info('User and UserContact data written successfully...') - const eventActivateAccount = new EventActivateAccount() - eventActivateAccount.userId = user.id - eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount)) + await EVENT_ACTIVATE_ACCOUNT(user.id) } catch (e) { await queryRunner.rollbackTransaction() throw new LogError('Error on writing User and User Contact data', e) @@ -793,19 +781,12 @@ export class UserResolver { email = email.trim().toLowerCase() // const user = await dbUser.findOne({ id: emailContact.userId }) const user = await findUserByEmail(email) - if (!user) { - throw new LogError('Could not find user to given email contact', email) - } - if (user.deletedAt) { + if (user.deletedAt || user.emailContact.deletedAt) { throw new LogError('User with given email contact is deleted', email) } - const emailContact = user.emailContact - if (emailContact.deletedAt) { - throw new LogError('The given email contact for this user is deleted', email) - } - emailContact.emailResendCount++ - await emailContact.save() + user.emailContact.emailResendCount++ + await user.emailContact.save() // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendAccountActivationEmail({ @@ -813,7 +794,7 @@ export class UserResolver { lastName: user.lastName, email, language: user.language, - activationLink: activationLink(emailContact.emailVerificationCode), + activationLink: activationLink(user.emailContact.emailVerificationCode), timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), }) @@ -821,12 +802,7 @@ export class UserResolver { if (!emailSent) { logger.info(`Account confirmation link: ${activationLink}`) } else { - const event = new Event() - const eventSendConfirmationEmail = new EventSendConfirmationEmail() - eventSendConfirmationEmail.userId = user.id - await eventProtocol.writeEvent( - event.setEventSendConfirmationEmail(eventSendConfirmationEmail), - ) + await EVENT_ADMIN_SEND_CONFIRMATION_EMAIL(user.id) } return true diff --git a/backend/src/graphql/resolver/util/getLastTransaction.ts b/backend/src/graphql/resolver/util/getLastTransaction.ts new file mode 100644 index 000000000..5b3e862c2 --- /dev/null +++ b/backend/src/graphql/resolver/util/getLastTransaction.ts @@ -0,0 +1,14 @@ +import { Transaction as DbTransaction } from '@entity/Transaction' + +export const getLastTransaction = async ( + userId: number, + relations?: string[], +): Promise => { + return DbTransaction.findOne( + { userId }, + { + order: { balanceDate: 'DESC', id: 'DESC' }, + relations, + }, + ) +} diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index 38b53508b..530e8db10 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -1,10 +1,5 @@ { "emails": { - "addedContributionMessage": { - "commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.", - "subject": "Gradido: Nachricht zu deinem Gemeinwohl-Beitrag", - "toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!" - }, "accountActivation": { "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen:", "emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.", @@ -19,12 +14,22 @@ "onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail" }, + "addedContributionMessage": { + "commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.", + "subject": "Gradido: Nachricht zu deinem Gemeinwohl-Beitrag", + "toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!" + }, "contributionConfirmed": { "commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.", "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt" }, - "contributionRejected": { - "commonGoodContributionRejected": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} abgelehnt.", + "contributionDeleted": { + "commonGoodContributionDeleted": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} gelöscht.", + "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde gelöscht", + "toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!" + }, + "contributionDenied": { + "commonGoodContributionDenied": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} abgelehnt.", "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde abgelehnt", "toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!" }, diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index 5cde70d26..269c38629 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -1,10 +1,5 @@ { "emails": { - "addedContributionMessage": { - "commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.", - "subject": "Gradido: Message about your common good contribution", - "toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!" - }, "accountActivation": { "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here:", "emailRegistered": "Your email address has just been registered with Gradido.", @@ -19,10 +14,20 @@ "onForgottenPasswordCopyLink": "or copy the link above into your browser window.", "subject": "Gradido: Try To Register Again With Your Email" }, + "addedContributionMessage": { + "commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.", + "subject": "Gradido: Message about your common good contribution", + "toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!" + }, "contributionConfirmed": { "commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.", "subject": "Gradido: Your contribution to the common good was confirmed" }, + "contributionDeleted": { + "commonGoodContributionDeleted": "Your public good contribution “{contributionMemo}” was deleted by {senderFirstName} {senderLastName}.", + "subject": "Gradido: Your common good contribution was deleted", + "toSeeContributionsAndMessages": "To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!" + }, "contributionDenied": { "commonGoodContributionDenied": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.", "subject": "Gradido: Your common good contribution was rejected", diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 2b4ed6656..5c05a4de9 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -68,6 +68,12 @@ export const createUser = gql` } ` +export const sendActivationEmail = gql` + mutation ($email: String!) { + sendActivationEmail(email: $email) + } +` + export const sendCoins = gql` mutation ($email: String!, $amount: Decimal!, $memo: String!) { sendCoins(email: $email, amount: $amount, memo: $memo) diff --git a/backend/src/seeds/users/stephen-hawking.ts b/backend/src/seeds/users/stephen-hawking.ts index 6c4f34d10..a683b7579 100644 --- a/backend/src/seeds/users/stephen-hawking.ts +++ b/backend/src/seeds/users/stephen-hawking.ts @@ -1,6 +1,5 @@ import { UserInterface } from './UserInterface' -// TODO: the generated email_contact is not deleted export const stephenHawking: UserInterface = { email: 'stephen@hawking.uk', firstName: 'Stephen', diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index 397a38730..482e9eb50 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -1,10 +1,10 @@ import { calculateDecay } from './decay' import Decimal from 'decimal.js-light' -import { Transaction } from '@entity/Transaction' import { Decay } from '@model/Decay' import { getCustomRepository } from '@dbTools/typeorm' import { TransactionLinkRepository } from '@repository/TransactionLink' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' +import { getLastTransaction } from '../graphql/resolver/util/getLastTransaction' function isStringBoolean(value: string): boolean { const lowerValue = value.toLowerCase() @@ -20,7 +20,7 @@ async function calculateBalance( time: Date, transactionLink?: dbTransactionLink | null, ): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> { - const lastTransaction = await Transaction.findOne({ userId }, { order: { id: 'DESC' } }) + const lastTransaction = await getLastTransaction(userId) if (!lastTransaction) return null const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) diff --git a/database/.env.dist b/database/.env.dist index 58362a7b9..689e4f509 100644 --- a/database/.env.dist +++ b/database/.env.dist @@ -1,5 +1,3 @@ -CONFIG_VERSION=v1.2022-03-18 - DB_HOST=localhost DB_PORT=3306 DB_USER=root diff --git a/database/entity/0050-add_messageId_to_event_protocol/EventProtocol.ts b/database/entity/0050-add_messageId_to_event_protocol/EventProtocol.ts index d4dbc526f..e457cc0a3 100644 --- a/database/entity/0050-add_messageId_to_event_protocol/EventProtocol.ts +++ b/database/entity/0050-add_messageId_to_event_protocol/EventProtocol.ts @@ -16,17 +16,17 @@ export class EventProtocol extends BaseEntity { @Column({ name: 'user_id', unsigned: true, nullable: false }) userId: number - @Column({ name: 'x_user_id', unsigned: true, nullable: true }) - xUserId: number + @Column({ name: 'x_user_id', type: 'int', unsigned: true, nullable: true }) + xUserId: number | null - @Column({ name: 'x_community_id', unsigned: true, nullable: true }) - xCommunityId: number + @Column({ name: 'x_community_id', type: 'int', unsigned: true, nullable: true }) + xCommunityId: number | null - @Column({ name: 'transaction_id', unsigned: true, nullable: true }) - transactionId: number + @Column({ name: 'transaction_id', type: 'int', unsigned: true, nullable: true }) + transactionId: number | null - @Column({ name: 'contribution_id', unsigned: true, nullable: true }) - contributionId: number + @Column({ name: 'contribution_id', type: 'int', unsigned: true, nullable: true }) + contributionId: number | null @Column({ type: 'decimal', @@ -35,8 +35,8 @@ export class EventProtocol extends BaseEntity { nullable: true, transformer: DecimalTransformer, }) - amount: Decimal + amount: Decimal | null - @Column({ name: 'message_id', unsigned: true, nullable: true }) - messageId: number + @Column({ name: 'message_id', type: 'int', unsigned: true, nullable: true }) + messageId: number | null } diff --git a/database/package.json b/database/package.json index f4e1c7e84..5be01a5d5 100644 --- a/database/package.json +++ b/database/package.json @@ -1,6 +1,6 @@ { "name": "gradido-database", - "version": "1.17.1", + "version": "1.18.2", "description": "Gradido Database Tool to execute database migrations", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/database", diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index e816c9236..9c9c7ac82 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -27,7 +27,7 @@ COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community" COMMUNITY_SUPPORT_MAIL=support@supportmail.com # backend -BACKEND_CONFIG_VERSION=v14.2022-12-22 +BACKEND_CONFIG_VERSION=v15.2023-02-07 JWT_EXPIRES_IN=10m GDT_API_URL=https://gdt.gradido.net @@ -56,9 +56,6 @@ EMAIL_CODE_REQUEST_TIME=10 WEBHOOK_ELOPAGE_SECRET=secret -# EventProtocol -EVENT_PROTOCOL_DISABLED=false - # Federation # if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen # on an hash created from this topic diff --git a/dht-node/.env.dist b/dht-node/.env.dist index c1641ea98..e005ca748 100644 --- a/dht-node/.env.dist +++ b/dht-node/.env.dist @@ -1,5 +1,3 @@ -CONFIG_VERSION=v1.2023-01-01 - # Database DB_HOST=localhost DB_PORT=3306 @@ -8,9 +6,6 @@ DB_PASSWORD= DB_DATABASE=gradido_community TYPEORM_LOGGING_RELATIVE_PATH=typeorm.dht-node.log -# EventProtocol -EVENT_PROTOCOL_DISABLED=false - # SET LOG LEVEL AS NEEDED IN YOUR .ENV # POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal # LOG_LEVEL=info diff --git a/dht-node/.env.template b/dht-node/.env.template index eca7cb277..b31667fdb 100644 --- a/dht-node/.env.template +++ b/dht-node/.env.template @@ -8,9 +8,6 @@ DB_PASSWORD=$DB_PASSWORD DB_DATABASE=gradido_community TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH -# EventProtocol -EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED - # Federation FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED diff --git a/dht-node/src/config/index.ts b/dht-node/src/config/index.ts index 02cbb20e9..795925ee3 100644 --- a/dht-node/src/config/index.ts +++ b/dht-node/src/config/index.ts @@ -9,7 +9,7 @@ const constants = { LOG_LEVEL: process.env.LOG_LEVEL || 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v1.2023-01-01', + EXPECTED: 'v2.2023-02-07', CURRENT: '', }, } @@ -28,11 +28,6 @@ const database = { process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.dht-node.log', } -const eventProtocol = { - // global switch to enable writing of EventProtocol-Entries - EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false, -} - const federation = { FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB', FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null, @@ -55,7 +50,6 @@ const CONFIG = { ...constants, ...server, ...database, - ...eventProtocol, ...federation, } diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts index c2d81d2c8..588f52c60 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -52,10 +52,6 @@ const community = { process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.', } */ -// const eventProtocol = { -// global switch to enable writing of EventProtocol-Entries -// EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false, -// } // This is needed by graphql-directive-auth // process.env.APP_SECRET = server.JWT_SECRET diff --git a/frontend/.env.dist b/frontend/.env.dist index 5ce6b430d..427d43359 100644 --- a/frontend/.env.dist +++ b/frontend/.env.dist @@ -1,5 +1,3 @@ -CONFIG_VERSION=v4.2022-12-20 - # Environment DEFAULT_PUBLISHER_ID=2896 diff --git a/frontend/package.json b/frontend/package.json index 29c440988..9aa457c19 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "bootstrap-vue-gradido-wallet", - "version": "1.17.1", + "version": "1.18.2", "private": true, "scripts": { "start": "node run/server.js", @@ -104,5 +104,10 @@ ], "author": "Gradido-Akademie - https://www.gradido.net/", "license": "Apache-2.0", - "description": "Gradido, the Natural Economy of Life, is a way to worldwide prosperity and peace in harmony with nature. - Gradido, die Natürliche Ökonomie des lebens, ist ein Weg zu weltweitem Wohlstand und Frieden in Harmonie mit der Natur." + "description": "Gradido, the Natural Economy of Life, is a way to worldwide prosperity and peace in harmony with nature. - Gradido, die Natürliche Ökonomie des lebens, ist ein Weg zu weltweitem Wohlstand und Frieden in Harmonie mit der Natur.", + "nodemonConfig": { + "ignore": [ + "**/*.spec.js" + ] + } } diff --git a/frontend/src/components/Contributions/ContributionForm.spec.js b/frontend/src/components/Contributions/ContributionForm.spec.js index 020e3f552..71ead88b9 100644 --- a/frontend/src/components/Contributions/ContributionForm.spec.js +++ b/frontend/src/components/Contributions/ContributionForm.spec.js @@ -24,11 +24,6 @@ describe('ContributionForm', () => { $t: jest.fn((t) => t), $d: jest.fn((d) => d), $n: jest.fn((n) => n), - $store: { - state: { - creation: ['1000', '1000', '1000'], - }, - }, $i18n: { locale: 'en', }, @@ -61,7 +56,7 @@ describe('ContributionForm', () => { }) }) - describe('dates', () => { + describe('dates and max amounts', () => { beforeEach(async () => { await wrapper.setData({ form: { @@ -73,204 +68,176 @@ describe('ContributionForm', () => { }) }) - describe('actual date', () => { - describe('same month', () => { - beforeEach(async () => { - const now = new Date().toISOString() - await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now) + describe('max amount reached for both months', () => { + beforeEach(() => { + wrapper.setProps({ + maxGddLastMonth: 0, + maxGddThisMonth: 0, }) - - describe('isThisMonth', () => { - it('has true', () => { - expect(wrapper.vm.isThisMonth).toBe(true) - }) + wrapper.setData({ + form: { + id: null, + date: 'set', + memo: '', + amount: '', + }, }) }) - describe.skip('month before', () => { - beforeEach(async () => { - await wrapper - .findComponent({ name: 'BFormDatepicker' }) - .vm.$emit('input', wrapper.vm.minimalDate) - }) - - describe('isThisMonth', () => { - it('has false', () => { - expect(wrapper.vm.isThisMonth).toBe(false) - }) - }) + it('shows message that no contributions are available', () => { + expect(wrapper.find('[data-test="contribtion-message"]').text()).toBe( + 'contribution.noOpenCreation.allMonth', + ) }) }) - describe.skip('date in middle of year', () => { - describe('same month', () => { - beforeEach(async () => { - // jest.useFakeTimers('modern') - // jest.setSystemTime(new Date('2020-07-06')) - // await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now) - await wrapper.setData({ - maximalDate: new Date(2020, 6, 6), - form: { date: new Date(2020, 6, 6) }, - }) - }) - - describe('minimalDate', () => { - it('has "2020-06-01T00:00:00.000Z"', () => { - expect(wrapper.vm.minimalDate.toISOString()).toBe('2020-06-01T00:00:00.000Z') - }) - }) - - describe('isThisMonth', () => { - it('has true', () => { - expect(wrapper.vm.isThisMonth).toBe(true) - }) + describe('max amount reached for last month, no date selected', () => { + beforeEach(() => { + wrapper.setProps({ + maxGddLastMonth: 0, }) }) - describe('month before', () => { - beforeEach(async () => { - // jest.useFakeTimers('modern') - // jest.setSystemTime(new Date('2020-07-06')) - // console.log('middle of year date – now:', wrapper.vm.minimalDate) - // await wrapper - // .findComponent({ name: 'BFormDatepicker' }) - // .vm.$emit('input', wrapper.vm.minimalDate) - await wrapper.setData({ - maximalDate: new Date(2020, 6, 6), - form: { date: new Date(2020, 5, 6) }, - }) - }) - - describe('minimalDate', () => { - it('has "2020-06-01T00:00:00.000Z"', () => { - expect(wrapper.vm.minimalDate.toISOString()).toBe('2020-06-01T00:00:00.000Z') - }) - }) - - describe('isThisMonth', () => { - it('has false', () => { - expect(wrapper.vm.isThisMonth).toBe(false) - }) - }) + it('shows no message', () => { + expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false) }) }) - describe.skip('date in january', () => { - describe('same month', () => { - beforeEach(async () => { - await wrapper.setData({ - maximalDate: new Date(2020, 0, 6), - form: { date: new Date(2020, 0, 6) }, - }) + describe('max amount reached for last month, last month selected', () => { + beforeEach(async () => { + wrapper.setProps({ + maxGddLastMonth: 0, + isThisMonth: false, }) - - describe('minimalDate', () => { - it('has "2019-12-01T00:00:00.000Z"', () => { - expect(wrapper.vm.minimalDate.toISOString()).toBe('2019-12-01T00:00:00.000Z') - }) - }) - - describe('isThisMonth', () => { - it('has true', () => { - expect(wrapper.vm.isThisMonth).toBe(true) - }) + await wrapper.setData({ + form: { + id: null, + date: 'set', + memo: '', + amount: '', + }, }) }) - describe('month before', () => { - beforeEach(async () => { - // jest.useFakeTimers('modern') - // jest.setSystemTime(new Date('2020-07-06')) - // console.log('middle of year date – now:', wrapper.vm.minimalDate) - // await wrapper - // .findComponent({ name: 'BFormDatepicker' }) - // .vm.$emit('input', wrapper.vm.minimalDate) - await wrapper.setData({ - maximalDate: new Date(2020, 0, 6), - form: { date: new Date(2019, 11, 6) }, - }) - }) - - describe('minimalDate', () => { - it('has "2019-12-01T00:00:00.000Z"', () => { - expect(wrapper.vm.minimalDate.toISOString()).toBe('2019-12-01T00:00:00.000Z') - }) - }) - - describe('isThisMonth', () => { - it('has false', () => { - expect(wrapper.vm.isThisMonth).toBe(false) - }) - }) + it('shows message that no contributions are available for last month', () => { + expect(wrapper.find('[data-test="contribtion-message"]').text()).toBe( + 'contribution.noOpenCreation.lastMonth', + ) }) }) - describe.skip('date with the 31st day of the month', () => { - describe('same month', () => { - beforeEach(async () => { - await wrapper.setData({ - maximalDate: new Date('2022-10-31T00:00:00.000Z'), - form: { date: new Date('2022-10-31T00:00:00.000Z') }, - }) + describe('max amount reached for last month, this month selected', () => { + beforeEach(async () => { + wrapper.setProps({ + maxGddLastMonth: 0, + isThisMonth: true, }) + await wrapper.setData({ + form: { + id: null, + date: 'set', + memo: '', + amount: '', + }, + }) + }) - describe('minimalDate', () => { - it('has "2022-09-01T00:00:00.000Z"', () => { - expect(wrapper.vm.minimalDate.toISOString()).toBe('2022-09-01T00:00:00.000Z') - }) - }) - - describe('isThisMonth', () => { - it('has true', () => { - expect(wrapper.vm.isThisMonth).toBe(true) - }) - }) + it('shows no message', () => { + expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false) }) }) - describe.skip('date with the 28th day of the month', () => { - describe('same month', () => { - beforeEach(async () => { - await wrapper.setData({ - maximalDate: new Date('2023-02-28T00:00:00.000Z'), - form: { date: new Date('2023-02-28T00:00:00.000Z') }, - }) + describe('max amount reached for this month, no date selected', () => { + beforeEach(() => { + wrapper.setProps({ + maxGddThisMonth: 0, }) + }) - describe('minimalDate', () => { - it('has "2023-01-01T00:00:00.000Z"', () => { - expect(wrapper.vm.minimalDate.toISOString()).toBe('2023-01-01T00:00:00.000Z') - }) - }) - - describe('isThisMonth', () => { - it('has true', () => { - expect(wrapper.vm.isThisMonth).toBe(true) - }) - }) + it('shows no message', () => { + expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false) }) }) - describe.skip('date with 29.02.2024 leap year', () => { - describe('same month', () => { - beforeEach(async () => { - await wrapper.setData({ - maximalDate: new Date('2024-02-29T00:00:00.000Z'), - form: { date: new Date('2024-02-29T00:00:00.000Z') }, - }) + describe('max amount reached for this month, this month selected', () => { + beforeEach(async () => { + wrapper.setProps({ + maxGddThisMonth: 0, + isThisMonth: true, }) + await wrapper.setData({ + form: { + id: null, + date: 'set', + memo: '', + amount: '', + }, + }) + }) - describe('minimalDate', () => { - it('has "2024-01-01T00:00:00.000Z"', () => { - expect(wrapper.vm.minimalDate.toISOString()).toBe('2024-01-01T00:00:00.000Z') - }) - }) + it('shows message that no contributions are available for last month', () => { + expect(wrapper.find('[data-test="contribtion-message"]').text()).toBe( + 'contribution.noOpenCreation.thisMonth', + ) + }) + }) - describe('isThisMonth', () => { - it('has true', () => { - expect(wrapper.vm.isThisMonth).toBe(true) - }) + describe('max amount reached for this month, last month selected', () => { + beforeEach(async () => { + wrapper.setProps({ + maxGddThisMonth: 0, + isThisMonth: false, }) + await wrapper.setData({ + form: { + id: null, + date: 'set', + memo: '', + amount: '', + }, + }) + }) + + it('shows no message', () => { + expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false) + }) + }) + }) + + describe('default return message', () => { + it('returns an empty string', () => { + expect(wrapper.vm.noOpenCreation).toBe('') + }) + }) + + describe('update amount', () => { + beforeEach(() => { + wrapper.findComponent({ name: 'InputHour' }).vm.$emit('updateAmount', 20) + }) + + it('updates form amount', () => { + expect(wrapper.vm.form.amount).toBe('400.00') + }) + }) + + describe('watch value', () => { + beforeEach(() => { + wrapper.setProps({ + value: { + id: 42, + date: 'set', + memo: 'Some Memo', + amount: '400.00', + }, + }) + }) + + it('updates form', () => { + expect(wrapper.vm.form).toEqual({ + id: 42, + date: 'set', + memo: 'Some Memo', + amount: '400.00', }) }) }) @@ -477,24 +444,23 @@ describe('ContributionForm', () => { }) }) - describe.skip('on trigger submit', () => { + describe('on trigger submit', () => { beforeEach(async () => { await wrapper.find('form').trigger('submit') }) it('emits "update-contribution"', () => { - expect(wrapper.emitted('update-contribution')).toEqual( - expect.arrayContaining([ - expect.arrayContaining([ - { - id: 2, - date: now, - memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', - amount: '200', - }, - ]), - ]), - ) + expect(wrapper.emitted('update-contribution')).toEqual([ + [ + { + id: 2, + date: now, + hours: 0, + memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', + amount: '200', + }, + ], + ]) }) }) }) diff --git a/frontend/src/components/Contributions/ContributionForm.vue b/frontend/src/components/Contributions/ContributionForm.vue index 9a2ffb54b..0d512ab63 100644 --- a/frontend/src/components/Contributions/ContributionForm.vue +++ b/frontend/src/components/Contributions/ContributionForm.vue @@ -23,10 +23,7 @@ -
+
{{ noOpenCreation }}
@@ -118,8 +115,8 @@ export default { } }, methods: { - updateAmount(amount) { - this.form.amount = (amount * 20).toFixed(2).toString() + updateAmount(hours) { + this.form.amount = (hours * 20).toFixed(2).toString() }, submit() { this.$emit(this.form.id ? 'update-contribution' : 'set-contribution', { ...this.form }) @@ -135,6 +132,15 @@ export default { }, }, computed: { + showMessage() { + if (this.maxGddThisMonth <= 0 && this.maxGddLastMonth <= 0) return true + if (this.form.date) + return ( + (this.isThisMonth && this.maxGddThisMonth <= 0) || + (!this.isThisMonth && this.maxGddLastMonth <= 0) + ) + return false + }, disabled() { return ( this.form.date === '' || diff --git a/frontend/src/components/Contributions/ContributionList.spec.js b/frontend/src/components/Contributions/ContributionList.spec.js index a1dfc934d..de875cf74 100644 --- a/frontend/src/components/Contributions/ContributionList.spec.js +++ b/frontend/src/components/Contributions/ContributionList.spec.js @@ -116,5 +116,15 @@ describe('ContributionList', () => { expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 2 }]]) }) }) + + describe('update status', () => { + beforeEach(() => { + wrapper.findComponent({ name: 'ContributionListItem' }).vm.$emit('update-state', { id: 2 }) + }) + + it('emits update status', () => { + expect(wrapper.emitted('update-state')).toEqual([[{ id: 2 }]]) + }) + }) }) }) diff --git a/frontend/src/components/DecayInformations/DecayInformation-BeforeStartblock.vue b/frontend/src/components/DecayInformations/DecayInformation-BeforeStartblock.vue index c0f34e24d..037b4f376 100644 --- a/frontend/src/components/DecayInformations/DecayInformation-BeforeStartblock.vue +++ b/frontend/src/components/DecayInformations/DecayInformation-BeforeStartblock.vue @@ -1,5 +1,9 @@