diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da8521a76..c204eb321 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 ###################################################### ############################################################################## 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..77e86ad46 100644 --- a/backend/src/event/Event.ts +++ b/backend/src/event/Event.ts @@ -1,4 +1,3 @@ -import { EventProtocol } from '@entity/EventProtocol' import decimal from 'decimal.js-light' import { EventProtocolType } from './EventProtocolType' @@ -68,6 +67,7 @@ export class EventTransactionReceiveRedeem extends EventBasicTxX {} export class EventContributionCreate extends EventBasicCt {} export class EventAdminContributionCreate extends EventBasicCt {} export class EventAdminContributionDelete extends EventBasicCt {} +export class EventAdminContributionDeny extends EventBasicCt {} export class EventAdminContributionUpdate extends EventBasicCt {} export class EventUserCreateContributionMessage extends EventBasicCtMsg {} export class EventAdminCreateContributionMessage extends EventBasicCtMsg {} @@ -87,21 +87,6 @@ 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() @@ -314,6 +299,13 @@ export class Event { return this } + public setEventAdminContributionDeny(ev: EventAdminContributionDeny): Event { + this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) + this.type = EventProtocolType.ADMIN_CONTRIBUTION_DENY + + return this + } + public setEventAdminContributionUpdate(ev: EventAdminContributionUpdate): Event { this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) this.type = EventProtocolType.ADMIN_CONTRIBUTION_UPDATE diff --git a/backend/src/event/EventProtocolEmitter.ts b/backend/src/event/EventProtocolEmitter.ts index b4b22bce1..a87e8a256 100644 --- a/backend/src/event/EventProtocolEmitter.ts +++ b/backend/src/event/EventProtocolEmitter.ts @@ -1,41 +1,17 @@ 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 writeEvent = async (event: Event): Promise => { + logger.info('writeEvent', event) + const dbEvent = new EventProtocol() + dbEvent.type = event.type + dbEvent.createdAt = event.createdAt + dbEvent.userId = event.userId + dbEvent.xUserId = event.xUserId || null + dbEvent.xCommunityId = event.xCommunityId || null + dbEvent.contributionId = event.contributionId || null + dbEvent.transactionId = event.transactionId || null + dbEvent.amount = event.amount || null + return dbEvent.save() } -export const eventProtocol = new EventProtocolEmitter() diff --git a/backend/src/event/EventProtocolType.ts b/backend/src/event/EventProtocolType.ts index b7c2f0151..ccd15d238 100644 --- a/backend/src/event/EventProtocolType.ts +++ b/backend/src/event/EventProtocolType.ts @@ -35,6 +35,7 @@ export enum EventProtocolType { 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', 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 46296e009..49bca2c42 100644 --- a/backend/src/graphql/resolver/ContributionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionLinkResolver.test.ts @@ -319,27 +319,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( @@ -382,27 +361,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 1e0930d91..4a74029ad 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') @@ -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() diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index c68c34f99..926742d8a 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -44,17 +44,21 @@ import { EventContributionConfirm, EventAdminContributionCreate, EventAdminContributionDelete, + EventAdminContributionDeny, EventAdminContributionUpdate, } from '@/event/Event' -import { eventProtocol } from '@/event/EventProtocolEmitter' +import { writeEvent } 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]) @@ -95,7 +99,7 @@ export class ContributionResolver { eventCreateContribution.userId = user.id eventCreateContribution.amount = amount eventCreateContribution.contributionId = contribution.id - await eventProtocol.writeEvent(event.setEventContributionCreate(eventCreateContribution)) + await writeEvent(event.setEventContributionCreate(eventCreateContribution)) return new UnconfirmedContribution(contribution, user, creations) } @@ -128,7 +132,7 @@ export class ContributionResolver { eventDeleteContribution.userId = user.id eventDeleteContribution.contributionId = contribution.id eventDeleteContribution.amount = contribution.amount - await eventProtocol.writeEvent(event.setEventContributionDelete(eventDeleteContribution)) + await writeEvent(event.setEventContributionDelete(eventDeleteContribution)) const res = await contribution.softRemove() return !!res @@ -281,7 +285,7 @@ export class ContributionResolver { eventUpdateContribution.userId = user.id eventUpdateContribution.contributionId = contributionId eventUpdateContribution.amount = amount - await eventProtocol.writeEvent(event.setEventContributionUpdate(eventUpdateContribution)) + await writeEvent(event.setEventContributionUpdate(eventUpdateContribution)) return new UnconfirmedContribution(contributionToUpdate, user, creations) } @@ -343,9 +347,7 @@ export class ContributionResolver { eventAdminCreateContribution.userId = moderator.id eventAdminCreateContribution.amount = amount eventAdminCreateContribution.contributionId = contribution.id - await eventProtocol.writeEvent( - event.setEventAdminContributionCreate(eventAdminCreateContribution), - ) + await writeEvent(event.setEventAdminContributionCreate(eventAdminCreateContribution)) return getUserCreation(emailContact.userId, clientTimezoneOffset) } @@ -445,9 +447,7 @@ export class ContributionResolver { eventAdminContributionUpdate.userId = emailContact.user.id eventAdminContributionUpdate.amount = amount eventAdminContributionUpdate.contributionId = contributionToUpdate.id - await eventProtocol.writeEvent( - event.setEventAdminContributionUpdate(eventAdminContributionUpdate), - ) + await writeEvent(event.setEventAdminContributionUpdate(eventAdminContributionUpdate)) return result } @@ -523,10 +523,8 @@ export class ContributionResolver { eventAdminContributionDelete.userId = contribution.userId eventAdminContributionDelete.amount = contribution.amount eventAdminContributionDelete.contributionId = contribution.id - await eventProtocol.writeEvent( - event.setEventAdminContributionDelete(eventAdminContributionDelete), - ) - sendContributionDeniedEmail({ + await writeEvent(event.setEventAdminContributionDelete(eventAdminContributionDelete)) + sendContributionDeletedEmail({ firstName: user.firstName, lastName: user.lastName, email: user.emailContact.email, @@ -582,16 +580,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) { @@ -647,7 +640,7 @@ export class ContributionResolver { eventContributionConfirm.userId = user.id eventContributionConfirm.amount = contribution.amount eventContributionConfirm.contributionId = contribution.id - await eventProtocol.writeEvent(event.setEventContributionConfirm(eventContributionConfirm)) + await writeEvent(event.setEventContributionConfirm(eventContributionConfirm)) } finally { releaseLock() } @@ -737,6 +730,13 @@ export class ContributionResolver { contributionToUpdate.deniedAt = new Date() const res = await contributionToUpdate.save() + const event = new Event() + const eventAdminContributionDeny = new EventAdminContributionDeny() + eventAdminContributionDeny.userId = contributionToUpdate.userId + eventAdminContributionDeny.amount = contributionToUpdate.amount + eventAdminContributionDeny.contributionId = contributionToUpdate.id + await writeEvent(event.setEventAdminContributionDeny(eventAdminContributionDeny)) + 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 af2e4fd59..f60ab45d0 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 () => { @@ -482,8 +487,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.ts b/backend/src/graphql/resolver/TransactionResolver.ts index c4fbe8c10..7ab9f34a1 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -30,7 +30,7 @@ import { sendTransactionReceivedEmail, } from '@/emails/sendEmailVariants' import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event' -import { eventProtocol } from '@/event/EventProtocolEmitter' +import { writeEvent } from '@/event/EventProtocolEmitter' import { BalanceResolver } from './BalanceResolver' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' @@ -39,6 +39,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, @@ -141,16 +143,14 @@ export const executeTransaction = async ( eventTransactionSend.xUserId = transactionSend.linkedUserId eventTransactionSend.transactionId = transactionSend.id eventTransactionSend.amount = transactionSend.amount.mul(-1) - await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend)) + await writeEvent(new Event().setEventTransactionSend(eventTransactionSend)) 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 writeEvent(new Event().setEventTransactionReceive(eventTransactionReceive)) } catch (e) { await queryRunner.rollbackTransaction() throw new LogError('Transaction was not successful', e) @@ -204,10 +204,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.ts b/backend/src/graphql/resolver/UserResolver.ts index d92958009..5ebf5b445 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -48,7 +48,7 @@ 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 { writeEvent } from '@/event/EventProtocolEmitter' import { Event, EventLogin, @@ -179,7 +179,7 @@ export class UserResolver { }) const ev = new EventLogin() ev.userId = user.id - eventProtocol.writeEvent(new Event().setEventLogin(ev)) + writeEvent(new Event().setEventLogin(ev)) logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`) return user } @@ -251,9 +251,7 @@ export class UserResolver { }) const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail() eventSendAccountMultiRegistrationEmail.userId = foundUser.id - eventProtocol.writeEvent( - event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail), - ) + writeEvent(event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail)) logger.info( `sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`, ) @@ -336,7 +334,7 @@ export class UserResolver { }) logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`) eventSendConfirmEmail.userId = dbUser.id - eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail)) + writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail)) if (!emailSent) { logger.debug(`Account confirmation link: ${activationLink}`) @@ -354,10 +352,10 @@ export class UserResolver { if (redeemCode) { eventRedeemRegister.userId = dbUser.id - await eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister)) + await writeEvent(event.setEventRedeemRegister(eventRedeemRegister)) } else { eventRegister.userId = dbUser.id - await eventProtocol.writeEvent(event.setEventRegister(eventRegister)) + await writeEvent(event.setEventRegister(eventRegister)) } return new User(dbUser) @@ -477,7 +475,7 @@ export class UserResolver { const eventActivateAccount = new EventActivateAccount() eventActivateAccount.userId = user.id - eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount)) + writeEvent(event.setEventActivateAccount(eventActivateAccount)) } catch (e) { await queryRunner.rollbackTransaction() throw new LogError('Error on writing User and User Contact data', e) @@ -824,9 +822,7 @@ export class UserResolver { const event = new Event() const eventSendConfirmationEmail = new EventSendConfirmationEmail() eventSendConfirmationEmail.userId = user.id - await eventProtocol.writeEvent( - event.setEventSendConfirmationEmail(eventSendConfirmationEmail), - ) + await writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmationEmail)) } 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/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 @@