Merge branch 'master' into logger_creations

This commit is contained in:
Ulf Gebhardt 2023-02-14 20:14:16 +01:00 committed by GitHub
commit ea42ac6a40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 1180 additions and 1746 deletions

View File

@ -360,6 +360,25 @@ jobs:
- name: backend | Lint
run: docker run --rm gradido/backend:test yarn run lint
##############################################################################
# JOB: LOCALES BACKEND #######################################################
##############################################################################
locales_backend:
name: Locales - Backend
runs-on: ubuntu-latest
needs: [build_test_backend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# LOCALES BACKEND #####################################################
##########################################################################
- name: Backend | Locales
run: cd backend && yarn && yarn locales
##############################################################################
# JOB: LINT DATABASE UP ######################################################
##############################################################################
@ -526,7 +545,7 @@ jobs:
report_name: Coverage Backend
type: lcov
result_path: ./backend/coverage/lcov.info
min_coverage: 78
min_coverage: 80
token: ${{ github.token }}
##########################################################################

View File

@ -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)

View File

@ -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

View File

@ -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"
]
}
}

View File

@ -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({})
})
})
})
})

View File

@ -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)
})
})
})
})

View File

@ -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', () => {

View File

@ -9,9 +9,7 @@
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav>
<b-nav-item to="/">{{ $t('navbar.overview') }}</b-nav-item>
<b-nav-item to="/user">{{ $t('navbar.user_search') }}</b-nav-item>
<b-nav-item to="/creation">{{ $t('navbar.multi_creation') }}</b-nav-item>
<b-nav-item
v-show="$store.state.openCreations > 0"
class="bg-color-creation p-1"

View File

@ -1,35 +0,0 @@
<template>
<div class="component-select-users-table">
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
<template #cell(bookmark)="row">
<div>
<b-button
v-if="row.item.emailChecked"
variant="warning"
size="md"
@click="$emit('push-item', row.item)"
class="mr-2"
>
<b-icon icon="plus" variant="success"></b-icon>
</b-button>
<div v-else>{{ $t('e_mail') }}{{ $t('math.exclaim') }}</div>
</div>
</template>
</b-table-lite>
</div>
</template>
<script>
export default {
name: 'SelectUsersTable',
props: {
items: {
type: Array,
required: true,
},
fields: {
type: Array,
required: true,
},
},
}
</script>

View File

@ -1,26 +0,0 @@
<template>
<div class="component-selected-users-table">
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
<template #cell(bookmark)="row">
<b-button variant="danger" size="md" @click="$emit('remove-item', row.item)" class="mr-2">
<b-icon icon="x" variant="light"></b-icon>
</b-button>
</template>
</b-table-lite>
</div>
</template>
<script>
export default {
name: 'SelectedUsersTable',
props: {
items: {
type: Array,
required: true,
},
fields: {
type: Array,
required: true,
},
},
}
</script>

View File

@ -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",

View File

@ -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",

View File

@ -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,
}),
)
})
})

View File

@ -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',
)
})
})
})
})

View File

@ -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')
})
})
})
})

View File

@ -1,200 +0,0 @@
<template>
<div class="creation">
<b-row>
<b-col cols="12" lg="6">
<label>{{ $t('user_search') }}</label>
<b-input-group>
<b-form-input
type="text"
class="test-input-criteria"
v-model="criteria"
:placeholder="$t('user_search')"
></b-form-input>
<b-input-group-append class="test-click-clear-criteria" @click="criteria = ''">
<b-input-group-text class="pointer">
<b-icon icon="x" />
</b-input-group-text>
</b-input-group-append>
</b-input-group>
<select-users-table
v-if="itemsList.length > 0"
:items="itemsList"
:fields="Searchfields"
@push-item="pushItem"
/>
<b-pagination
pills
v-model="currentPage"
per-page="perPage"
:total-rows="rows"
align="center"
:hide-ellipsis="true"
></b-pagination>
</b-col>
<b-col cols="12" lg="6" class="shadow p-3 mb-5 rounded bg-info">
<div v-show="itemsMassCreation.length > 0">
<div class="text-right pr-4 mb-1">
<b-button @click="removeAllBookmarks()" variant="light">
<b-icon icon="x" scale="2" variant="danger"></b-icon>
{{ $t('remove_all') }}
</b-button>
</div>
<selected-users-table
class="shadow p-3 mb-5 bg-white rounded"
:items="itemsMassCreation"
:fields="fields"
@remove-item="removeItem"
/>
</div>
<div v-if="itemsMassCreation.length === 0">
{{ $t('multiple_creation_text') }}
</div>
<creation-formular
v-else
type="massCreation"
:creation="creation"
:items="itemsMassCreation"
@remove-all-bookmark="removeAllBookmarks"
@toast-failed-creations="toastFailedCreations"
/>
</b-col>
</b-row>
</div>
</template>
<script>
import CreationFormular from '../components/CreationFormular.vue'
import SelectUsersTable from '../components/Tables/SelectUsersTable.vue'
import SelectedUsersTable from '../components/Tables/SelectedUsersTable.vue'
import { searchUsers } from '../graphql/searchUsers'
import { creationMonths } from '../mixins/creationMonths'
export default {
name: 'Creation',
mixins: [creationMonths],
components: {
CreationFormular,
SelectUsersTable,
SelectedUsersTable,
},
data() {
return {
showArrays: false,
itemsList: [],
itemsMassCreation: this.$store.state.userSelectedInMassCreation,
radioSelectedMass: '',
criteria: '',
rows: 0,
currentPage: 1,
perPage: 25,
now: Date.now(),
}
},
async created() {
await this.getUsers()
},
methods: {
async getUsers() {
this.$apollo
.query({
query: searchUsers,
variables: {
searchText: this.criteria,
currentPage: this.currentPage,
pageSize: this.perPage,
filters: {
byActivated: true,
byDeleted: false,
},
},
fetchPolicy: 'network-only',
})
.then((result) => {
this.rows = result.data.searchUsers.userCount
this.itemsList = result.data.searchUsers.userList.map((user) => {
return {
...user,
showDetails: false,
}
})
if (this.itemsMassCreation.length !== 0) {
const selectedIndices = this.itemsMassCreation.map((item) => item.userId)
this.itemsList = this.itemsList.filter((item) => !selectedIndices.includes(item.userId))
}
})
.catch((error) => {
this.toastError(error.message)
})
},
pushItem(selectedItem) {
this.itemsMassCreation = [
this.itemsList.find((item) => selectedItem.userId === item.userId),
...this.itemsMassCreation,
]
this.itemsList = this.itemsList.filter((item) => selectedItem.userId !== item.userId)
this.$store.commit('setUserSelectedInMassCreation', this.itemsMassCreation)
},
removeItem(selectedItem) {
this.itemsList = [
this.itemsMassCreation.find((item) => selectedItem.userId === item.userId),
...this.itemsList,
]
this.itemsMassCreation = this.itemsMassCreation.filter(
(item) => selectedItem.userId !== item.userId,
)
this.$store.commit('setUserSelectedInMassCreation', this.itemsMassCreation)
},
removeAllBookmarks() {
this.itemsMassCreation = []
this.$store.commit('setUserSelectedInMassCreation', [])
this.getUsers()
},
toastFailedCreations(failedCreations) {
failedCreations.forEach((email) =>
this.toastError(this.$t('creation_form.creation_failed', { email })),
)
},
},
computed: {
Searchfields() {
return [
{ key: 'bookmark', label: 'bookmark' },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'creation',
label: this.creationLabel,
formatter: (value, key, item) => {
return value.join(' | ')
},
},
{ key: 'email', label: this.$t('e_mail') },
]
},
fields() {
return [
{ key: 'email', label: this.$t('e_mail') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'creation',
label: this.creationLabel,
formatter: (value, key, item) => {
return value.join(' | ')
},
},
{ key: 'bookmark', label: this.$t('remove') },
]
},
},
watch: {
currentPage() {
this.getUsers()
},
criteria() {
this.getUsers()
},
},
}
</script>

