Merge branch 'master' into refactor-deny-contribution-unit-test

This commit is contained in:
Hannes Heine 2023-02-15 09:23:08 +01:00 committed by GitHub
commit 38b7234d96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1162 additions and 1740 deletions

View File

@ -360,6 +360,25 @@ jobs:
- name: backend | Lint - name: backend | Lint
run: docker run --rm gradido/backend:test yarn run 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 ###################################################### # JOB: LINT DATABASE UP ######################################################
############################################################################## ##############################################################################
@ -526,7 +545,7 @@ jobs:
report_name: Coverage Backend report_name: Coverage Backend
type: lcov type: lcov
result_path: ./backend/coverage/lcov.info result_path: ./backend/coverage/lcov.info
min_coverage: 78 min_coverage: 80
token: ${{ github.token }} token: ${{ github.token }}
########################################################################## ##########################################################################

View File

@ -42,14 +42,30 @@ describe('ContributionLink', () => {
expect(wrapper.find('div.contribution-link').exists()).toBe(true) expect(wrapper.find('div.contribution-link').exists()).toBe(true)
}) })
it('emits toggle::collapse new Contribution', async () => { describe('function editContributionLinkData', () => {
wrapper.vm.editContributionLinkData() beforeEach(() => {
expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy() 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 () => { describe('function closeContributionForm', () => {
wrapper.vm.closeContributionForm() beforeEach(async () => {
expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy() 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!') 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', () => { 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', () => { it('has a link to /user', () => {
expect(wrapper.findAll('.nav-item').at(1).find('a').attributes('href')).toBe('/user') expect(wrapper.findAll('.nav-item').at(0).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')
}) })
it('has a link to /creation-confirm', () => { 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', '/creation-confirm',
) )
}) })
it('has a link to /contribution-links', () => { 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', '/contribution-links',
) )
}) })
it('has a link to /statistic', () => { 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', () => { describe('wallet', () => {
const assignLocationSpy = jest.fn() const assignLocationSpy = jest.fn()
beforeEach(async () => { 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', () => { it.skip('changes window location to wallet', () => {
@ -97,7 +89,7 @@ describe('NavBar', () => {
window.location = { window.location = {
assign: windowLocationMock, 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', () => { it('redirects to /logout', () => {

View File

@ -9,9 +9,7 @@
<b-collapse id="nav-collapse" is-nav> <b-collapse id="nav-collapse" is-nav>
<b-navbar-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="/user">{{ $t('navbar.user_search') }}</b-nav-item>
<b-nav-item to="/creation">{{ $t('navbar.multi_creation') }}</b-nav-item>
<b-nav-item <b-nav-item
v-show="$store.state.openCreations > 0" v-show="$store.state.openCreations > 0"
class="bg-color-creation p-1" 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", "creation": "Schöpfung",
"creationList": "Schöpfungsliste", "creationList": "Schöpfungsliste",
"creation_form": { "creation_form": {
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
"creation_for": "Aktives Grundeinkommen für", "creation_for": "Aktives Grundeinkommen für",
"enter_text": "Text eintragen", "enter_text": "Text eintragen",
"form": "Schöpfungsformular", "form": "Schöpfungsformular",
@ -87,7 +86,6 @@
"lastname": "Nachname", "lastname": "Nachname",
"math": { "math": {
"equals": "=", "equals": "=",
"exclaim": "!",
"pipe": "|", "pipe": "|",
"plus": "+" "plus": "+"
}, },
@ -95,15 +93,12 @@
"request": "Die Anfrage wurde gesendet." "request": "Die Anfrage wurde gesendet."
}, },
"moderator": "Moderator", "moderator": "Moderator",
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
"name": "Name", "name": "Name",
"navbar": { "navbar": {
"automaticContributions": "Automatische Beiträge", "automaticContributions": "Automatische Beiträge",
"logout": "Abmelden", "logout": "Abmelden",
"multi_creation": "Mehrfachschöpfung",
"my-account": "Mein Konto", "my-account": "Mein Konto",
"open_creation": "Offene Schöpfungen", "open_creation": "Offene Schöpfungen",
"overview": "Übersicht",
"statistic": "Statistik", "statistic": "Statistik",
"user_search": "Nutzersuche" "user_search": "Nutzersuche"
}, },
@ -132,9 +127,7 @@
} }
}, },
"redeemed": "eingelöst", "redeemed": "eingelöst",
"remove": "Entfernen",
"removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.", "removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.",
"remove_all": "alle Nutzer entfernen",
"save": "Speichern", "save": "Speichern",
"statistic": { "statistic": {
"activeUsers": "Aktive Mitglieder", "activeUsers": "Aktive Mitglieder",

View File

@ -32,7 +32,6 @@
"creation": "Creation", "creation": "Creation",
"creationList": "Creation list", "creationList": "Creation list",
"creation_form": { "creation_form": {
"creation_failed": "Could not create pending creation for {email}",
"creation_for": "Active Basic Income for", "creation_for": "Active Basic Income for",
"enter_text": "Enter text", "enter_text": "Enter text",
"form": "Creation form", "form": "Creation form",
@ -87,7 +86,6 @@
"lastname": "Lastname", "lastname": "Lastname",
"math": { "math": {
"equals": "=", "equals": "=",
"exclaim": "!",
"pipe": "|", "pipe": "|",
"plus": "+" "plus": "+"
}, },
@ -95,15 +93,12 @@
"request": "Request has been sent." "request": "Request has been sent."
}, },
"moderator": "Moderator", "moderator": "Moderator",
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
"name": "Name", "name": "Name",
"navbar": { "navbar": {
"automaticContributions": "Automatic Contributions", "automaticContributions": "Automatic Contributions",
"logout": "Logout", "logout": "Logout",
"multi_creation": "Multiple creation",
"my-account": "My Account", "my-account": "My Account",
"open_creation": "Open creations", "open_creation": "Open creations",
"overview": "Overview",
"statistic": "Statistic", "statistic": "Statistic",
"user_search": "User search" "user_search": "User search"
}, },
@ -132,9 +127,7 @@
} }
}, },
"redeemed": "redeemed", "redeemed": "redeemed",
"remove": "Remove",
"removeNotSelf": "As an admin/moderator, you cannot delete yourself.", "removeNotSelf": "As an admin/moderator, you cannot delete yourself.",
"remove_all": "Remove all users",
"save": "Speichern", "save": "Speichern",
"statistic": { "statistic": {
"activeUsers": "Active members", "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 { mount } from '@vue/test-utils'
import ContributionLinks from './ContributionLinks.vue' import ContributionLinks from './ContributionLinks.vue'
import { listContributionLinks } from '@/graphql/listContributionLinks.js' import { listContributionLinks } from '@/graphql/listContributionLinks.js'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue const localVue = global.localVue
@ -46,13 +47,31 @@ describe('ContributionLinks', () => {
beforeEach(() => { beforeEach(() => {
wrapper = Wrapper() wrapper = Wrapper()
}) })
describe('apollo returns', () => {
it('calls listContributionLinks', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
query: listContributionLinks,
}),
)
})
})
it('calls listContributionLinks', () => { describe.skip('query transaction with error', () => {
expect(apolloQueryMock).toBeCalledWith( beforeEach(() => {
expect.objectContaining({ apolloQueryMock.mockRejectedValue({ message: 'OUCH!' })
query: listContributionLinks, 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

@ -45,7 +45,7 @@ describe('router', () => {
describe('routes', () => { describe('routes', () => {
it('has nine routes defined', () => { it('has nine routes defined', () => {
expect(routes).toHaveLength(9) expect(routes).toHaveLength(8)
}) })
it('has "/overview" as default', async () => { 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', () => { describe('creation-confirm', () => {
it('loads the "CreationConfirm" component', async () => { it('loads the "CreationConfirm" component', async () => {
const component = await routes.find((r) => r.path === '/creation-confirm').component() const component = await routes.find((r) => r.path === '/creation-confirm').component()

View File

@ -19,10 +19,6 @@ const routes = [
path: '/user', path: '/user',
component: () => import('@/pages/UserSearch.vue'), component: () => import('@/pages/UserSearch.vue'),
}, },
{
path: '/creation',
component: () => import('@/pages/Creation.vue'),
},
{ {
path: '/creation-confirm', path: '/creation-confirm',
component: () => import('@/pages/CreationConfirm.vue'), component: () => import('@/pages/CreationConfirm.vue'),

View File

@ -15,7 +15,8 @@
"lint": "eslint --max-warnings=0 --ext .js,.ts .", "lint": "eslint --max-warnings=0 --ext .js,.ts .",
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles", "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", "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": { "dependencies": {
"@hyperswarm/dht": "^6.2.0", "@hyperswarm/dht": "^6.2.0",

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

@ -1,509 +1,212 @@
import decimal from 'decimal.js-light' import { EventProtocol as DbEvent } from '@entity/EventProtocol'
import Decimal from 'decimal.js-light'
import { EventProtocolType } from './EventProtocolType' import { EventProtocolType } from './EventProtocolType'
export class EventBasic { export const Event = (
type: string type: EventProtocolType,
createdAt: Date userId: number,
} xUserId: number | null = null,
export class EventBasicUserId extends EventBasic { xCommunityId: number | null = null,
userId: number 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 { export const EVENT_CONTRIBUTION_CREATE = async (
transactionId: number userId: number,
amount: decimal contributionId: number,
} amount: Decimal,
): Promise<DbEvent> =>
export class EventBasicTxX extends EventBasicTx { Event(
xUserId: number EventProtocolType.CONTRIBUTION_CREATE,
xCommunityId: number userId,
} null,
null,
export class EventBasicCt extends EventBasicUserId { null,
contributionId: number contributionId,
amount: decimal amount,
} ).save()
export class EventBasicCtX extends EventBasicCt { export const EVENT_CONTRIBUTION_DELETE = async (
xUserId: number userId: number,
xCommunityId: number contributionId: number,
} amount: Decimal,
): Promise<DbEvent> =>
export class EventBasicRedeem extends EventBasicUserId { Event(
transactionId?: number EventProtocolType.CONTRIBUTION_DELETE,
contributionId?: number userId,
} null,
null,
export class EventBasicCtMsg extends EventBasicCt { null,
messageId: number contributionId,
} amount,
).save()
export class EventVisitGradido extends EventBasic {}
export class EventRegister extends EventBasicUserId {} export const EVENT_CONTRIBUTION_UPDATE = async (
export class EventRedeemRegister extends EventBasicRedeem {} userId: number,
export class EventVerifyRedeem extends EventBasicRedeem {} contributionId: number,
export class EventInactiveAccount extends EventBasicUserId {} amount: Decimal,
export class EventSendConfirmationEmail extends EventBasicUserId {} ): Promise<DbEvent> =>
export class EventSendAccountMultiRegistrationEmail extends EventBasicUserId {} Event(
export class EventSendForgotPasswordEmail extends EventBasicUserId {} EventProtocolType.CONTRIBUTION_UPDATE,
export class EventSendTransactionSendEmail extends EventBasicTxX {} userId,
export class EventSendTransactionReceiveEmail extends EventBasicTxX {} null,
export class EventSendTransactionLinkRedeemEmail extends EventBasicTxX {} null,
export class EventSendAddedContributionEmail extends EventBasicCt {} null,
export class EventSendContributionConfirmEmail extends EventBasicCt {} contributionId,
export class EventConfirmationEmail extends EventBasicUserId {} amount,
export class EventRegisterEmailKlicktipp extends EventBasicUserId {} ).save()
export class EventLogin extends EventBasicUserId {}
export class EventLogout extends EventBasicUserId {} export const EVENT_ADMIN_CONTRIBUTION_CREATE = async (
export class EventRedeemLogin extends EventBasicRedeem {} userId: number,
export class EventActivateAccount extends EventBasicUserId {} contributionId: number,
export class EventPasswordChange extends EventBasicUserId {} amount: Decimal,
export class EventTransactionSend extends EventBasicTxX {} ): Promise<DbEvent> =>
export class EventTransactionSendRedeem extends EventBasicTxX {} Event(
export class EventTransactionRepeateRedeem extends EventBasicTxX {} EventProtocolType.ADMIN_CONTRIBUTION_CREATE,
export class EventTransactionCreation extends EventBasicTx {} userId,
export class EventTransactionReceive extends EventBasicTxX {} null,
export class EventTransactionReceiveRedeem extends EventBasicTxX {} null,
export class EventContributionCreate extends EventBasicCt {} null,
export class EventAdminContributionCreate extends EventBasicCt {} contributionId,
export class EventAdminContributionDelete extends EventBasicCt {} amount,
export class EventAdminContributionDeny extends EventBasicCt {} ).save()
export class EventAdminContributionUpdate extends EventBasicCt {}
export class EventUserCreateContributionMessage extends EventBasicCtMsg {} export const EVENT_ADMIN_CONTRIBUTION_UPDATE = async (
export class EventAdminCreateContributionMessage extends EventBasicCtMsg {} userId: number,
export class EventContributionDelete extends EventBasicCt {} contributionId: number,
export class EventContributionUpdate extends EventBasicCt {} amount: Decimal,
export class EventContributionConfirm extends EventBasicCtX {} ): Promise<DbEvent> =>
export class EventContributionDeny extends EventBasicCtX {} Event(
export class EventContributionLinkDefine extends EventBasicCt {} EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
export class EventContributionLinkActivateRedeem extends EventBasicCt {} userId,
export class EventDeleteUser extends EventBasicUserId {} null,
export class EventUndeleteUser extends EventBasicUserId {} null,
export class EventChangeUserRole extends EventBasicUserId {} null,
export class EventAdminUpdateContribution extends EventBasicCt {} contributionId,
export class EventAdminDeleteContribution extends EventBasicCt {} amount,
export class EventCreateContributionLink extends EventBasicCt {} ).save()
export class EventDeleteContributionLink extends EventBasicCt {}
export class EventUpdateContributionLink extends EventBasicCt {} export const EVENT_ADMIN_CONTRIBUTION_DELETE = async (
userId: number,
export class Event { contributionId: number,
public setEventBasic(): Event { amount: Decimal,
this.type = EventProtocolType.BASIC ): Promise<DbEvent> =>
this.createdAt = new Date() Event(
EventProtocolType.ADMIN_CONTRIBUTION_DELETE,
return this userId,
} null,
null,
public setEventVisitGradido(): Event { null,
this.setEventBasic() contributionId,
this.type = EventProtocolType.VISIT_GRADIDO amount,
).save()
return this
} export const EVENT_CONTRIBUTION_CONFIRM = async (
userId: number,
public setEventRegister(ev: EventRegister): Event { contributionId: number,
this.setByBasicUser(ev.userId) amount: Decimal,
this.type = EventProtocolType.REGISTER ): Promise<DbEvent> =>
Event(
return this EventProtocolType.CONTRIBUTION_CONFIRM,
} userId,
null,
public setEventRedeemRegister(ev: EventRedeemRegister): Event { null,
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId) null,
this.type = EventProtocolType.REDEEM_REGISTER contributionId,
amount,
return this ).save()
}
export const EVENT_ADMIN_CONTRIBUTION_DENY = async (
public setEventVerifyRedeem(ev: EventVerifyRedeem): Event { userId: number,
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId) xUserId: number,
this.type = EventProtocolType.VERIFY_REDEEM contributionId: number,
amount: Decimal,
return this ): Promise<DbEvent> =>
} Event(
EventProtocolType.ADMIN_CONTRIBUTION_DENY,
public setEventInactiveAccount(ev: EventInactiveAccount): Event { userId,
this.setByBasicUser(ev.userId) xUserId,
this.type = EventProtocolType.INACTIVE_ACCOUNT null,
null,
return this contributionId,
} amount,
).save()
public setEventSendConfirmationEmail(ev: EventSendConfirmationEmail): Event {
this.setByBasicUser(ev.userId) export const EVENT_TRANSACTION_SEND = async (
this.type = EventProtocolType.SEND_CONFIRMATION_EMAIL userId: number,
xUserId: number,
return this transactionId: number,
} amount: Decimal,
): Promise<DbEvent> =>
public setEventSendAccountMultiRegistrationEmail( Event(
ev: EventSendAccountMultiRegistrationEmail, EventProtocolType.TRANSACTION_SEND,
): Event { userId,
this.setByBasicUser(ev.userId) xUserId,
this.type = EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL null,
transactionId,
return this null,
} amount,
).save()
public setEventSendForgotPasswordEmail(ev: EventSendForgotPasswordEmail): Event {
this.setByBasicUser(ev.userId) export const EVENT_TRANSACTION_RECEIVE = async (
this.type = EventProtocolType.SEND_FORGOT_PASSWORD_EMAIL userId: number,
xUserId: number,
return this transactionId: number,
} amount: Decimal,
): Promise<DbEvent> =>
public setEventSendTransactionSendEmail(ev: EventSendTransactionSendEmail): Event { Event(
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) EventProtocolType.TRANSACTION_RECEIVE,
this.type = EventProtocolType.SEND_TRANSACTION_SEND_EMAIL userId,
xUserId,
return this null,
} transactionId,
null,
public setEventSendTransactionReceiveEmail(ev: EventSendTransactionReceiveEmail): Event { amount,
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) ).save()
this.type = EventProtocolType.SEND_TRANSACTION_RECEIVE_EMAIL
export const EVENT_LOGIN = async (userId: number): Promise<DbEvent> =>
return this Event(EventProtocolType.LOGIN, userId, null, null, null, null, null, null).save()
}
export const EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = async (
public setEventSendTransactionLinkRedeemEmail(ev: EventSendTransactionLinkRedeemEmail): Event { userId: number,
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) ): Promise<DbEvent> => Event(EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL, userId).save()
this.type = EventProtocolType.SEND_TRANSACTION_LINK_REDEEM_EMAIL
export const EVENT_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise<DbEvent> =>
return this Event(EventProtocolType.SEND_CONFIRMATION_EMAIL, userId).save()
}
export const EVENT_ADMIN_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise<DbEvent> =>
public setEventSendAddedContributionEmail(ev: EventSendAddedContributionEmail): Event { Event(EventProtocolType.ADMIN_SEND_CONFIRMATION_EMAIL, userId).save()
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.SEND_ADDED_CONTRIBUTION_EMAIL /* export const EVENT_REDEEM_REGISTER = async (
userId: number,
return this transactionId: number | null = null,
} contributionId: number | null = null,
): Promise<Event> =>
public setEventSendContributionConfirmEmail(ev: EventSendContributionConfirmEmail): Event { Event(
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) EventProtocolType.REDEEM_REGISTER,
this.type = EventProtocolType.SEND_CONTRIBUTION_CONFIRM_EMAIL userId,
null,
return this null,
} transactionId,
contributionId,
public setEventConfirmationEmail(ev: EventConfirmationEmail): Event { ).save()
this.setByBasicUser(ev.userId) */
this.type = EventProtocolType.CONFIRM_EMAIL
export const EVENT_REGISTER = async (userId: number): Promise<DbEvent> =>
return this Event(EventProtocolType.REGISTER, userId).save()
}
export const EVENT_ACTIVATE_ACCOUNT = async (userId: number): Promise<DbEvent> =>
public setEventRegisterEmailKlicktipp(ev: EventRegisterEmailKlicktipp): Event { Event(EventProtocolType.ACTIVATE_ACCOUNT, userId).save()
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 setEventAdminContributionDeny(ev: EventAdminContributionDeny): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_CONTRIBUTION_DENY
return this
}
public setEventAdminContributionUpdate(ev: EventAdminContributionUpdate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_CONTRIBUTION_UPDATE
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
}

View File

@ -1,17 +0,0 @@
import { Event } from '@/event/Event'
import { backendLogger as logger } from '@/server/logger'
import { EventProtocol } from '@entity/EventProtocol'
export const writeEvent = async (event: Event): Promise<EventProtocol | null> => {
logger.info('writeEvent', event)
const dbEvent = new EventProtocol()
dbEvent.type = event.type
dbEvent.createdAt = event.createdAt
dbEvent.userId = event.userId
dbEvent.xUserId = event.xUserId || null
dbEvent.xCommunityId = event.xCommunityId || null
dbEvent.contributionId = event.contributionId || null
dbEvent.transactionId = event.transactionId || null
dbEvent.amount = event.amount || null
return dbEvent.save()
}

View File

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

View File

@ -283,7 +283,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( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.CONTRIBUTION_CREATE, type: EventProtocolType.CONTRIBUTION_CREATE,
@ -581,7 +581,7 @@ describe('ContributionResolver', () => {
}) })
}) })
it('stores the update contribution event in the database', async () => { it('stores the CONTRIBUTION_UPDATE event in the database', async () => {
await query({ await query({
query: login, query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
@ -928,7 +928,7 @@ describe('ContributionResolver', () => {
expect(isDenied).toBeTruthy() expect(isDenied).toBeTruthy()
}) })
it('stores the delete contribution event in the database', async () => { it('stores the CONTRIBUTION_DELETE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.CONTRIBUTION_DELETE, type: EventProtocolType.CONTRIBUTION_DELETE,
@ -2066,7 +2066,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( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_CREATE, type: EventProtocolType.ADMIN_CONTRIBUTION_CREATE,
@ -2332,7 +2332,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( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE, type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
@ -2372,7 +2372,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( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE, type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
@ -2550,7 +2550,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( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_DELETE, type: EventProtocolType.ADMIN_CONTRIBUTION_DELETE,
@ -2693,7 +2693,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( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.CONTRIBUTION_CONFIRM, type: EventProtocolType.CONTRIBUTION_CONFIRM,
@ -2725,7 +2725,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( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.SEND_CONFIRMATION_EMAIL, type: EventProtocolType.SEND_CONFIRMATION_EMAIL,

View File

@ -37,17 +37,15 @@ import {
} from './util/creations' } from './util/creations'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, FULL_CREATION_AVAILABLE } from './const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, FULL_CREATION_AVAILABLE } from './const/const'
import { import {
Event, EVENT_CONTRIBUTION_CREATE,
EventContributionCreate, EVENT_CONTRIBUTION_DELETE,
EventContributionDelete, EVENT_CONTRIBUTION_UPDATE,
EventContributionUpdate, EVENT_ADMIN_CONTRIBUTION_CREATE,
EventContributionConfirm, EVENT_ADMIN_CONTRIBUTION_UPDATE,
EventAdminContributionCreate, EVENT_ADMIN_CONTRIBUTION_DELETE,
EventAdminContributionDelete, EVENT_CONTRIBUTION_CONFIRM,
EventAdminContributionDeny, EVENT_ADMIN_CONTRIBUTION_DENY,
EventAdminContributionUpdate,
} from '@/event/Event' } from '@/event/Event'
import { writeEvent } from '@/event/EventProtocolEmitter'
import { calculateDecay } from '@/util/decay' import { calculateDecay } from '@/util/decay'
import { import {
sendContributionConfirmedEmail, sendContributionConfirmedEmail,
@ -75,8 +73,6 @@ export class ContributionResolver {
throw new LogError('Memo text is too long', memo.length) throw new LogError('Memo text is too long', memo.length)
} }
const event = new Event()
const user = getUser(context) const user = getUser(context)
const creations = await getUserCreation(user.id, clientTimezoneOffset) const creations = await getUserCreation(user.id, clientTimezoneOffset)
logger.trace('creations', creations) logger.trace('creations', creations)
@ -95,11 +91,7 @@ export class ContributionResolver {
logger.trace('contribution to save', contribution) logger.trace('contribution to save', contribution)
await DbContribution.save(contribution) await DbContribution.save(contribution)
const eventCreateContribution = new EventContributionCreate() await EVENT_CONTRIBUTION_CREATE(user.id, contribution.id, amount)
eventCreateContribution.userId = user.id
eventCreateContribution.amount = amount
eventCreateContribution.contributionId = contribution.id
await writeEvent(event.setEventContributionCreate(eventCreateContribution))
return new UnconfirmedContribution(contribution, user, creations) return new UnconfirmedContribution(contribution, user, creations)
} }
@ -110,7 +102,6 @@ export class ContributionResolver {
@Arg('id', () => Int) id: number, @Arg('id', () => Int) id: number,
@Ctx() context: Context, @Ctx() context: Context,
): Promise<boolean> { ): Promise<boolean> {
const event = new Event()
const user = getUser(context) const user = getUser(context)
const contribution = await DbContribution.findOne(id) const contribution = await DbContribution.findOne(id)
if (!contribution) { if (!contribution) {
@ -128,11 +119,7 @@ export class ContributionResolver {
contribution.deletedAt = new Date() contribution.deletedAt = new Date()
await contribution.save() await contribution.save()
const eventDeleteContribution = new EventContributionDelete() await EVENT_CONTRIBUTION_DELETE(user.id, contribution.id, contribution.amount)
eventDeleteContribution.userId = user.id
eventDeleteContribution.contributionId = contribution.id
eventDeleteContribution.amount = contribution.amount
await writeEvent(event.setEventContributionDelete(eventDeleteContribution))
const res = await contribution.softRemove() const res = await contribution.softRemove()
return !!res return !!res
@ -279,13 +266,7 @@ export class ContributionResolver {
contributionToUpdate.updatedAt = new Date() contributionToUpdate.updatedAt = new Date()
DbContribution.save(contributionToUpdate) DbContribution.save(contributionToUpdate)
const event = new Event() await EVENT_CONTRIBUTION_UPDATE(user.id, contributionId, amount)
const eventUpdateContribution = new EventContributionUpdate()
eventUpdateContribution.userId = user.id
eventUpdateContribution.contributionId = contributionId
eventUpdateContribution.amount = amount
await writeEvent(event.setEventContributionUpdate(eventUpdateContribution))
return new UnconfirmedContribution(contributionToUpdate, user, creations) return new UnconfirmedContribution(contributionToUpdate, user, creations)
} }
@ -321,7 +302,6 @@ export class ContributionResolver {
) )
} }
const event = new Event()
const moderator = getUser(context) const moderator = getUser(context)
logger.trace('moderator: ', moderator.id) logger.trace('moderator: ', moderator.id)
const creations = await getUserCreation(emailContact.userId, clientTimezoneOffset) const creations = await getUserCreation(emailContact.userId, clientTimezoneOffset)
@ -343,11 +323,7 @@ export class ContributionResolver {
await DbContribution.save(contribution) await DbContribution.save(contribution)
const eventAdminCreateContribution = new EventAdminContributionCreate() await EVENT_ADMIN_CONTRIBUTION_CREATE(moderator.id, contribution.id, amount)
eventAdminCreateContribution.userId = moderator.id
eventAdminCreateContribution.amount = amount
eventAdminCreateContribution.contributionId = contribution.id
await writeEvent(event.setEventAdminContributionCreate(eventAdminCreateContribution))
return getUserCreation(emailContact.userId, clientTimezoneOffset) return getUserCreation(emailContact.userId, clientTimezoneOffset)
} }
@ -442,12 +418,7 @@ export class ContributionResolver {
result.creation = await getUserCreation(emailContact.user.id, clientTimezoneOffset) result.creation = await getUserCreation(emailContact.user.id, clientTimezoneOffset)
const event = new Event() await EVENT_ADMIN_CONTRIBUTION_UPDATE(emailContact.user.id, contributionToUpdate.id, amount)
const eventAdminContributionUpdate = new EventAdminContributionUpdate()
eventAdminContributionUpdate.userId = emailContact.user.id
eventAdminContributionUpdate.amount = amount
eventAdminContributionUpdate.contributionId = contributionToUpdate.id
await writeEvent(event.setEventAdminContributionUpdate(eventAdminContributionUpdate))
return result return result
} }
@ -518,12 +489,8 @@ export class ContributionResolver {
await contribution.save() await contribution.save()
const res = await contribution.softRemove() const res = await contribution.softRemove()
const event = new Event() await EVENT_ADMIN_CONTRIBUTION_DELETE(contribution.userId, contribution.id, contribution.amount)
const eventAdminContributionDelete = new EventAdminContributionDelete()
eventAdminContributionDelete.userId = contribution.userId
eventAdminContributionDelete.amount = contribution.amount
eventAdminContributionDelete.contributionId = contribution.id
await writeEvent(event.setEventAdminContributionDelete(eventAdminContributionDelete))
sendContributionDeletedEmail({ sendContributionDeletedEmail({
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
@ -635,12 +602,7 @@ export class ContributionResolver {
await queryRunner.release() await queryRunner.release()
} }
const event = new Event() await EVENT_CONTRIBUTION_CONFIRM(user.id, contribution.id, contribution.amount)
const eventContributionConfirm = new EventContributionConfirm()
eventContributionConfirm.userId = user.id
eventContributionConfirm.amount = contribution.amount
eventContributionConfirm.contributionId = contribution.id
await writeEvent(event.setEventContributionConfirm(eventContributionConfirm))
} finally { } finally {
releaseLock() releaseLock()
} }
@ -730,12 +692,12 @@ export class ContributionResolver {
contributionToUpdate.deniedAt = new Date() contributionToUpdate.deniedAt = new Date()
const res = await contributionToUpdate.save() const res = await contributionToUpdate.save()
const event = new Event() await EVENT_ADMIN_CONTRIBUTION_DENY(
const eventAdminContributionDeny = new EventAdminContributionDeny() contributionToUpdate.userId,
eventAdminContributionDeny.userId = moderator.id moderator.id,
eventAdminContributionDeny.amount = contributionToUpdate.amount contributionToUpdate.id,
eventAdminContributionDeny.contributionId = contributionToUpdate.id contributionToUpdate.amount,
await writeEvent(event.setEventAdminContributionDeny(eventAdminContributionDeny)) )
sendContributionDeniedEmail({ sendContributionDeniedEmail({
firstName: user.firstName, firstName: user.firstName,

View File

@ -16,6 +16,7 @@ import {
redeemTransactionLink, redeemTransactionLink,
createContribution, createContribution,
updateContribution, updateContribution,
createTransactionLink,
} from '@/seeds/graphql/mutations' } from '@/seeds/graphql/mutations'
import { listTransactionLinksAdmin } from '@/seeds/graphql/queries' import { listTransactionLinksAdmin } from '@/seeds/graphql/queries'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
@ -24,6 +25,7 @@ import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { logger } from '@test/testSetup'
// mock semaphore to allow use fake timers // mock semaphore to allow use fake timers
jest.mock('@/util/TRANSACTIONS_LOCK') jest.mock('@/util/TRANSACTIONS_LOCK')
@ -50,7 +52,75 @@ afterAll(async () => {
}) })
describe('TransactionLinkResolver', () => { describe('TransactionLinkResolver', () => {
describe('createTransactionLink', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
it('throws error when amount is zero', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createTransactionLink,
variables: {
amount: 0,
memo: 'Test',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Amount must be a positive number')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0))
})
it('throws error when amount is negative', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createTransactionLink,
variables: {
amount: -10,
memo: 'Test',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Amount must be a positive number')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10))
})
it('throws error when user has not enough GDD', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createTransactionLink,
variables: {
amount: 1001,
memo: 'Test',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('User has not enough GDD')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number))
})
})
describe('redeemTransactionLink', () => { describe('redeemTransactionLink', () => {
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('contributionLink', () => { describe('contributionLink', () => {
describe('input not valid', () => { describe('input not valid', () => {
beforeAll(async () => { beforeAll(async () => {
@ -61,6 +131,7 @@ describe('TransactionLinkResolver', () => {
}) })
it('throws error when link does not exists', async () => { it('throws error when link does not exists', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: redeemTransactionLink, mutation: redeemTransactionLink,
@ -69,16 +140,26 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: No contribution link found to given code: CL-123456',
),
],
}) })
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No contribution link found to given code',
'CL-123456',
)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('No contribution link found to given code'),
)
})
const now = new Date()
const validFrom = new Date(now.getFullYear() + 1, 0, 1)
it('throws error when link is not valid yet', async () => { it('throws error when link is not valid yet', async () => {
const now = new Date() jest.clearAllMocks()
const { const {
data: { createContributionLink: contributionLink }, data: { createContributionLink: contributionLink },
} = await mutate({ } = await mutate({
@ -88,7 +169,7 @@ describe('TransactionLinkResolver', () => {
name: 'Daily Contribution Link', name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community', memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY', cycle: 'DAILY',
validFrom: new Date(now.getFullYear() + 1, 0, 1).toISOString(), validFrom: validFrom.toISOString(),
validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(), validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200), maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1, maxPerCycle: 1,
@ -102,16 +183,21 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link not valid yet',
),
],
}) })
await resetEntity(DbContributionLink) await resetEntity(DbContributionLink)
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link is not valid yet'),
)
})
it('throws error when contributionLink cycle is invalid', async () => { it('throws error when contributionLink cycle is invalid', async () => {
jest.clearAllMocks()
const now = new Date() const now = new Date()
const { const {
data: { createContributionLink: contributionLink }, data: { createContributionLink: contributionLink },
@ -136,17 +222,22 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link has unknown cycle',
),
],
}) })
await resetEntity(DbContributionLink) await resetEntity(DbContributionLink)
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID')
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link has unknown cycle'),
)
})
const validTo = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 0)
it('throws error when link is no longer valid', async () => { it('throws error when link is no longer valid', async () => {
const now = new Date() jest.clearAllMocks()
const { const {
data: { createContributionLink: contributionLink }, data: { createContributionLink: contributionLink },
} = await mutate({ } = await mutate({
@ -157,7 +248,7 @@ describe('TransactionLinkResolver', () => {
memo: 'Thank you for contribute daily to the community', memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY', cycle: 'DAILY',
validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(), validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
validTo: new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 999).toISOString(), validTo: validTo.toISOString(),
maxAmountPerMonth: new Decimal(200), maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1, maxPerCycle: 1,
}, },
@ -170,14 +261,18 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link is no longer valid',
),
],
}) })
await resetEntity(DbContributionLink) await resetEntity(DbContributionLink)
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link is no longer valid'),
)
})
}) })
// TODO: have this test separated into a transactionLink and a contributionLink part // TODO: have this test separated into a transactionLink and a contributionLink part
@ -250,6 +345,7 @@ describe('TransactionLinkResolver', () => {
}) })
it('does not allow the user to redeem the contribution link', async () => { it('does not allow the user to redeem the contribution link', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: redeemTransactionLink, mutation: redeemTransactionLink,
@ -258,13 +354,18 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
),
],
}) })
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error(
'The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
),
)
})
}) })
describe('user has no pending contributions that would not allow to redeem the link', () => { describe('user has no pending contributions that would not allow to redeem the link', () => {
@ -301,6 +402,7 @@ describe('TransactionLinkResolver', () => {
}) })
it('does not allow the user to redeem the contribution link a second time on the same day', async () => { it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: redeemTransactionLink, mutation: redeemTransactionLink,
@ -309,14 +411,17 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
),
],
}) })
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link already redeemed today'),
)
})
describe('after one day', () => { describe('after one day', () => {
beforeAll(async () => { beforeAll(async () => {
jest.useFakeTimers() jest.useFakeTimers()
@ -349,6 +454,7 @@ describe('TransactionLinkResolver', () => {
}) })
it('does not allow the user to redeem the contribution link a second time on the same day', async () => { it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: redeemTransactionLink, mutation: redeemTransactionLink,
@ -357,33 +463,65 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
),
],
}) })
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link already redeemed today'),
)
})
}) })
}) })
}) })
}) })
})
describe('transaction links list', () => { describe('listTransactionLinksAdmin', () => {
const variables = { const variables = {
userId: 1, // dummy, may be replaced userId: 1, // dummy, may be replaced
filters: null, filters: null,
currentPage: 1, currentPage: 1,
pageSize: 5, pageSize: 5,
} }
// TODO: there is a test not cleaning up after itself! Fix it! afterAll(async () => {
beforeAll(async () => { await cleanDB()
await cleanDB() resetToken()
resetToken() })
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
query({
query: listTransactionLinksAdmin,
variables,
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
}) })
})
describe('authenticated', () => {
describe('without admin rights', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('unauthenticated', () => {
it('returns an error', async () => { it('returns an error', async () => {
await expect( await expect(
query({ query({
@ -398,22 +536,40 @@ describe('TransactionLinkResolver', () => {
}) })
}) })
describe('authenticated', () => { describe('with admin rights', () => {
describe('without admin rights', () => { beforeAll(async () => {
beforeAll(async () => { // admin 'peter@lustig.de' has to exists for 'creationFactory'
user = await userFactory(testEnv, bibiBloxberg) await userFactory(testEnv, peterLustig)
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(async () => { user = await userFactory(testEnv, bibiBloxberg)
await cleanDB() variables.userId = user.id
resetToken() variables.pageSize = 25
}) // bibi needs GDDs
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await creationFactory(testEnv, bibisCreation!)
// bibis transaktion links
const bibisTransaktionLinks = transactionLinks.filter(
(transactionLink) => transactionLink.email === 'bibi@bloxberg.de',
)
for (let i = 0; i < bibisTransaktionLinks.length; i++) {
await transactionLinkFactory(testEnv, bibisTransaktionLinks[i])
}
it('returns an error', async () => { // admin: only now log in
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('without any filters', () => {
it('finds 6 open transaction links and no deleted or redeemed', async () => {
await expect( await expect(
query({ query({
query: listTransactionLinksAdmin, query: listTransactionLinksAdmin,
@ -421,219 +577,169 @@ describe('TransactionLinkResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')], data: {
listTransactionLinksAdmin: {
linkCount: 6,
linkList: expect.not.arrayContaining([
expect.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}), }),
) )
}) })
}) })
describe('with admin rights', () => { describe('all filters are null', () => {
beforeAll(async () => { it('finds 6 open transaction links and no deleted or redeemed', async () => {
// admin 'peter@lustig.de' has to exists for 'creationFactory' await expect(
await userFactory(testEnv, peterLustig) query({
query: listTransactionLinksAdmin,
user = await userFactory(testEnv, bibiBloxberg) variables: {
variables.userId = user.id ...variables,
variables.pageSize = 25 filters: {
// bibi needs GDDs withDeleted: null,
const bibisCreation = creations.find( withExpired: null,
(creation) => creation.email === 'bibi@bloxberg.de', withRedeemed: null,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listTransactionLinksAdmin: {
linkCount: 6,
linkList: expect.not.arrayContaining([
expect.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}),
) )
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion })
await creationFactory(testEnv, bibisCreation!) })
// bibis transaktion links
const bibisTransaktionLinks = transactionLinks.filter( describe('filter with deleted', () => {
(transactionLink) => transactionLink.email === 'bibi@bloxberg.de', it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
await expect(
query({
query: listTransactionLinksAdmin,
variables: {
...variables,
filters: {
withDeleted: true,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listTransactionLinksAdmin: {
linkCount: 7,
linkList: expect.arrayContaining([
expect.not.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}),
) )
for (let i = 0; i < bibisTransaktionLinks.length; i++) {
await transactionLinkFactory(testEnv, bibisTransaktionLinks[i])
}
// admin: only now log in
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
}) })
})
afterAll(async () => { describe('filter by expired', () => {
await cleanDB() it('finds 5 open transaction links, 1 expired, and no redeemed', async () => {
resetToken() await expect(
query({
query: listTransactionLinksAdmin,
variables: {
...variables,
filters: {
withExpired: true,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listTransactionLinksAdmin: {
linkCount: 7,
linkList: expect.arrayContaining([
expect.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.not.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}),
)
}) })
})
describe('without any filters', () => { // TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory
it('finds 6 open transaction links and no deleted or redeemed', async () => { describe.skip('filter by redeemed', () => {
await expect( it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
query({ await expect(
query: listTransactionLinksAdmin, query({
variables, query: listTransactionLinksAdmin,
}), variables: {
).resolves.toEqual( ...variables,
expect.objectContaining({ filters: {
data: { withDeleted: null,
listTransactionLinksAdmin: { withExpired: null,
linkCount: 6, withRedeemed: true,
linkList: expect.not.arrayContaining([
expect.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
}, },
}), },
) }),
}) ).resolves.toEqual(
}) expect.objectContaining({
data: {
describe('all filters are null', () => { listTransactionLinksAdmin: {
it('finds 6 open transaction links and no deleted or redeemed', async () => { linkCount: 6,
await expect( linkList: expect.arrayContaining([
query({ expect.not.objectContaining({
query: listTransactionLinksAdmin, memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
variables: { createdAt: expect.any(String),
...variables, }),
filters: { expect.objectContaining({
withDeleted: null, memo: 'Yeah, eingelöst!',
withExpired: null, redeemedAt: expect.any(String),
withRedeemed: null, redeemedBy: expect.any(Number),
}, }),
expect.not.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
}, },
}), },
).resolves.toEqual( }),
expect.objectContaining({ )
data: {
listTransactionLinksAdmin: {
linkCount: 6,
linkList: expect.not.arrayContaining([
expect.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}),
)
})
})
describe('filter with deleted', () => {
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
await expect(
query({
query: listTransactionLinksAdmin,
variables: {
...variables,
filters: {
withDeleted: true,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listTransactionLinksAdmin: {
linkCount: 7,
linkList: expect.arrayContaining([
expect.not.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}),
)
})
})
describe('filter by expired', () => {
it('finds 5 open transaction links, 1 expired, and no redeemed', async () => {
await expect(
query({
query: listTransactionLinksAdmin,
variables: {
...variables,
filters: {
withExpired: true,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listTransactionLinksAdmin: {
linkCount: 7,
linkList: expect.arrayContaining([
expect.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.not.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}),
)
})
})
// TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory
describe.skip('filter by redeemed', () => {
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
await expect(
query({
query: listTransactionLinksAdmin,
variables: {
...variables,
filters: {
withDeleted: null,
withExpired: null,
withRedeemed: true,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listTransactionLinksAdmin: {
linkCount: 6,
linkList: expect.arrayContaining([
expect.not.objectContaining({
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: expect.any(String),
}),
expect.objectContaining({
memo: 'Yeah, eingelöst!',
redeemedAt: expect.any(String),
redeemedBy: expect.any(Number),
}),
expect.not.objectContaining({
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: expect.any(String),
}),
]),
},
},
}),
)
})
}) })
}) })
}) })

View File

@ -32,6 +32,7 @@ import { getUserCreation, validateContribution } from './util/creations'
import { executeTransaction } from './TransactionResolver' import { executeTransaction } from './TransactionResolver'
import QueryLinkResult from '@union/QueryLinkResult' import QueryLinkResult from '@union/QueryLinkResult'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction' import { getLastTransaction } from './util/getLastTransaction'
@ -65,12 +66,16 @@ export class TransactionLinkResolver {
const createdDate = new Date() const createdDate = new Date()
const validUntil = transactionLinkExpireDate(createdDate) const validUntil = transactionLinkExpireDate(createdDate)
if (amount.lessThanOrEqualTo(0)) {
throw new LogError('Amount must be a positive number', amount)
}
const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay) const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay)
// validate amount // validate amount
const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate) const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate)
if (!sendBalance) { if (!sendBalance) {
throw new Error("user hasn't enough GDD or amount is < 0") throw new LogError('User has not enough GDD', user.id)
} }
const transactionLink = DbTransactionLink.create() const transactionLink = DbTransactionLink.create()
@ -186,24 +191,15 @@ export class TransactionLinkResolver {
.where('contributionLink.code = :code', { code: code.replace('CL-', '') }) .where('contributionLink.code = :code', { code: code.replace('CL-', '') })
.getOne() .getOne()
if (!contributionLink) { if (!contributionLink) {
logger.error('no contribution link found to given code:', code) throw new LogError('No contribution link found to given code', code)
throw new Error(`No contribution link found to given code: ${code}`)
} }
logger.info('...contribution link found with id', contributionLink.id) logger.info('...contribution link found with id', contributionLink.id)
if (new Date(contributionLink.validFrom).getTime() > now.getTime()) { if (new Date(contributionLink.validFrom).getTime() > now.getTime()) {
logger.error( throw new LogError('Contribution link is not valid yet', contributionLink.validFrom)
'contribution link is not valid yet. Valid from: ',
contributionLink.validFrom,
)
throw new Error('Contribution link not valid yet')
} }
if (contributionLink.validTo) { if (contributionLink.validTo) {
if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) { if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) {
logger.error( throw new LogError('Contribution link is no longer valid', contributionLink.validTo)
'contribution link is no longer valid. Valid to: ',
contributionLink.validTo,
)
throw new Error('Contribution link is no longer valid')
} }
} }
let alreadyRedeemed: DbContribution | undefined let alreadyRedeemed: DbContribution | undefined
@ -219,11 +215,7 @@ export class TransactionLinkResolver {
}) })
.getOne() .getOne()
if (alreadyRedeemed) { if (alreadyRedeemed) {
logger.error( throw new LogError('Contribution link already redeemed', user.id)
'contribution link with rule ONCE already redeemed by user with id',
user.id,
)
throw new Error('Contribution link already redeemed')
} }
break break
} }
@ -248,17 +240,12 @@ export class TransactionLinkResolver {
) )
.getOne() .getOne()
if (alreadyRedeemed) { if (alreadyRedeemed) {
logger.error( throw new LogError('Contribution link already redeemed today', user.id)
'contribution link with rule DAILY already redeemed by user with id',
user.id,
)
throw new Error('Contribution link already redeemed today')
} }
break break
} }
default: { default: {
logger.error('contribution link has unknown cycle', contributionLink.cycle) throw new LogError('Contribution link has unknown cycle', contributionLink.cycle)
throw new Error('Contribution link has unknown cycle')
} }
} }
@ -308,8 +295,7 @@ export class TransactionLinkResolver {
logger.info('creation from contribution link commited successfuly.') logger.info('creation from contribution link commited successfuly.')
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error(`Creation from contribution link was not successful: ${e}`) throw new LogError('Creation from contribution link was not successful', e)
throw new Error(`Creation from contribution link was not successful. ${e}`)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import Decimal from 'decimal.js-light'
import { EventProtocolType } from '@/event/EventProtocolType' import { EventProtocolType } from '@/event/EventProtocolType'
import { userFactory } from '@/seeds/factory/user' import { userFactory } from '@/seeds/factory/user'
import { import {
@ -118,10 +119,8 @@ describe('send coins', () => {
it('logs the error thrown', async () => { it('logs the error thrown', async () => {
// find peter to check the log // find peter to check the log
const user = await findUserByEmail(peterData.email) const user = await findUserByEmail('stephen@hawking.uk')
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith('The recipient account was deleted', user)
`The recipient account was deleted: recipientUser=${user}`,
)
}) })
}) })
@ -151,10 +150,8 @@ describe('send coins', () => {
it('logs the error thrown', async () => { it('logs the error thrown', async () => {
// find peter to check the log // find peter to check the log
const user = await findUserByEmail(peterData.email) const user = await findUserByEmail('garrick@ollivander.com')
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith('The recipient account is not activated', user)
`The recipient account is not activated: recipientUser=${user}`,
)
}) })
}) })
}) })
@ -181,37 +178,13 @@ describe('send coins', () => {
}), }),
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('Sender and Recipient are the same.')], errors: [new GraphQLError('Sender and Recipient are the same')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Sender and Recipient are the same.') expect(logger.error).toBeCalledWith('Sender and Recipient are the same', expect.any(Number))
})
})
describe('memo text is too long', () => {
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
amount: 100,
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test t',
},
}),
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('memo text is too long (255 characters maximum)')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('memo text is too long: memo.length=256 > 255')
}) })
}) })
@ -229,13 +202,37 @@ describe('send coins', () => {
}), }),
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('memo text is too short (5 characters minimum)')], errors: [new GraphQLError('Memo text is too short')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('memo text is too short: memo.length=4 < 5') expect(logger.error).toBeCalledWith('Memo text is too short', 4)
})
})
describe('memo text is too long', () => {
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
amount: 100,
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test t',
},
}),
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('Memo text is too long')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Memo text is too long', 256)
}) })
}) })
@ -253,15 +250,13 @@ describe('send coins', () => {
}), }),
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError(`user hasn't enough GDD or amount is < 0`)], errors: [new GraphQLError('User has not enough GDD or amount is < 0')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith('User has not enough GDD or amount is < 0', null)
`user hasn't enough GDD or amount is < 0 : balance=null`,
)
}) })
}) })
}) })
@ -293,6 +288,7 @@ describe('send coins', () => {
describe('trying to send negative amount', () => { describe('trying to send negative amount', () => {
it('throws an error', async () => { it('throws an error', async () => {
jest.clearAllMocks()
expect( expect(
await mutate({ await mutate({
mutation: sendCoins, mutation: sendCoins,
@ -304,13 +300,13 @@ describe('send coins', () => {
}), }),
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError(`Amount to send must be positive`)], errors: [new GraphQLError('Amount to send must be positive')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(`Amount to send must be positive`) expect(logger.error).toBeCalledWith('Amount to send must be positive', new Decimal(-50))
}) })
}) })
@ -334,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) // Find the exact transaction (sent one is the one with user[1] as user)
const transaction = await Transaction.find({ const transaction = await Transaction.find({
userId: user[1].id, userId: user[1].id,
@ -351,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) // Find the exact transaction (received one is the one with user[0] as user)
const transaction = await Transaction.find({ const transaction = await Transaction.find({
userId: user[0].id, userId: user[0].id,

View File

@ -29,14 +29,14 @@ import {
sendTransactionLinkRedeemedEmail, sendTransactionLinkRedeemedEmail,
sendTransactionReceivedEmail, sendTransactionReceivedEmail,
} from '@/emails/sendEmailVariants' } from '@/emails/sendEmailVariants'
import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event' import { EVENT_TRANSACTION_RECEIVE, EVENT_TRANSACTION_SEND } from '@/event/Event'
import { writeEvent } from '@/event/EventProtocolEmitter'
import { BalanceResolver } from './BalanceResolver' import { BalanceResolver } from './BalanceResolver'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
import { findUserByEmail } from './UserResolver' import { findUserByEmail } from './UserResolver'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction' import { getLastTransaction } from './util/getLastTransaction'
@ -55,18 +55,15 @@ export const executeTransaction = async (
) )
if (sender.id === recipient.id) { if (sender.id === recipient.id) {
logger.error(`Sender and Recipient are the same.`) throw new LogError('Sender and Recipient are the same', sender.id)
throw new Error('Sender and Recipient are the same.')
}
if (memo.length > MEMO_MAX_CHARS) {
logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`)
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
} }
if (memo.length < MEMO_MIN_CHARS) { if (memo.length < MEMO_MIN_CHARS) {
logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`) throw new LogError('Memo text is too short', memo.length)
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`) }
if (memo.length > MEMO_MAX_CHARS) {
throw new LogError('Memo text is too long', memo.length)
} }
// validate amount // validate amount
@ -79,8 +76,7 @@ export const executeTransaction = async (
) )
logger.debug(`calculated Balance=${sendBalance}`) logger.debug(`calculated Balance=${sendBalance}`)
if (!sendBalance) { if (!sendBalance) {
logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`) throw new LogError('User has not enough GDD or amount is < 0', sendBalance)
throw new Error("user hasn't enough GDD or amount is < 0")
} }
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
@ -141,23 +137,22 @@ export const executeTransaction = async (
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.info(`commit Transaction successful...`) logger.info(`commit Transaction successful...`)
const eventTransactionSend = new EventTransactionSend() await EVENT_TRANSACTION_SEND(
eventTransactionSend.userId = transactionSend.userId transactionSend.userId,
eventTransactionSend.xUserId = transactionSend.linkedUserId transactionSend.linkedUserId,
eventTransactionSend.transactionId = transactionSend.id transactionSend.id,
eventTransactionSend.amount = transactionSend.amount.mul(-1) transactionSend.amount.mul(-1),
await writeEvent(new Event().setEventTransactionSend(eventTransactionSend)) )
const eventTransactionReceive = new EventTransactionReceive() await EVENT_TRANSACTION_RECEIVE(
eventTransactionReceive.userId = transactionReceive.userId transactionReceive.userId,
eventTransactionReceive.xUserId = transactionReceive.linkedUserId transactionReceive.linkedUserId,
eventTransactionReceive.transactionId = transactionReceive.id transactionReceive.id,
eventTransactionReceive.amount = transactionReceive.amount transactionReceive.amount,
await writeEvent(new Event().setEventTransactionReceive(eventTransactionReceive)) )
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error(`Transaction was not successful: ${e}`) throw new LogError('Transaction was not successful', e)
throw new Error(`Transaction was not successful: ${e}`)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
@ -316,8 +311,7 @@ export class TransactionResolver {
): Promise<boolean> { ): Promise<boolean> {
logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`) logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`)
if (amount.lte(0)) { if (amount.lte(0)) {
logger.error(`Amount to send must be positive`) throw new LogError('Amount to send must be positive', amount)
throw new Error('Amount to send must be positive')
} }
// TODO this is subject to replay attacks // TODO this is subject to replay attacks
@ -326,13 +320,11 @@ export class TransactionResolver {
// validate recipient user // validate recipient user
const recipientUser = await findUserByEmail(email) const recipientUser = await findUserByEmail(email)
if (recipientUser.deletedAt) { if (recipientUser.deletedAt) {
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`) throw new LogError('The recipient account was deleted', recipientUser)
throw new Error('The recipient account was deleted')
} }
const emailContact = recipientUser.emailContact const emailContact = recipientUser.emailContact
if (!emailContact.emailChecked) { if (!emailContact.emailChecked) {
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`) throw new LogError('The recipient account is not activated', recipientUser)
throw new Error('The recipient account is not activated')
} }
await executeTransaction(amount, memo, senderUser, recipientUser) await executeTransaction(amount, memo, senderUser, recipientUser)

View File

@ -19,6 +19,7 @@ import {
setUserRole, setUserRole,
deleteUser, deleteUser,
unDeleteUser, unDeleteUser,
sendActivationEmail,
} from '@/seeds/graphql/mutations' } from '@/seeds/graphql/mutations'
import { verifyLogin, queryOptIn, searchAdminUsers, searchUsers } from '@/seeds/graphql/queries' import { verifyLogin, queryOptIn, searchAdminUsers, searchUsers } from '@/seeds/graphql/queries'
import { GraphQLError } from 'graphql' 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', () => { 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(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.SEND_CONFIRMATION_EMAIL, type: EventProtocolType.SEND_CONFIRMATION_EMAIL,
@ -206,7 +220,7 @@ describe('UserResolver', () => {
}) })
}) })
describe('email already exists', () => { describe('user already exists', () => {
let mutation: User let mutation: User
beforeAll(async () => { beforeAll(async () => {
mutation = await mutate({ mutation: createUser, variables }) 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', () => { 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(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ACTIVATE_ACCOUNT, 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(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.REDEEM_REGISTER, 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( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.REDEEM_REGISTER, type: EventProtocolType.REDEEM_REGISTER,
@ -647,6 +674,19 @@ describe('UserResolver', () => {
it('sets the token in the header', () => { it('sets the token in the header', () => {
expect(headerPushMock).toBeCalledWith({ key: 'token', value: expect.any(String) }) 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', () => { 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(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.LOGIN, 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('unDelete user', () => {
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('returns an error', async () => { it('returns an error', async () => {

View File

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

View File

@ -1,10 +1,5 @@
{ {
"emails": { "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": { "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:", "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.", "emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.",
@ -19,6 +14,11 @@
"onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.",
"subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail" "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": { "contributionConfirmed": {
"commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.", "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" "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt"

View File

@ -1,10 +1,5 @@
{ {
"emails": { "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": { "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:", "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.", "emailRegistered": "Your email address has just been registered with Gradido.",
@ -19,6 +14,11 @@
"onForgottenPasswordCopyLink": "or copy the link above into your browser window.", "onForgottenPasswordCopyLink": "or copy the link above into your browser window.",
"subject": "Gradido: Try To Register Again With Your Email" "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": { "contributionConfirmed": {
"commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.", "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" "subject": "Gradido: Your contribution to the common good was confirmed"

View File

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

View File

@ -1,5 +1,9 @@
<template> <template>
<div class="decayinformation-startblock"> <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"> <div class="mt-3 mb-3 text-center">
<b>{{ $t('decay.before_startblock_transaction') }}</b> <b>{{ $t('decay.before_startblock_transaction') }}</b>
</div> </div>
@ -8,5 +12,11 @@
<script> <script>
export default { export default {
name: 'DecayInformation-StartBlock', name: 'DecayInformation-StartBlock',
props: {
memo: {
type: String,
required: true,
},
},
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="decay-information-box"> <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 <decay-information-decay-startblock
v-else-if="isStartBlock" v-else-if="isStartBlock"
:amount="amount" :amount="amount"

View File

@ -179,6 +179,17 @@ export default {
}, },
}, },
computed: { 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() { isBalanceDisabled() {
return this.balance <= 0 ? 'disabled' : false return this.balance <= 0 ? 'disabled' : false
}, },

View File

@ -3,20 +3,20 @@
<redeem-information v-bind="linkData" :isContributionLink="isContributionLink" /> <redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
<b-jumbotron> <b-jumbotron>
<div class="mb-6"> <div class="mb-2">
<h2>{{ $t('gdd_per_link.redeem') }}</h2> <h2>{{ $t('gdd_per_link.redeem') }}</h2>
</div> </div>
<b-row> <b-row>
<b-col col sm="12" md="6"> <b-col sm="12" md="6">
<p>{{ $t('gdd_per_link.no-account') }}</p> <p>{{ $t('gdd_per_link.no-account') }}</p>
<b-button variant="primary" :to="register"> <b-button variant="primary" :to="register">
{{ $t('gdd_per_link.to-register') }} {{ $t('gdd_per_link.to-register') }}
</b-button> </b-button>
</b-col> </b-col>
<b-col sm="12" md="6" class="mt-xs-6 mt-sm-6 mt-md-0"> <b-col sm="12" md="6" class="mt-4 mt-lg-0">
<p>{{ $t('gdd_per_link.has-account') }}</p> <p>{{ $t('gdd_per_link.has-account') }}</p>
<b-button variant="info" :to="login">{{ $t('gdd_per_link.to-login') }}</b-button> <b-button variant="gradido" :to="login">{{ $t('gdd_per_link.to-login') }}</b-button>
</b-col> </b-col>
</b-row> </b-row>
</b-jumbotron> </b-jumbotron>

View File

@ -3,7 +3,7 @@
<redeem-information v-bind="linkData" :isContributionLink="isContributionLink" /> <redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
<b-jumbotron> <b-jumbotron>
<div class="mb-3 text-center"> <div class="mb-3 text-center">
<b-button variant="primary" @click="$emit('mutation-link', linkData.amount)" size="lg"> <b-button variant="gradido" @click="$emit('mutation-link', linkData.amount)" size="lg">
{{ $t('gdd_per_link.redeem') }} {{ $t('gdd_per_link.redeem') }}
</b-button> </b-button>
</div> </div>

View File

@ -23,15 +23,17 @@
</b-col> </b-col>
</b-row> </b-row>
<b-row> <b-row>
<b-col class="d-flex justify-content-end"> <b-col class="d-flex justify-content-end mb-4 mb-lg-0">
<router-link to="/forgot-password" class="mt-3"> <router-link to="/forgot-password">
{{ $t('settings.password.forgot_pwd') }} {{ $t('settings.password.forgot_pwd') }}
</router-link> </router-link>
</b-col> </b-col>
</b-row> </b-row>
<div class="mt-5"> <b-row>
<b-button type="submit" variant="gradido">{{ $t('login') }}</b-button> <b-col cols="12" lg="4">
</div> <b-button type="submit" variant="gradido" block>{{ $t('login') }}</b-button>
</b-col>
</b-row>
</b-form> </b-form>
</validation-observer> </validation-observer>
</b-container> </b-container>

View File

@ -68,25 +68,30 @@
></input-email> ></input-email>
</b-col> </b-col>
</b-row> </b-row>
<div class="my-4"> <b-row>
<b-form-checkbox <b-col cols="12" class="my-4">
id="registerCheckbox" <b-form-checkbox
v-model="form.agree" id="registerCheckbox"
:name="$t('site.signup.agree')" v-model="form.agree"
> :name="$t('site.signup.agree')"
<!-- eslint-disable-next-line @intlify/vue-i18n/no-v-html --> >
<span class="text-muted" v-html="$t('site.signup.agree')"></span> <!-- eslint-disable-next-line @intlify/vue-i18n/no-v-html -->
</b-form-checkbox> <span class="text-muted" v-html="$t('site.signup.agree')"></span>
</div> </b-form-checkbox>
<div> </b-col>
<b-button </b-row>
type="submit" <b-row>
:disabled="disabled" <b-col cols="12" lg="5">
:variant="disabled ? 'gradido-disable' : 'gradido'" <b-button
> block
{{ $t('signup') }} type="submit"
</b-button> :disabled="disabled"
</div> :variant="disabled ? 'gradido-disable' : 'gradido'"
>
{{ $t('signup') }}
</b-button>
</b-col>
</b-row>
</b-form> </b-form>
</validation-observer> </validation-observer>
</b-container> </b-container>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="show-transaction-link-informations"> <div class="show-transaction-link-informations">
<b-container class="mt-4"> <div class="mt-4">
<transaction-link-item :type="itemType"> <transaction-link-item :type="itemType">
<template #LOGGED_OUT> <template #LOGGED_OUT>
<redeem-logged-out :linkData="linkData" :isContributionLink="isContributionLink" /> <redeem-logged-out :linkData="linkData" :isContributionLink="isContributionLink" />
@ -22,7 +22,7 @@
<redeemed-text-box :text="redeemedBoxText" /> <redeemed-text-box :text="redeemedBoxText" />
</template> </template>
</transaction-link-item> </transaction-link-item>
</b-container> </div>
</div> </div>
</template> </template>
<script> <script>