diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bb812c7f4..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 ###################################################### ############################################################################## 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/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/package.json b/backend/package.json index 320f4ded6..497c4d82d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", 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/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index 50f9b0414..f60ab45d0 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -16,6 +16,7 @@ import { redeemTransactionLink, createContribution, updateContribution, + createTransactionLink, } from '@/seeds/graphql/mutations' import { listTransactionLinksAdmin } from '@/seeds/graphql/queries' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' @@ -24,6 +25,7 @@ import { UnconfirmedContribution } from '@model/UnconfirmedContribution' import Decimal from 'decimal.js-light' import { GraphQLError } from 'graphql' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' +import { logger } from '@test/testSetup' // mock semaphore to allow use fake timers jest.mock('@/util/TRANSACTIONS_LOCK') @@ -50,7 +52,75 @@ afterAll(async () => { }) describe('TransactionLinkResolver', () => { + describe('createTransactionLink', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + }) + + it('throws error when amount is zero', async () => { + jest.clearAllMocks() + await expect( + mutate({ + mutation: createTransactionLink, + variables: { + amount: 0, + memo: 'Test', + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('Amount must be a positive number')], + }) + }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0)) + }) + + it('throws error when amount is negative', async () => { + jest.clearAllMocks() + await expect( + mutate({ + mutation: createTransactionLink, + variables: { + amount: -10, + memo: 'Test', + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('Amount must be a positive number')], + }) + }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10)) + }) + + it('throws error when user has not enough GDD', async () => { + jest.clearAllMocks() + await expect( + mutate({ + mutation: createTransactionLink, + variables: { + amount: 1001, + memo: 'Test', + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('User has not enough GDD')], + }) + }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number)) + }) + }) + describe('redeemTransactionLink', () => { + afterAll(async () => { + await cleanDB() + resetToken() + }) + describe('contributionLink', () => { describe('input not valid', () => { beforeAll(async () => { @@ -61,6 +131,7 @@ describe('TransactionLinkResolver', () => { }) it('throws error when link does not exists', async () => { + jest.clearAllMocks() await expect( mutate({ mutation: redeemTransactionLink, @@ -69,16 +140,26 @@ describe('TransactionLinkResolver', () => { }, }), ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: No contribution link found to given code: CL-123456', - ), - ], + errors: [new GraphQLError('Creation from contribution link was not successful')], }) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'No contribution link found to given code', + 'CL-123456', + ) + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error('No contribution link found to given code'), + ) + }) + + const now = new Date() + const validFrom = new Date(now.getFullYear() + 1, 0, 1) + it('throws error when link is not valid yet', async () => { - const now = new Date() + jest.clearAllMocks() const { data: { createContributionLink: contributionLink }, } = await mutate({ @@ -88,7 +169,7 @@ describe('TransactionLinkResolver', () => { name: 'Daily Contribution Link', memo: 'Thank you for contribute daily to the community', cycle: 'DAILY', - validFrom: new Date(now.getFullYear() + 1, 0, 1).toISOString(), + validFrom: validFrom.toISOString(), validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(), maxAmountPerMonth: new Decimal(200), maxPerCycle: 1, @@ -102,16 +183,21 @@ describe('TransactionLinkResolver', () => { }, }), ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: Contribution link not valid yet', - ), - ], + errors: [new GraphQLError('Creation from contribution link was not successful')], }) await resetEntity(DbContributionLink) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom) + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error('Contribution link is not valid yet'), + ) + }) + it('throws error when contributionLink cycle is invalid', async () => { + jest.clearAllMocks() const now = new Date() const { data: { createContributionLink: contributionLink }, @@ -136,17 +222,22 @@ describe('TransactionLinkResolver', () => { }, }), ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: Contribution link has unknown cycle', - ), - ], + errors: [new GraphQLError('Creation from contribution link was not successful')], }) await resetEntity(DbContributionLink) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID') + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error('Contribution link has unknown cycle'), + ) + }) + + const validTo = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 0) it('throws error when link is no longer valid', async () => { - const now = new Date() + jest.clearAllMocks() const { data: { createContributionLink: contributionLink }, } = await mutate({ @@ -157,7 +248,7 @@ describe('TransactionLinkResolver', () => { memo: 'Thank you for contribute daily to the community', cycle: 'DAILY', validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(), - validTo: new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 999).toISOString(), + validTo: validTo.toISOString(), maxAmountPerMonth: new Decimal(200), maxPerCycle: 1, }, @@ -170,14 +261,18 @@ describe('TransactionLinkResolver', () => { }, }), ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: Contribution link is no longer valid', - ), - ], + errors: [new GraphQLError('Creation from contribution link was not successful')], }) await resetEntity(DbContributionLink) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo) + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error('Contribution link is no longer valid'), + ) + }) }) // TODO: have this test separated into a transactionLink and a contributionLink part @@ -250,6 +345,7 @@ describe('TransactionLinkResolver', () => { }) it('does not allow the user to redeem the contribution link', async () => { + jest.clearAllMocks() await expect( mutate({ mutation: redeemTransactionLink, @@ -258,13 +354,18 @@ describe('TransactionLinkResolver', () => { }, }), ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.', - ), - ], + errors: [new GraphQLError('Creation from contribution link was not successful')], }) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error( + 'The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.', + ), + ) + }) }) describe('user has no pending contributions that would not allow to redeem the link', () => { @@ -301,6 +402,7 @@ describe('TransactionLinkResolver', () => { }) it('does not allow the user to redeem the contribution link a second time on the same day', async () => { + jest.clearAllMocks() await expect( mutate({ mutation: redeemTransactionLink, @@ -309,14 +411,17 @@ describe('TransactionLinkResolver', () => { }, }), ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: Contribution link already redeemed today', - ), - ], + errors: [new GraphQLError('Creation from contribution link was not successful')], }) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error('Contribution link already redeemed today'), + ) + }) + describe('after one day', () => { beforeAll(async () => { jest.useFakeTimers() @@ -349,6 +454,7 @@ describe('TransactionLinkResolver', () => { }) it('does not allow the user to redeem the contribution link a second time on the same day', async () => { + jest.clearAllMocks() await expect( mutate({ mutation: redeemTransactionLink, @@ -357,33 +463,65 @@ describe('TransactionLinkResolver', () => { }, }), ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: Contribution link already redeemed today', - ), - ], + errors: [new GraphQLError('Creation from contribution link was not successful')], }) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error('Contribution link already redeemed today'), + ) + }) }) }) }) }) + }) - describe('transaction links list', () => { - const variables = { - userId: 1, // dummy, may be replaced - filters: null, - currentPage: 1, - pageSize: 5, - } + describe('listTransactionLinksAdmin', () => { + const variables = { + userId: 1, // dummy, may be replaced + filters: null, + currentPage: 1, + pageSize: 5, + } - // TODO: there is a test not cleaning up after itself! Fix it! - beforeAll(async () => { - await cleanDB() - resetToken() + afterAll(async () => { + await cleanDB() + resetToken() + }) + + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables, + }), + ).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() + }) - describe('unauthenticated', () => { it('returns an error', async () => { await expect( query({ @@ -398,22 +536,40 @@ describe('TransactionLinkResolver', () => { }) }) - describe('authenticated', () => { - describe('without admin rights', () => { - beforeAll(async () => { - user = await userFactory(testEnv, bibiBloxberg) - await mutate({ - mutation: login, - variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, - }) - }) + describe('with admin rights', () => { + beforeAll(async () => { + // admin 'peter@lustig.de' has to exists for 'creationFactory' + await userFactory(testEnv, peterLustig) - afterAll(async () => { - await cleanDB() - resetToken() - }) + user = await userFactory(testEnv, bibiBloxberg) + variables.userId = user.id + variables.pageSize = 25 + // bibi needs GDDs + const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await creationFactory(testEnv, bibisCreation!) + // bibis transaktion links + const bibisTransaktionLinks = transactionLinks.filter( + (transactionLink) => transactionLink.email === 'bibi@bloxberg.de', + ) + for (let i = 0; i < bibisTransaktionLinks.length; i++) { + await transactionLinkFactory(testEnv, bibisTransaktionLinks[i]) + } - it('returns an error', async () => { + // admin: only now log in + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) + + describe('without any filters', () => { + it('finds 6 open transaction links and no deleted or redeemed', async () => { await expect( query({ query: listTransactionLinksAdmin, @@ -421,219 +577,169 @@ describe('TransactionLinkResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], + data: { + listTransactionLinksAdmin: { + linkCount: 6, + linkList: expect.not.arrayContaining([ + expect.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, }), ) }) }) - describe('with admin rights', () => { - beforeAll(async () => { - // admin 'peter@lustig.de' has to exists for 'creationFactory' - await userFactory(testEnv, peterLustig) - - user = await userFactory(testEnv, bibiBloxberg) - variables.userId = user.id - variables.pageSize = 25 - // bibi needs GDDs - const bibisCreation = creations.find( - (creation) => creation.email === 'bibi@bloxberg.de', + describe('all filters are null', () => { + it('finds 6 open transaction links and no deleted or redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withDeleted: null, + withExpired: null, + withRedeemed: null, + }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 6, + linkList: expect.not.arrayContaining([ + expect.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, + }), ) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await creationFactory(testEnv, bibisCreation!) - // bibis transaktion links - const bibisTransaktionLinks = transactionLinks.filter( - (transactionLink) => transactionLink.email === 'bibi@bloxberg.de', + }) + }) + + describe('filter with deleted', () => { + it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withDeleted: true, + }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 7, + linkList: expect.arrayContaining([ + expect.not.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, + }), ) - for (let i = 0; i < bibisTransaktionLinks.length; i++) { - await transactionLinkFactory(testEnv, bibisTransaktionLinks[i]) - } - - // admin: only now log in - await mutate({ - mutation: login, - variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, - }) }) + }) - afterAll(async () => { - await cleanDB() - resetToken() + describe('filter by expired', () => { + it('finds 5 open transaction links, 1 expired, and no redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withExpired: true, + }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 7, + linkList: expect.arrayContaining([ + expect.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.not.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, + }), + ) }) + }) - describe('without any filters', () => { - it('finds 6 open transaction links and no deleted or redeemed', async () => { - await expect( - query({ - query: listTransactionLinksAdmin, - variables, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listTransactionLinksAdmin: { - linkCount: 6, - linkList: expect.not.arrayContaining([ - expect.objectContaining({ - memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', - createdAt: expect.any(String), - }), - expect.objectContaining({ - memo: 'Da habe ich mich wohl etwas übernommen.', - deletedAt: expect.any(String), - }), - ]), - }, + // TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory + describe.skip('filter by redeemed', () => { + it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withDeleted: null, + withExpired: null, + withRedeemed: true, }, - }), - ) - }) - }) - - describe('all filters are null', () => { - it('finds 6 open transaction links and no deleted or redeemed', async () => { - await expect( - query({ - query: listTransactionLinksAdmin, - variables: { - ...variables, - filters: { - withDeleted: null, - withExpired: null, - withRedeemed: null, - }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 6, + linkList: expect.arrayContaining([ + expect.not.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Yeah, eingelöst!', + redeemedAt: expect.any(String), + redeemedBy: expect.any(Number), + }), + expect.not.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listTransactionLinksAdmin: { - linkCount: 6, - linkList: expect.not.arrayContaining([ - expect.objectContaining({ - memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', - createdAt: expect.any(String), - }), - expect.objectContaining({ - memo: 'Da habe ich mich wohl etwas übernommen.', - deletedAt: expect.any(String), - }), - ]), - }, - }, - }), - ) - }) - }) - - describe('filter with deleted', () => { - it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => { - await expect( - query({ - query: listTransactionLinksAdmin, - variables: { - ...variables, - filters: { - withDeleted: true, - }, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listTransactionLinksAdmin: { - linkCount: 7, - linkList: expect.arrayContaining([ - expect.not.objectContaining({ - memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', - createdAt: expect.any(String), - }), - expect.objectContaining({ - memo: 'Da habe ich mich wohl etwas übernommen.', - deletedAt: expect.any(String), - }), - ]), - }, - }, - }), - ) - }) - }) - - describe('filter by expired', () => { - it('finds 5 open transaction links, 1 expired, and no redeemed', async () => { - await expect( - query({ - query: listTransactionLinksAdmin, - variables: { - ...variables, - filters: { - withExpired: true, - }, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listTransactionLinksAdmin: { - linkCount: 7, - linkList: expect.arrayContaining([ - expect.objectContaining({ - memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', - createdAt: expect.any(String), - }), - expect.not.objectContaining({ - memo: 'Da habe ich mich wohl etwas übernommen.', - deletedAt: expect.any(String), - }), - ]), - }, - }, - }), - ) - }) - }) - - // TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory - describe.skip('filter by redeemed', () => { - it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => { - await expect( - query({ - query: listTransactionLinksAdmin, - variables: { - ...variables, - filters: { - withDeleted: null, - withExpired: null, - withRedeemed: true, - }, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listTransactionLinksAdmin: { - linkCount: 6, - linkList: expect.arrayContaining([ - expect.not.objectContaining({ - memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', - createdAt: expect.any(String), - }), - expect.objectContaining({ - memo: 'Yeah, eingelöst!', - redeemedAt: expect.any(String), - redeemedBy: expect.any(Number), - }), - expect.not.objectContaining({ - memo: 'Da habe ich mich wohl etwas übernommen.', - deletedAt: expect.any(String), - }), - ]), - }, - }, - }), - ) - }) + }, + }), + ) }) }) }) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index b3376c65f..4647dde60 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -32,6 +32,7 @@ import { getUserCreation, validateContribution } from './util/creations' import { executeTransaction } from './TransactionResolver' import QueryLinkResult from '@union/QueryLinkResult' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' +import LogError from '@/server/LogError' import { getLastTransaction } from './util/getLastTransaction' @@ -65,12 +66,16 @@ export class TransactionLinkResolver { const createdDate = new Date() const validUntil = transactionLinkExpireDate(createdDate) + if (amount.lessThanOrEqualTo(0)) { + throw new LogError('Amount must be a positive number', amount) + } + const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay) // validate amount const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate) if (!sendBalance) { - throw new Error("user hasn't enough GDD or amount is < 0") + throw new LogError('User has not enough GDD', user.id) } const transactionLink = DbTransactionLink.create() @@ -186,24 +191,15 @@ export class TransactionLinkResolver { .where('contributionLink.code = :code', { code: code.replace('CL-', '') }) .getOne() if (!contributionLink) { - logger.error('no contribution link found to given code:', code) - throw new Error(`No contribution link found to given code: ${code}`) + throw new LogError('No contribution link found to given code', code) } logger.info('...contribution link found with id', contributionLink.id) if (new Date(contributionLink.validFrom).getTime() > now.getTime()) { - logger.error( - 'contribution link is not valid yet. Valid from: ', - contributionLink.validFrom, - ) - throw new Error('Contribution link not valid yet') + throw new LogError('Contribution link is not valid yet', contributionLink.validFrom) } if (contributionLink.validTo) { if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) { - logger.error( - 'contribution link is no longer valid. Valid to: ', - contributionLink.validTo, - ) - throw new Error('Contribution link is no longer valid') + throw new LogError('Contribution link is no longer valid', contributionLink.validTo) } } let alreadyRedeemed: DbContribution | undefined @@ -219,11 +215,7 @@ export class TransactionLinkResolver { }) .getOne() if (alreadyRedeemed) { - logger.error( - 'contribution link with rule ONCE already redeemed by user with id', - user.id, - ) - throw new Error('Contribution link already redeemed') + throw new LogError('Contribution link already redeemed', user.id) } break } @@ -248,17 +240,12 @@ export class TransactionLinkResolver { ) .getOne() if (alreadyRedeemed) { - logger.error( - 'contribution link with rule DAILY already redeemed by user with id', - user.id, - ) - throw new Error('Contribution link already redeemed today') + throw new LogError('Contribution link already redeemed today', user.id) } break } default: { - logger.error('contribution link has unknown cycle', contributionLink.cycle) - throw new Error('Contribution link has unknown cycle') + throw new LogError('Contribution link has unknown cycle', contributionLink.cycle) } } @@ -308,8 +295,7 @@ export class TransactionLinkResolver { logger.info('creation from contribution link commited successfuly.') } catch (e) { await queryRunner.rollbackTransaction() - logger.error(`Creation from contribution link was not successful: ${e}`) - throw new Error(`Creation from contribution link was not successful. ${e}`) + throw new LogError('Creation from contribution link was not successful', e) } finally { await queryRunner.release() } diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index 304ae2adc..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,6 +14,11 @@ "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" diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index bdc92b2cf..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,6 +14,11 @@ "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" 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 @@