View File

@ -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', () => {

View File

@ -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') },
]
},

View File

@ -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()

View File

@ -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'),

View File

@ -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

View File

@ -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

View File

@ -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"
]
}
}

25
backend/scripts/sort.sh Executable file
View File

@ -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

View File

@ -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)

View File

@ -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,
}

View File

@ -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 <peter@lustig.de>',
},
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 <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
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('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Your common good contribution was deleted</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Your common good contribution was deleted</h1>',
)
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: <a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
)
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
})
})
})
describe('sendResetPasswordEmail', () => {
beforeAll(async () => {
result = await sendResetPasswordEmail({

View File

@ -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<Record<string, unknown> | 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

View File

@ -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

View File

@ -0,0 +1 @@
= t('emails.contributionDeleted.subject')

View File

@ -1,517 +1,212 @@
import { EventProtocol } from '@entity/EventProtocol'
import decimal from 'decimal.js-light'
import { EventProtocol as DbEvent } from '@entity/EventProtocol'
import Decimal from 'decimal.js-light'
import { EventProtocolType } from './EventProtocolType'
export class EventBasic {
type: string
createdAt: Date
}
export class EventBasicUserId extends EventBasic {
userId: number
export const Event = (
type: EventProtocolType,
userId: number,
xUserId: number | null = null,
xCommunityId: number | null = null,
transactionId: number | null = null,
contributionId: number | null = null,
amount: Decimal | null = null,
messageId: number | null = null,
): DbEvent => {
const event = new DbEvent()
event.type = type
event.userId = userId
event.xUserId = xUserId
event.xCommunityId = xCommunityId
event.transactionId = transactionId
event.contributionId = contributionId
event.amount = amount
event.messageId = messageId
return event
}
export class EventBasicTx extends EventBasicUserId {
transactionId: number
amount: decimal
}
export class EventBasicTxX extends EventBasicTx {
xUserId: number
xCommunityId: number
}
export class EventBasicCt extends EventBasicUserId {
contributionId: number
amount: decimal
}
export class EventBasicCtX extends EventBasicCt {
xUserId: number
xCommunityId: number
}
export class EventBasicRedeem extends EventBasicUserId {
transactionId?: number
contributionId?: number
}
export class EventBasicCtMsg extends EventBasicCt {
messageId: number
}
export class EventVisitGradido extends EventBasic {}
export class EventRegister extends EventBasicUserId {}
export class EventRedeemRegister extends EventBasicRedeem {}
export class EventVerifyRedeem extends EventBasicRedeem {}
export class EventInactiveAccount extends EventBasicUserId {}
export class EventSendConfirmationEmail extends EventBasicUserId {}
export class EventSendAccountMultiRegistrationEmail extends EventBasicUserId {}
export class EventSendForgotPasswordEmail extends EventBasicUserId {}
export class EventSendTransactionSendEmail extends EventBasicTxX {}
export class EventSendTransactionReceiveEmail extends EventBasicTxX {}
export class EventSendTransactionLinkRedeemEmail extends EventBasicTxX {}
export class EventSendAddedContributionEmail extends EventBasicCt {}
export class EventSendContributionConfirmEmail extends EventBasicCt {}
export class EventConfirmationEmail extends EventBasicUserId {}
export class EventRegisterEmailKlicktipp extends EventBasicUserId {}
export class EventLogin extends EventBasicUserId {}
export class EventLogout extends EventBasicUserId {}
export class EventRedeemLogin extends EventBasicRedeem {}
export class EventActivateAccount extends EventBasicUserId {}
export class EventPasswordChange extends EventBasicUserId {}
export class EventTransactionSend extends EventBasicTxX {}
export class EventTransactionSendRedeem extends EventBasicTxX {}
export class EventTransactionRepeateRedeem extends EventBasicTxX {}
export class EventTransactionCreation extends EventBasicTx {}
export class EventTransactionReceive extends EventBasicTxX {}
export class EventTransactionReceiveRedeem extends EventBasicTxX {}
export class EventContributionCreate extends EventBasicCt {}
export class EventAdminContributionCreate extends EventBasicCt {}
export class EventAdminContributionDelete extends EventBasicCt {}
export class EventAdminContributionUpdate extends EventBasicCt {}
export class EventUserCreateContributionMessage extends EventBasicCtMsg {}
export class EventAdminCreateContributionMessage extends EventBasicCtMsg {}
export class EventContributionDelete extends EventBasicCt {}
export class EventContributionUpdate extends EventBasicCt {}
export class EventContributionConfirm extends EventBasicCtX {}
export class EventContributionDeny extends EventBasicCtX {}
export class EventContributionLinkDefine extends EventBasicCt {}
export class EventContributionLinkActivateRedeem extends EventBasicCt {}
export class EventDeleteUser extends EventBasicUserId {}
export class EventUndeleteUser extends EventBasicUserId {}
export class EventChangeUserRole extends EventBasicUserId {}
export class EventAdminUpdateContribution extends EventBasicCt {}
export class EventAdminDeleteContribution extends EventBasicCt {}
export class EventCreateContributionLink extends EventBasicCt {}
export class EventDeleteContributionLink extends EventBasicCt {}
export class EventUpdateContributionLink extends EventBasicCt {}
export class Event {
constructor()
constructor(event?: EventProtocol) {
if (event) {
this.id = event.id
this.type = event.type
this.createdAt = event.createdAt
this.userId = event.userId
this.xUserId = event.xUserId
this.xCommunityId = event.xCommunityId
this.transactionId = event.transactionId
this.contributionId = event.contributionId
this.amount = event.amount
}
}
public setEventBasic(): Event {
this.type = EventProtocolType.BASIC
this.createdAt = new Date()
return this
}
public setEventVisitGradido(): Event {
this.setEventBasic()
this.type = EventProtocolType.VISIT_GRADIDO
return this
}
public setEventRegister(ev: EventRegister): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.REGISTER
return this
}
public setEventRedeemRegister(ev: EventRedeemRegister): Event {
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
this.type = EventProtocolType.REDEEM_REGISTER
return this
}
public setEventVerifyRedeem(ev: EventVerifyRedeem): Event {
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
this.type = EventProtocolType.VERIFY_REDEEM
return this
}
public setEventInactiveAccount(ev: EventInactiveAccount): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.INACTIVE_ACCOUNT
return this
}
public setEventSendConfirmationEmail(ev: EventSendConfirmationEmail): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.SEND_CONFIRMATION_EMAIL
return this
}
public setEventSendAccountMultiRegistrationEmail(
ev: EventSendAccountMultiRegistrationEmail,
): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL
return this
}
public setEventSendForgotPasswordEmail(ev: EventSendForgotPasswordEmail): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.SEND_FORGOT_PASSWORD_EMAIL
return this
}
public setEventSendTransactionSendEmail(ev: EventSendTransactionSendEmail): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.SEND_TRANSACTION_SEND_EMAIL
return this
}
public setEventSendTransactionReceiveEmail(ev: EventSendTransactionReceiveEmail): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.SEND_TRANSACTION_RECEIVE_EMAIL
return this
}
public setEventSendTransactionLinkRedeemEmail(ev: EventSendTransactionLinkRedeemEmail): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.SEND_TRANSACTION_LINK_REDEEM_EMAIL
return this
}
public setEventSendAddedContributionEmail(ev: EventSendAddedContributionEmail): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.SEND_ADDED_CONTRIBUTION_EMAIL
return this
}
public setEventSendContributionConfirmEmail(ev: EventSendContributionConfirmEmail): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.SEND_CONTRIBUTION_CONFIRM_EMAIL
return this
}
public setEventConfirmationEmail(ev: EventConfirmationEmail): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.CONFIRM_EMAIL
return this
}
public setEventRegisterEmailKlicktipp(ev: EventRegisterEmailKlicktipp): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.REGISTER_EMAIL_KLICKTIPP
return this
}
public setEventLogin(ev: EventLogin): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.LOGIN
return this
}
public setEventLogout(ev: EventLogout): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.LOGOUT
return this
}
public setEventRedeemLogin(ev: EventRedeemLogin): Event {
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
this.type = EventProtocolType.REDEEM_LOGIN
return this
}
public setEventActivateAccount(ev: EventActivateAccount): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.ACTIVATE_ACCOUNT
return this
}
public setEventPasswordChange(ev: EventPasswordChange): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.PASSWORD_CHANGE
return this
}
public setEventTransactionSend(ev: EventTransactionSend): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.TRANSACTION_SEND
return this
}
public setEventTransactionSendRedeem(ev: EventTransactionSendRedeem): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.TRANSACTION_SEND_REDEEM
return this
}
public setEventTransactionRepeateRedeem(ev: EventTransactionRepeateRedeem): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.TRANSACTION_REPEATE_REDEEM
return this
}
public setEventTransactionCreation(ev: EventTransactionCreation): Event {
this.setByBasicTx(ev.userId, ev.transactionId, ev.amount)
this.type = EventProtocolType.TRANSACTION_CREATION
return this
}
public setEventTransactionReceive(ev: EventTransactionReceive): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.TRANSACTION_RECEIVE
return this
}
public setEventTransactionReceiveRedeem(ev: EventTransactionReceiveRedeem): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.TRANSACTION_RECEIVE_REDEEM
return this
}
public setEventContributionCreate(ev: EventContributionCreate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_CREATE
return this
}
public setEventAdminContributionCreate(ev: EventAdminContributionCreate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_CONTRIBUTION_CREATE
return this
}
public setEventAdminContributionDelete(ev: EventAdminContributionDelete): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_CONTRIBUTION_DELETE
return this
}
public setEventAdminContributionUpdate(ev: EventAdminContributionUpdate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_CONTRIBUTION_UPDATE
return this
}
public setEventUserCreateContributionMessage(ev: EventUserCreateContributionMessage): Event {
this.setByBasicCtMsg(ev.userId, ev.contributionId, ev.amount, ev.messageId)
this.type = EventProtocolType.USER_CREATE_CONTRIBUTION_MESSAGE
return this
}
public setEventAdminCreateContributionMessage(ev: EventAdminCreateContributionMessage): Event {
this.setByBasicCtMsg(ev.userId, ev.contributionId, ev.amount, ev.messageId)
this.type = EventProtocolType.ADMIN_CREATE_CONTRIBUTION_MESSAGE
return this
}
public setEventContributionDelete(ev: EventContributionDelete): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_DELETE
return this
}
public setEventContributionUpdate(ev: EventContributionUpdate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_UPDATE
return this
}
public setEventContributionConfirm(ev: EventContributionConfirm): Event {
this.setByBasicCtX(ev.userId, ev.contributionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.CONTRIBUTION_CONFIRM
return this
}
public setEventContributionDeny(ev: EventContributionDeny): Event {
this.setByBasicCtX(ev.userId, ev.contributionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.CONTRIBUTION_DENY
return this
}
public setEventContributionLinkDefine(ev: EventContributionLinkDefine): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_LINK_DEFINE
return this
}
public setEventContributionLinkActivateRedeem(ev: EventContributionLinkActivateRedeem): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_LINK_ACTIVATE_REDEEM
return this
}
public setEventDeleteUser(ev: EventDeleteUser): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.DELETE_USER
return this
}
public setEventUndeleteUser(ev: EventUndeleteUser): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.UNDELETE_USER
return this
}
public setEventChangeUserRole(ev: EventChangeUserRole): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.CHANGE_USER_ROLE
return this
}
public setEventAdminUpdateContribution(ev: EventAdminUpdateContribution): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_UPDATE_CONTRIBUTION
return this
}
public setEventAdminDeleteContribution(ev: EventAdminDeleteContribution): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_DELETE_CONTRIBUTION
return this
}
public setEventCreateContributionLink(ev: EventCreateContributionLink): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CREATE_CONTRIBUTION_LINK
return this
}
public setEventDeleteContributionLink(ev: EventDeleteContributionLink): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.DELETE_CONTRIBUTION_LINK
return this
}
public setEventUpdateContributionLink(ev: EventUpdateContributionLink): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.UPDATE_CONTRIBUTION_LINK
return this
}
setByBasicUser(userId: number): Event {
this.setEventBasic()
this.userId = userId
return this
}
setByBasicTx(userId: number, transactionId: number, amount: decimal): Event {
this.setByBasicUser(userId)
this.transactionId = transactionId
this.amount = amount
return this
}
setByBasicTxX(
userId: number,
transactionId: number,
amount: decimal,
xUserId: number,
xCommunityId: number,
): Event {
this.setByBasicTx(userId, transactionId, amount)
this.xUserId = xUserId
this.xCommunityId = xCommunityId
return this
}
setByBasicCt(userId: number, contributionId: number, amount: decimal): Event {
this.setByBasicUser(userId)
this.contributionId = contributionId
this.amount = amount
return this
}
setByBasicCtMsg(
userId: number,
contributionId: number,
amount: decimal,
messageId: number,
): Event {
this.setByBasicCt(userId, contributionId, amount)
this.messageId = messageId
return this
}
setByBasicCtX(
userId: number,
contributionId: number,
amount: decimal,
xUserId: number,
xCommunityId: number,
): Event {
this.setByBasicCt(userId, contributionId, amount)
this.xUserId = xUserId
this.xCommunityId = xCommunityId
return this
}
setByBasicRedeem(userId: number, transactionId?: number, contributionId?: number): Event {
this.setByBasicUser(userId)
if (transactionId) this.transactionId = transactionId
if (contributionId) this.contributionId = contributionId
return this
}
id: number
type: string
createdAt: Date
userId: number
xUserId?: number
xCommunityId?: number
transactionId?: number
contributionId?: number
amount?: decimal
messageId?: number
}
export const EVENT_CONTRIBUTION_CREATE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_CREATE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_CONTRIBUTION_DELETE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_DELETE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_CONTRIBUTION_UPDATE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_UPDATE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_CREATE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_CREATE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_UPDATE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_DELETE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_DELETE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_CONTRIBUTION_CONFIRM = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_CONFIRM,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_DENY = async (
userId: number,
xUserId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_DENY,
userId,
xUserId,
null,
null,
contributionId,
amount,
).save()
export const EVENT_TRANSACTION_SEND = async (
userId: number,
xUserId: number,
transactionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.TRANSACTION_SEND,
userId,
xUserId,
null,
transactionId,
null,
amount,
).save()
export const EVENT_TRANSACTION_RECEIVE = async (
userId: number,
xUserId: number,
transactionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.TRANSACTION_RECEIVE,
userId,
xUserId,
null,
transactionId,
null,
amount,
).save()
export const EVENT_LOGIN = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.LOGIN, userId, null, null, null, null, null, null).save()
export const EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = async (
userId: number,
): Promise<DbEvent> => Event(EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL, userId).save()
export const EVENT_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.SEND_CONFIRMATION_EMAIL, userId).save()
export const EVENT_ADMIN_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.ADMIN_SEND_CONFIRMATION_EMAIL, userId).save()
/* export const EVENT_REDEEM_REGISTER = async (
userId: number,
transactionId: number | null = null,
contributionId: number | null = null,
): Promise<Event> =>
Event(
EventProtocolType.REDEEM_REGISTER,
userId,
null,
null,
transactionId,
contributionId,
).save()
*/
export const EVENT_REGISTER = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.REGISTER, userId).save()
export const EVENT_ACTIVATE_ACCOUNT = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.ACTIVATE_ACCOUNT, userId).save()

View File

@ -1,41 +0,0 @@
import { Event } from '@/event/Event'
import { backendLogger as logger } from '@/server/logger'
import { EventProtocol } from '@entity/EventProtocol'
import CONFIG from '@/config'
class EventProtocolEmitter {
/* }extends EventEmitter { */
private events: Event[]
/*
public addEvent(event: Event) {
this.events.push(event)
}
public getEvents(): Event[] {
return this.events
}
*/
public isDisabled() {
logger.info(`EventProtocol - isDisabled=${CONFIG.EVENT_PROTOCOL_DISABLED}`)
return CONFIG.EVENT_PROTOCOL_DISABLED === true
}
public async writeEvent(event: Event): Promise<void> {
if (!eventProtocol.isDisabled()) {
logger.info(`writeEvent(${JSON.stringify(event)})`)
const dbEvent = new EventProtocol()
dbEvent.type = event.type
dbEvent.createdAt = event.createdAt
dbEvent.userId = event.userId
if (event.xUserId) dbEvent.xUserId = event.xUserId
if (event.xCommunityId) dbEvent.xCommunityId = event.xCommunityId
if (event.contributionId) dbEvent.contributionId = event.contributionId
if (event.transactionId) dbEvent.transactionId = event.transactionId
if (event.amount) dbEvent.amount = event.amount
await dbEvent.save()
}
}
}
export const eventProtocol = new EventProtocolEmitter()

View File

@ -1,49 +1,50 @@
export enum EventProtocolType {
BASIC = 'BASIC',
VISIT_GRADIDO = 'VISIT_GRADIDO',
// VISIT_GRADIDO = 'VISIT_GRADIDO',
REGISTER = 'REGISTER',
REDEEM_REGISTER = 'REDEEM_REGISTER',
VERIFY_REDEEM = 'VERIFY_REDEEM',
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
// VERIFY_REDEEM = 'VERIFY_REDEEM',
// INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
ADMIN_SEND_CONFIRMATION_EMAIL = 'ADMIN_SEND_CONFIRMATION_EMAIL',
SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTIREGISTRATION_EMAIL',
CONFIRM_EMAIL = 'CONFIRM_EMAIL',
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
// CONFIRM_EMAIL = 'CONFIRM_EMAIL',
// REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
LOGIN = 'LOGIN',
LOGOUT = 'LOGOUT',
REDEEM_LOGIN = 'REDEEM_LOGIN',
// LOGOUT = 'LOGOUT',
// REDEEM_LOGIN = 'REDEEM_LOGIN',
ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT',
SEND_FORGOT_PASSWORD_EMAIL = 'SEND_FORGOT_PASSWORD_EMAIL',
PASSWORD_CHANGE = 'PASSWORD_CHANGE',
SEND_TRANSACTION_SEND_EMAIL = 'SEND_TRANSACTION_SEND_EMAIL',
SEND_TRANSACTION_RECEIVE_EMAIL = 'SEND_TRANSACTION_RECEIVE_EMAIL',
// SEND_FORGOT_PASSWORD_EMAIL = 'SEND_FORGOT_PASSWORD_EMAIL',
// PASSWORD_CHANGE = 'PASSWORD_CHANGE',
// SEND_TRANSACTION_SEND_EMAIL = 'SEND_TRANSACTION_SEND_EMAIL',
// SEND_TRANSACTION_RECEIVE_EMAIL = 'SEND_TRANSACTION_RECEIVE_EMAIL',
TRANSACTION_SEND = 'TRANSACTION_SEND',
TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM',
TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM',
TRANSACTION_CREATION = 'TRANSACTION_CREATION',
// TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM',
// TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM',
// TRANSACTION_CREATION = 'TRANSACTION_CREATION',
TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE',
TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM',
SEND_TRANSACTION_LINK_REDEEM_EMAIL = 'SEND_TRANSACTION_LINK_REDEEM_EMAIL',
SEND_ADDED_CONTRIBUTION_EMAIL = 'SEND_ADDED_CONTRIBUTION_EMAIL',
SEND_CONTRIBUTION_CONFIRM_EMAIL = 'SEND_CONTRIBUTION_CONFIRM_EMAIL',
// TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM',
// SEND_TRANSACTION_LINK_REDEEM_EMAIL = 'SEND_TRANSACTION_LINK_REDEEM_EMAIL',
// SEND_ADDED_CONTRIBUTION_EMAIL = 'SEND_ADDED_CONTRIBUTION_EMAIL',
// SEND_CONTRIBUTION_CONFIRM_EMAIL = 'SEND_CONTRIBUTION_CONFIRM_EMAIL',
CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE',
CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM',
CONTRIBUTION_DENY = 'CONTRIBUTION_DENY',
CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE',
CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM',
// CONTRIBUTION_DENY = 'CONTRIBUTION_DENY',
// CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE',
// CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM',
CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE',
CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE',
ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE',
ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE',
ADMIN_CONTRIBUTION_DENY = 'ADMIN_CONTRIBUTION_DENY',
ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE',
USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE',
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
DELETE_USER = 'DELETE_USER',
UNDELETE_USER = 'UNDELETE_USER',
CHANGE_USER_ROLE = 'CHANGE_USER_ROLE',
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
// USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE',
// ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
// DELETE_USER = 'DELETE_USER',
// UNDELETE_USER = 'UNDELETE_USER',
// CHANGE_USER_ROLE = 'CHANGE_USER_ROLE',
// ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
// ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
// CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
// DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
// UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
}

View File

@ -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}`)

View File

@ -313,27 +313,6 @@ describe('Contribution Links', () => {
)
})
it('returns an error if name is an empty string', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createContributionLink,
variables: {
...variables,
name: '',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('The name must be initialized')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('The name must be initialized')
})
it('returns an error if name is shorter than 5 characters', async () => {
jest.clearAllMocks()
await expect(
@ -376,27 +355,6 @@ describe('Contribution Links', () => {
expect(logger.error).toBeCalledWith('The value of name is too long', 101)
})
it('returns an error if memo is an empty string', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createContributionLink,
variables: {
...variables,
memo: '',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('The memo must be initialized')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('The memo must be initialized')
})
it('returns an error if memo is shorter than 5 characters', async () => {
jest.clearAllMocks()
await expect(

View File

@ -40,20 +40,12 @@ export class ContributionLinkResolver {
}: ContributionLinkArgs,
): Promise<ContributionLink> {
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)
}

View File

@ -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'),
)
})

View File

@ -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()
}

View File

@ -22,11 +22,7 @@ import {
listContributions,
listUnconfirmedContributions,
} from '@/seeds/graphql/queries'
import {
// sendAccountActivationEmail,
sendContributionConfirmedEmail,
// sendContributionRejectedEmail,
} from '@/emails/sendEmailVariants'
import { sendContributionConfirmedEmail } from '@/emails/sendEmailVariants'
import {
cleanDB,
resetToken,
@ -48,7 +44,6 @@ import { logger, i18n as localization } from '@test/testSetup'
import { UserInputError } from 'apollo-server-express'
import { ContributionStatus } from '../enum/ContributionStatus'
// mock account activation email to avoid console spam
// mock account activation email to avoid console spam
jest.mock('@/emails/sendEmailVariants', () => {
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
@ -249,7 +244,7 @@ describe('ContributionResolver', () => {
)
})
it('stores the create contribution event in the database', async () => {
it('stores the CONTRIBUTION_CREATE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.CONTRIBUTION_CREATE,
@ -536,7 +531,6 @@ describe('ContributionResolver', () => {
})
})
// TODO: why is this here - this is a different call `adminUpdateContribution` not `updateContribution`
describe('admin tries to update a user contribution', () => {
it('throws an error', async () => {
jest.clearAllMocks()
@ -704,7 +698,7 @@ describe('ContributionResolver', () => {
)
})
it('stores the update contribution event in the database', async () => {
it('stores the CONTRIBUTION_UPDATE event in the database', async () => {
bibi = await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
@ -1271,7 +1265,7 @@ describe('ContributionResolver', () => {
).resolves.toBeTruthy()
})
it('stores the delete contribution event in the database', async () => {
it('stores the CONTRIBUTION_DELETE event in the database', async () => {
const contribution = await mutate({
mutation: createContribution,
variables: {
@ -1790,7 +1784,7 @@ describe('ContributionResolver', () => {
)
})
it('stores the admin create contribution event in the database', async () => {
it('stores the ADMIN_CONTRIBUTION_CREATE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_CREATE,
@ -2060,7 +2054,7 @@ describe('ContributionResolver', () => {
)
})
it('stores the admin update contribution event in the database', async () => {
it('stores the ADMIN_CONTRIBUTION_UPDATE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
@ -2100,7 +2094,7 @@ describe('ContributionResolver', () => {
)
})
it('stores the admin update contribution event in the database', async () => {
it('stores the ADMIN_CONTRIBUTION_UPDATE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
@ -2244,7 +2238,7 @@ describe('ContributionResolver', () => {
)
})
it('stores the admin delete contribution event in the database', async () => {
it('stores the ADMIN_CONTRIBUTION_DELETE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_DELETE,
@ -2386,7 +2380,7 @@ describe('ContributionResolver', () => {
)
})
it('stores the contribution confirm event in the database', async () => {
it('stores the CONTRIBUTION_CONFIRM event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.CONTRIBUTION_CONFIRM,
@ -2418,7 +2412,7 @@ describe('ContributionResolver', () => {
})
})
it('stores the send confirmation email event in the database', async () => {
it('stores the SEND_CONFIRMATION_EMAIL event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.SEND_CONFIRMATION_EMAIL,

View File

@ -37,24 +37,26 @@ import {
} from './util/creations'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, FULL_CREATION_AVAILABLE } from './const/const'
import {
Event,
EventContributionCreate,
EventContributionDelete,
EventContributionUpdate,
EventContributionConfirm,
EventAdminContributionCreate,
EventAdminContributionDelete,
EventAdminContributionUpdate,
EVENT_CONTRIBUTION_CREATE,
EVENT_CONTRIBUTION_DELETE,
EVENT_CONTRIBUTION_UPDATE,
EVENT_ADMIN_CONTRIBUTION_CREATE,
EVENT_ADMIN_CONTRIBUTION_UPDATE,
EVENT_ADMIN_CONTRIBUTION_DELETE,
EVENT_CONTRIBUTION_CONFIRM,
EVENT_ADMIN_CONTRIBUTION_DENY,
} from '@/event/Event'
import { eventProtocol } from '@/event/EventProtocolEmitter'
import { calculateDecay } from '@/util/decay'
import {
sendContributionConfirmedEmail,
sendContributionDeletedEmail,
sendContributionDeniedEmail,
} from '@/emails/sendEmailVariants'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction'
@Resolver()
export class ContributionResolver {
@Authorized([RIGHTS.CREATE_CONTRIBUTION])
@ -71,8 +73,6 @@ export class ContributionResolver {
throw new LogError('Memo text is too long', memo.length)
}
const event = new Event()
const user = getUser(context)
const creations = await getUserCreation(user.id, clientTimezoneOffset)
logger.trace('creations', creations)
@ -91,11 +91,7 @@ export class ContributionResolver {
logger.trace('contribution to save', contribution)
await DbContribution.save(contribution)
const eventCreateContribution = new EventContributionCreate()
eventCreateContribution.userId = user.id
eventCreateContribution.amount = amount
eventCreateContribution.contributionId = contribution.id
await eventProtocol.writeEvent(event.setEventContributionCreate(eventCreateContribution))
await EVENT_CONTRIBUTION_CREATE(user.id, contribution.id, amount)
return new UnconfirmedContribution(contribution, user, creations)
}
@ -106,7 +102,6 @@ export class ContributionResolver {
@Arg('id', () => Int) id: number,
@Ctx() context: Context,
): Promise<boolean> {
const event = new Event()
const user = getUser(context)
const contribution = await DbContribution.findOne(id)
if (!contribution) {
@ -124,11 +119,7 @@ export class ContributionResolver {
contribution.deletedAt = new Date()
await contribution.save()
const eventDeleteContribution = new EventContributionDelete()
eventDeleteContribution.userId = user.id
eventDeleteContribution.contributionId = contribution.id
eventDeleteContribution.amount = contribution.amount
await eventProtocol.writeEvent(event.setEventContributionDelete(eventDeleteContribution))
await EVENT_CONTRIBUTION_DELETE(user.id, contribution.id, contribution.amount)
const res = await contribution.softRemove()
return !!res
@ -275,13 +266,7 @@ export class ContributionResolver {
contributionToUpdate.updatedAt = new Date()
DbContribution.save(contributionToUpdate)
const event = new Event()
const eventUpdateContribution = new EventContributionUpdate()
eventUpdateContribution.userId = user.id
eventUpdateContribution.contributionId = contributionId
eventUpdateContribution.amount = amount
await eventProtocol.writeEvent(event.setEventContributionUpdate(eventUpdateContribution))
await EVENT_CONTRIBUTION_UPDATE(user.id, contributionId, amount)
return new UnconfirmedContribution(contributionToUpdate, user, creations)
}
@ -317,7 +302,6 @@ export class ContributionResolver {
)
}
const event = new Event()
const moderator = getUser(context)
logger.trace('moderator: ', moderator.id)
const creations = await getUserCreation(emailContact.userId, clientTimezoneOffset)
@ -339,13 +323,7 @@ export class ContributionResolver {
await DbContribution.save(contribution)
const eventAdminCreateContribution = new EventAdminContributionCreate()
eventAdminCreateContribution.userId = moderator.id
eventAdminCreateContribution.amount = amount
eventAdminCreateContribution.contributionId = contribution.id
await eventProtocol.writeEvent(
event.setEventAdminContributionCreate(eventAdminCreateContribution),
)
await EVENT_ADMIN_CONTRIBUTION_CREATE(moderator.id, contribution.id, amount)
return getUserCreation(emailContact.userId, clientTimezoneOffset)
}
@ -440,14 +418,7 @@ export class ContributionResolver {
result.creation = await getUserCreation(emailContact.user.id, clientTimezoneOffset)
const event = new Event()
const eventAdminContributionUpdate = new EventAdminContributionUpdate()
eventAdminContributionUpdate.userId = emailContact.user.id
eventAdminContributionUpdate.amount = amount
eventAdminContributionUpdate.contributionId = contributionToUpdate.id
await eventProtocol.writeEvent(
event.setEventAdminContributionUpdate(eventAdminContributionUpdate),
)
await EVENT_ADMIN_CONTRIBUTION_UPDATE(emailContact.user.id, contributionToUpdate.id, amount)
return result
}
@ -518,15 +489,9 @@ export class ContributionResolver {
await contribution.save()
const res = await contribution.softRemove()
const event = new Event()
const eventAdminContributionDelete = new EventAdminContributionDelete()
eventAdminContributionDelete.userId = contribution.userId
eventAdminContributionDelete.amount = contribution.amount
eventAdminContributionDelete.contributionId = contribution.id
await eventProtocol.writeEvent(
event.setEventAdminContributionDelete(eventAdminContributionDelete),
)
sendContributionDeniedEmail({
await EVENT_ADMIN_CONTRIBUTION_DELETE(contribution.userId, contribution.id, contribution.amount)
sendContributionDeletedEmail({
firstName: user.firstName,
lastName: user.lastName,
email: user.emailContact.email,
@ -582,16 +547,11 @@ export class ContributionResolver {
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
try {
const lastTransaction = await queryRunner.manager
.createQueryBuilder()
.select('transaction')
.from(DbTransaction, 'transaction')
.where('transaction.userId = :id', { id: contribution.userId })
.orderBy('transaction.id', 'DESC')
.getOne()
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
const lastTransaction = await getLastTransaction(contribution.userId)
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
try {
let newBalance = new Decimal(0)
let decay: Decay | null = null
if (lastTransaction) {
@ -642,12 +602,7 @@ export class ContributionResolver {
await queryRunner.release()
}
const event = new Event()
const eventContributionConfirm = new EventContributionConfirm()
eventContributionConfirm.userId = user.id
eventContributionConfirm.amount = contribution.amount
eventContributionConfirm.contributionId = contribution.id
await eventProtocol.writeEvent(event.setEventContributionConfirm(eventContributionConfirm))
await EVENT_CONTRIBUTION_CONFIRM(user.id, contribution.id, contribution.amount)
} finally {
releaseLock()
}
@ -737,6 +692,13 @@ export class ContributionResolver {
contributionToUpdate.deniedAt = new Date()
const res = await contributionToUpdate.save()
await EVENT_ADMIN_CONTRIBUTION_DENY(
contributionToUpdate.userId,
moderator.id,
contributionToUpdate.id,
contributionToUpdate.amount,
)
sendContributionDeniedEmail({
firstName: user.firstName,
lastName: user.lastName,

View File

@ -116,6 +116,11 @@ describe('TransactionLinkResolver', () => {
})
describe('redeemTransactionLink', () => {
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('contributionLink', () => {
describe('input not valid', () => {
beforeAll(async () => {
@ -487,8 +492,7 @@ describe('TransactionLinkResolver', () => {
pageSize: 5,
}
// TODO: there is a test not cleaning up after itself! Fix it!
beforeAll(async () => {
afterAll(async () => {
await cleanDB()
resetToken()
})

View File

@ -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

View File

@ -330,7 +330,7 @@ describe('send coins', () => {
)
})
it('stores the send transaction event in the database', async () => {
it('stores the TRANSACTION_SEND event in the database', async () => {
// Find the exact transaction (sent one is the one with user[1] as user)
const transaction = await Transaction.find({
userId: user[1].id,
@ -347,7 +347,7 @@ describe('send coins', () => {
)
})
it('stores the receive event in the database', async () => {
it('stores the TRANSACTION_RECEIVE event in the database', async () => {
// Find the exact transaction (received one is the one with user[0] as user)
const transaction = await Transaction.find({
userId: user[0].id,

View File

@ -29,8 +29,7 @@ import {
sendTransactionLinkRedeemedEmail,
sendTransactionReceivedEmail,
} from '@/emails/sendEmailVariants'
import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event'
import { eventProtocol } from '@/event/EventProtocolEmitter'
import { EVENT_TRANSACTION_RECEIVE, EVENT_TRANSACTION_SEND } from '@/event/Event'
import { BalanceResolver } from './BalanceResolver'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
@ -39,6 +38,8 @@ import { findUserByEmail } from './UserResolver'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction'
export const executeTransaction = async (
amount: Decimal,
memo: string,
@ -136,20 +137,18 @@ export const executeTransaction = async (
await queryRunner.commitTransaction()
logger.info(`commit Transaction successful...`)
const eventTransactionSend = new EventTransactionSend()
eventTransactionSend.userId = transactionSend.userId
eventTransactionSend.xUserId = transactionSend.linkedUserId
eventTransactionSend.transactionId = transactionSend.id
eventTransactionSend.amount = transactionSend.amount.mul(-1)
await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend))
await EVENT_TRANSACTION_SEND(
transactionSend.userId,
transactionSend.linkedUserId,
transactionSend.id,
transactionSend.amount.mul(-1),
)
const eventTransactionReceive = new EventTransactionReceive()
eventTransactionReceive.userId = transactionReceive.userId
eventTransactionReceive.xUserId = transactionReceive.linkedUserId
eventTransactionReceive.transactionId = transactionReceive.id
eventTransactionReceive.amount = transactionReceive.amount
await eventProtocol.writeEvent(
new Event().setEventTransactionReceive(eventTransactionReceive),
await EVENT_TRANSACTION_RECEIVE(
transactionReceive.userId,
transactionReceive.linkedUserId,
transactionReceive.id,
transactionReceive.amount,
)
} catch (e) {
await queryRunner.rollbackTransaction()
@ -204,10 +203,7 @@ export class TransactionResolver {
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`)
// find current balance
const lastTransaction = await dbTransaction.findOne(
{ userId: user.id },
{ order: { id: 'DESC' }, relations: ['contribution'] },
)
const lastTransaction = await getLastTransaction(user.id, ['contribution'])
logger.debug(`lastTransaction=${lastTransaction}`)
const balanceResolver = new BalanceResolver()

View File

@ -19,6 +19,7 @@ import {
setUserRole,
deleteUser,
unDeleteUser,
sendActivationEmail,
} from '@/seeds/graphql/mutations'
import { verifyLogin, queryOptIn, searchAdminUsers, searchUsers } from '@/seeds/graphql/queries'
import { GraphQLError } from 'graphql'
@ -175,6 +176,19 @@ describe('UserResolver', () => {
})
})
})
it('stores the REGISTER event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.REGISTER,
userId: userConatct.user.id,
}),
)
})
})
describe('account activation email', () => {
@ -196,7 +210,7 @@ describe('UserResolver', () => {
})
})
it('stores the send confirmation event in the database', () => {
it('stores the SEND_CONFIRMATION_EMAIL event in the database', () => {
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.SEND_CONFIRMATION_EMAIL,
@ -206,7 +220,7 @@ describe('UserResolver', () => {
})
})
describe('email already exists', () => {
describe('user already exists', () => {
let mutation: User
beforeAll(async () => {
mutation = await mutate({ mutation: createUser, variables })
@ -236,6 +250,19 @@ describe('UserResolver', () => {
}),
)
})
it('stores the SEND_ACCOUNT_MULTIREGISTRATION_EMAIL event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL,
userId: userConatct.user.id,
}),
)
})
})
describe('unknown language', () => {
@ -328,7 +355,7 @@ describe('UserResolver', () => {
)
})
it('stores the account activated event in the database', () => {
it('stores the ACTIVATE_ACCOUNT event in the database', () => {
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.ACTIVATE_ACCOUNT,
@ -337,7 +364,7 @@ describe('UserResolver', () => {
)
})
it('stores the redeem register event in the database', () => {
it('stores the REDEEM_REGISTER event in the database', () => {
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.REDEEM_REGISTER,
@ -421,7 +448,7 @@ describe('UserResolver', () => {
)
})
it('stores the redeem register event in the database', async () => {
it('stores the REDEEM_REGISTER event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.REDEEM_REGISTER,
@ -647,6 +674,19 @@ describe('UserResolver', () => {
it('sets the token in the header', () => {
expect(headerPushMock).toBeCalledWith({ key: 'token', value: expect.any(String) })
})
it('stores the LOGIN event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.LOGIN,
userId: userConatct.user.id,
}),
)
})
})
describe('user is in database and wrong password', () => {
@ -887,7 +927,7 @@ describe('UserResolver', () => {
)
})
it('stores the login event in the database', () => {
it('stores the LOGIN event in the database', () => {
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.LOGIN,
@ -1668,6 +1708,157 @@ describe('UserResolver', () => {
})
})
///
describe('sendActivationEmail', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
mutate({ mutation: sendActivationEmail, variables: { email: 'bibi@bloxberg.de' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
describe('without admin rights', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
it('returns an error', async () => {
await expect(
mutate({ mutation: sendActivationEmail, variables: { email: 'bibi@bloxberg.de' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('with admin rights', () => {
beforeAll(async () => {
admin = await userFactory(testEnv, peterLustig)
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('user does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: sendActivationEmail, variables: { email: 'INVALID' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('No user with this credentials')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('No user with this credentials', 'invalid')
})
})
describe('user is deleted', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await userFactory(testEnv, stephenHawking)
await expect(
mutate({ mutation: sendActivationEmail, variables: { email: 'stephen@hawking.uk' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('User with given email contact is deleted')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'User with given email contact is deleted',
'stephen@hawking.uk',
)
})
})
describe('sendActivationEmail with success', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
})
it('returns true', async () => {
const result = await mutate({
mutation: sendActivationEmail,
variables: { email: 'bibi@bloxberg.de' },
})
expect(result).toEqual(
expect.objectContaining({
data: {
sendActivationEmail: true,
},
}),
)
})
it('sends an account activation email', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/{optin}/g,
userConatct.emailVerificationCode.toString(),
).replace(/{code}/g, '')
expect(sendAccountActivationEmail).toBeCalledWith({
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
language: 'de',
activationLink,
timeDurationObject: expect.objectContaining({
hours: expect.any(Number),
minutes: expect.any(Number),
}),
})
})
it('stores the ADMIN_SEND_CONFIRMATION_EMAIL event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.ADMIN_SEND_CONFIRMATION_EMAIL,
userId: userConatct.user.id,
}),
)
})
})
})
})
})
describe('unDelete user', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {

View File

@ -48,15 +48,14 @@ import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddle
import { klicktippSignIn } from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS'
import { hasElopageBuys } from '@/util/hasElopageBuys'
import { eventProtocol } from '@/event/EventProtocolEmitter'
import {
Event,
EventLogin,
EventRedeemRegister,
EventRegister,
EventSendAccountMultiRegistrationEmail,
EventSendConfirmationEmail,
EventActivateAccount,
EVENT_LOGIN,
EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL,
EVENT_SEND_CONFIRMATION_EMAIL,
EVENT_REGISTER,
EVENT_ACTIVATE_ACCOUNT,
EVENT_ADMIN_SEND_CONFIRMATION_EMAIL,
} from '@/event/Event'
import { getUserCreations } from './util/creations'
import { isValidPassword } from '@/password/EncryptorUtils'
@ -64,6 +63,7 @@ import { FULL_CREATION_AVAILABLE } from './const/const'
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
import LogError from '@/server/LogError'
import { EventProtocolType } from '@/event/EventProtocolType'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const sodium = require('sodium-native')
@ -177,9 +177,8 @@ export class UserResolver {
key: 'token',
value: encode(dbUser.gradidoID),
})
const ev = new EventLogin()
ev.userId = user.id
eventProtocol.writeEvent(new Event().setEventLogin(ev))
await EVENT_LOGIN(user.id)
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
return user
}
@ -211,7 +210,6 @@ export class UserResolver {
)
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
// default int publisher_id = 0;
const event = new Event()
// Validate Language (no throw)
if (!language || !isLanguage(language)) {
@ -249,11 +247,9 @@ export class UserResolver {
email,
language: foundUser.language, // use language of the emails owner for sending
})
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
eventSendAccountMultiRegistrationEmail.userId = foundUser.id
eventProtocol.writeEvent(
event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail),
)
await EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL(foundUser.id)
logger.info(
`sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`,
)
@ -270,10 +266,7 @@ export class UserResolver {
const gradidoID = await newGradidoID()
const eventRegister = new EventRegister()
const eventRedeemRegister = new EventRedeemRegister()
const eventSendConfirmEmail = new EventSendConfirmationEmail()
const eventRegisterRedeem = Event(EventProtocolType.REDEEM_REGISTER, 0)
let dbUser = new DbUser()
dbUser.gradidoID = gradidoID
dbUser.firstName = firstName
@ -290,14 +283,14 @@ export class UserResolver {
logger.info('redeemCode found contributionLink=' + contributionLink)
if (contributionLink) {
dbUser.contributionLinkId = contributionLink.id
eventRedeemRegister.contributionId = contributionLink.id
eventRegisterRedeem.contributionId = contributionLink.id
}
} else {
const transactionLink = await DbTransactionLink.findOne({ code: redeemCode })
logger.info('redeemCode found transactionLink=' + transactionLink)
if (transactionLink) {
dbUser.referrerId = transactionLink.userId
eventRedeemRegister.transactionId = transactionLink.id
eventRegisterRedeem.transactionId = transactionLink.id
}
}
}
@ -335,8 +328,8 @@ export class UserResolver {
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
})
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
eventSendConfirmEmail.userId = dbUser.id
eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail))
await EVENT_SEND_CONFIRMATION_EMAIL(dbUser.id)
if (!emailSent) {
logger.debug(`Account confirmation link: ${activationLink}`)
@ -353,11 +346,10 @@ export class UserResolver {
logger.info('createUser() successful...')
if (redeemCode) {
eventRedeemRegister.userId = dbUser.id
await eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
eventRegisterRedeem.userId = dbUser.id
await eventRegisterRedeem.save()
} else {
eventRegister.userId = dbUser.id
await eventProtocol.writeEvent(event.setEventRegister(eventRegister))
await EVENT_REGISTER(dbUser.id)
}
return new User(dbUser)
@ -460,8 +452,6 @@ export class UserResolver {
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ')
const event = new Event()
try {
// Save user
await queryRunner.manager.save(user).catch((error) => {
@ -475,9 +465,7 @@ export class UserResolver {
await queryRunner.commitTransaction()
logger.info('User and UserContact data written successfully...')
const eventActivateAccount = new EventActivateAccount()
eventActivateAccount.userId = user.id
eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount))
await EVENT_ACTIVATE_ACCOUNT(user.id)
} catch (e) {
await queryRunner.rollbackTransaction()
throw new LogError('Error on writing User and User Contact data', e)
@ -793,19 +781,12 @@ export class UserResolver {
email = email.trim().toLowerCase()
// const user = await dbUser.findOne({ id: emailContact.userId })
const user = await findUserByEmail(email)
if (!user) {
throw new LogError('Could not find user to given email contact', email)
}
if (user.deletedAt) {
if (user.deletedAt || user.emailContact.deletedAt) {
throw new LogError('User with given email contact is deleted', email)
}
const emailContact = user.emailContact
if (emailContact.deletedAt) {
throw new LogError('The given email contact for this user is deleted', email)
}
emailContact.emailResendCount++
await emailContact.save()
user.emailContact.emailResendCount++
await user.emailContact.save()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendAccountActivationEmail({
@ -813,7 +794,7 @@ export class UserResolver {
lastName: user.lastName,
email,
language: user.language,
activationLink: activationLink(emailContact.emailVerificationCode),
activationLink: activationLink(user.emailContact.emailVerificationCode),
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
})
@ -821,12 +802,7 @@ export class UserResolver {
if (!emailSent) {
logger.info(`Account confirmation link: ${activationLink}`)
} else {
const event = new Event()
const eventSendConfirmationEmail = new EventSendConfirmationEmail()
eventSendConfirmationEmail.userId = user.id
await eventProtocol.writeEvent(
event.setEventSendConfirmationEmail(eventSendConfirmationEmail),
)
await EVENT_ADMIN_SEND_CONFIRMATION_EMAIL(user.id)
}
return true

View File

@ -0,0 +1,14 @@
import { Transaction as DbTransaction } from '@entity/Transaction'
export const getLastTransaction = async (
userId: number,
relations?: string[],
): Promise<DbTransaction | undefined> => {
return DbTransaction.findOne(
{ userId },
{
order: { balanceDate: 'DESC', id: 'DESC' },
relations,
},
)
}

View File

@ -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“!"
},

View File

@ -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",

View File

@ -68,6 +68,12 @@ export const createUser = gql`
}
`
export const sendActivationEmail = gql`
mutation ($email: String!) {
sendActivationEmail(email: $email)
}
`
export const sendCoins = gql`
mutation ($email: String!, $amount: Decimal!, $memo: String!) {
sendCoins(email: $email, amount: $amount, memo: $memo)

View File

@ -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',

View File

@ -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)

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=v1.2022-03-18
DB_HOST=localhost
DB_PORT=3306
DB_USER=root

View File

@ -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
}

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
}

View File

@ -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

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=v4.2022-12-20
# Environment
DEFAULT_PUBLISHER_ID=2896

View File

@ -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"
]
}
}

View File

@ -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',
},
],
])
})
})
})

View File

@ -23,10 +23,7 @@
<template #nav-next-year><span></span></template>
</b-form-datepicker>
<div
v-if="(isThisMonth && maxGddThisMonth <= 0) || (!isThisMonth && maxGddLastMonth <= 0)"
class="p-3"
>
<div v-if="showMessage" class="p-3" data-test="contribtion-message">
{{ noOpenCreation }}
</div>
<div v-else>
@ -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 === '' ||

View File

@ -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 }]])
})
})
})
})

View File

@ -1,5 +1,9 @@
<template>
<div class="decayinformation-startblock">
<div class="my-4">
<div class="font-weight-bold pb-2">{{ $t('form.memo') }}</div>
<div>{{ memo }}</div>
</div>
<div class="mt-3 mb-3 text-center">
<b>{{ $t('decay.before_startblock_transaction') }}</b>
</div>
@ -8,5 +12,11 @@
<script>
export default {
name: 'DecayInformation-StartBlock',
props: {
memo: {
type: String,
required: true,
},
},
}
</script>

View File

@ -1,6 +1,6 @@
<template>
<div class="decay-information-box">
<decay-information-before-startblock v-if="decay.start === null" />
<decay-information-before-startblock v-if="decay.start === null" :memo="memo" />
<decay-information-decay-startblock
v-else-if="isStartBlock"
:amount="amount"

View File

@ -179,6 +179,17 @@ export default {
},
},
computed: {
disabled() {
if (
this.form.email.length > 5 &&
parseInt(this.form.amount) <= parseInt(this.balance) &&
this.form.memo.length > 5 &&
this.form.memo.length <= 255
) {
return false
}
return true
},
isBalanceDisabled() {
return this.balance <= 0 ? 'disabled' : false
},

View File

@ -67,6 +67,7 @@ export default {
}
if (this.tokenExpiresInSeconds === 0) {
this.$timer.stop('tokenExpires')
this.toastInfoNoHide(this.$t('session.automaticallyLoggedOut'))
this.$emit('logout')
}
},
@ -84,6 +85,7 @@ export default {
})
.catch(() => {
this.$timer.stop('tokenExpires')
this.toastInfoNoHide(this.$t('session.automaticallyLoggedOut'))
this.$emit('logout')
})
},

View File

@ -173,7 +173,7 @@
"GDD": "GDD",
"gddKonto": "GDD Konto",
"gdd_per_link": {
"choose-amount": "Wähle einen Betrag aus, welchen du per Link versenden möchtest. Du kannst auch noch eine Nachricht eintragen. Beim Klick „Jetzt generieren“ wird ein Link erstellt, den du versenden kannst.",
"choose-amount": "Wähle einen Betrag aus, welchen du per Link versenden möchtest, und trage eine Nachricht ein. Die Nachricht ist ein Pflichtfeld.",
"copy-link": "Link kopieren",
"copy-link-with-text": "Link und Text kopieren",
"created": "Der Link wurde erstellt!",
@ -272,6 +272,7 @@
"send_gdd": "GDD versenden",
"send_per_link": "GDD versenden per Link",
"session": {
"automaticallyLoggedOut": "Du wurdest automatisch abgemeldet",
"extend": "Angemeldet bleiben",
"lightText": "Wenn du länger als 10 Minuten keine Aktion getätigt hast, wirst du aus Sicherheitsgründen abgemeldet.",
"logoutIn": "Abmelden in ",

View File

@ -173,7 +173,7 @@
"GDD": "GDD",
"gddKonto": "GDD Konto",
"gdd_per_link": {
"choose-amount": "Select an amount that you would like to send via link. You can also enter a message. Click 'Generate now' to create a link that you can share.",
"choose-amount": "Select an amount you want to send via link and enter a message. The message is mandatory.",
"copy-link": "Copy link",
"copy-link-with-text": "Copy link and text",
"created": "Link was created!",
@ -272,6 +272,7 @@
"send_gdd": "Send GDD",
"send_per_link": "Send GDD via Link",
"session": {
"automaticallyLoggedOut": "You have been automatically logged out.",
"extend": "Stay logged in",
"lightText": "If you have not performed any action for more than 10 minutes, you will be logged out for security reasons.",
"logoutIn": "Log out in ",

View File

@ -18,11 +18,18 @@ export const toasters = {
variant: 'warning',
})
},
toastInfoNoHide(message) {
this.toast(message, {
title: this.$t('navigation.info'),
variant: 'warning',
noAutoHide: true,
})
},
toast(message, options) {
if (message.replace) message = message.replace(/^GraphQL error: /, '')
this.$root.$bvToast.toast(message, {
autoHideDelay: 5000,
appendToast: true,
autoHideDelay: 5000,
solid: true,
toaster: 'b-toaster-top-right',
headerClass: 'gdd-toaster-title',

View File

@ -70,6 +70,8 @@ describe('Community', () => {
lastName: 'Bloxberg',
state: 'IN_PROGRESS',
messagesCount: 0,
deniedAt: null,
deniedBy: null,
},
{
id: 1550,
@ -84,6 +86,8 @@ describe('Community', () => {
lastName: 'Bloxberg',
state: 'CONFIRMED',
messagesCount: 0,
deniedAt: null,
deniedBy: null,
},
],
contributionCount: 1,
@ -112,6 +116,10 @@ describe('Community', () => {
confirmedAt: null,
firstName: 'Bibi',
lastName: 'Bloxberg',
deniedAt: null,
deniedBy: null,
messagesCount: 0,
state: 'IN_PROGRESS',
},
{
id: 1550,
@ -124,7 +132,10 @@ describe('Community', () => {
firstName: 'Bibi',
contributionDate: '2022-06-15T08:47:06.000Z',
lastName: 'Bloxberg',
deniedAt: null,
deniedBy: null,
messagesCount: 0,
state: 'IN_PROGRESS',
},
{
id: 1556,
@ -137,6 +148,10 @@ describe('Community', () => {
confirmedAt: null,
firstName: 'Bob',
lastName: 'der Baumeister',
deniedAt: null,
deniedBy: null,
messagesCount: 0,
state: 'IN_PROGRESS',
},
],
contributionCount: 3,

View File

@ -6,9 +6,9 @@
{{ CONFIG.COMMUNITY_DESCRIPTION }}
</div>
<div>
<router-link :to="CONFIG.COMMUNITY_URL">
<b-link :href="CONFIG.COMMUNITY_URL">
{{ CONFIG.COMMUNITY_URL }}
</router-link>
</b-link>
</div>
<hr />
<div class="h3">{{ $t('community.openContributionLinks') }}</div>

View File

@ -1,6 +1,6 @@
{
"name": "gradido",
"version": "1.17.1",
"version": "1.18.2",
"description": "Gradido",
"main": "index.js",
"repository": "git@github.com:gradido/gradido.git",