Merge branch 'master' into logger_transactionResolver

This commit is contained in:
Ulf Gebhardt 2023-02-14 16:18:29 +01:00 committed by GitHub
commit 18242ccfea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 711 additions and 1101 deletions

View File

@ -360,6 +360,25 @@ jobs:
- name: backend | Lint
run: docker run --rm gradido/backend:test yarn run lint
##############################################################################
# JOB: LOCALES BACKEND #######################################################
##############################################################################
locales_backend:
name: Locales - Backend
runs-on: ubuntu-latest
needs: [build_test_backend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# LOCALES BACKEND #####################################################
##########################################################################
- name: Backend | Locales
run: cd backend && yarn && yarn locales
##############################################################################
# JOB: LINT DATABASE UP ######################################################
##############################################################################

View File

@ -4,8 +4,57 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [1.18.2](https://github.com/gradido/gradido/compare/1.18.1...1.18.2)
- fix(admin): deny contribution button to left [`#2699`](https://github.com/gradido/gradido/pull/2699)
#### [1.18.1](https://github.com/gradido/gradido/compare/1.18.0...1.18.1)
> 10 February 2023
- chore(release): version 1.18.1 [`#2698`](https://github.com/gradido/gradido/pull/2698)
- fix(frontend): fix is last month for empty form date [`#2697`](https://github.com/gradido/gradido/pull/2697)
- fix(frontend): community link [`#2696`](https://github.com/gradido/gradido/pull/2696)
#### [1.18.0](https://github.com/gradido/gradido/compare/1.17.1...1.18.0)
> 9 February 2023
- feat(release): version 1.18.0 [`#2690`](https://github.com/gradido/gradido/pull/2690)
- refactor(frontend): toast by automatically logged out [`#2681`](https://github.com/gradido/gradido/pull/2681)
- refactor(frontend): change text for gdd_per_link.choose-amount [`#2638`](https://github.com/gradido/gradido/pull/2638)
- fix(backend): emails for deny and delete contribution [`#2688`](https://github.com/gradido/gradido/pull/2688)
- refactor(other): remove config version from `.env.dist` [`#2686`](https://github.com/gradido/gradido/pull/2686)
- refactor(backend): use LogError on contributionMessageResolver [`#2663`](https://github.com/gradido/gradido/pull/2663)
- refactor(backend): get last transaction by only one function [`#2668`](https://github.com/gradido/gradido/pull/2668)
- refactor(other): don't rebuild modul if unit test file has been changed [`#2667`](https://github.com/gradido/gradido/pull/2667)
- refactor(backend): use LogError on contributionLinkResolver [`#2662`](https://github.com/gradido/gradido/pull/2662)
- refactor(backend): remove event protocol config switch [`#2670`](https://github.com/gradido/gradido/pull/2670)
- refactor(backend): event protocol [`#2652`](https://github.com/gradido/gradido/pull/2652)
- refactor(frontend): sidebar becomes smaller when critical phase [`#2649`](https://github.com/gradido/gradido/pull/2649)
- refactor(backend): use LogError on sendEMailTranslated [`#2656`](https://github.com/gradido/gradido/pull/2656)
- refactor(backend): log error class [`#2640`](https://github.com/gradido/gradido/pull/2640)
- feat(backend): add filterState parameter to listAllContributions query [`#2619`](https://github.com/gradido/gradido/pull/2619)
- refactor(frontend): there is no message when a month is fully created [`#2626`](https://github.com/gradido/gradido/pull/2626)
- refactor(frontend): better text alignment on send via link [`#2637`](https://github.com/gradido/gradido/pull/2637)
- refactor(frontend): text changed as indicated in the issues [`#2642`](https://github.com/gradido/gradido/pull/2642)
- refactor(frontend): when you click on create, you will be directed to the form [`#2645`](https://github.com/gradido/gradido/pull/2645)
- feat(backend): federation: separated dht-hub features in new dht-node modul [`#2510`](https://github.com/gradido/gradido/pull/2510)
- refactor(backend): refine assembly of error message in user resolver [`#2636`](https://github.com/gradido/gradido/pull/2636)
- fix(backend): unit tests creations for 31st day [`#2641`](https://github.com/gradido/gradido/pull/2641)
- fix(workflow): properly lint pr - prevent requirement to restart linting [`#2635`](https://github.com/gradido/gradido/pull/2635)
- feat(frontend): 'yes'-button shows which dialog is currently open with a different color [`#2629`](https://github.com/gradido/gradido/pull/2629)
- feat(backend): federation implement multiple apollo graphql endpoints [`#2459`](https://github.com/gradido/gradido/pull/2459)
- refactor(frontend): add legend to all contribution tab, and add tests. [`#2625`](https://github.com/gradido/gradido/pull/2625)
- feat(frontend): unit tests community page [`#2587`](https://github.com/gradido/gradido/pull/2587)
- feat(backend): deny contributions [`#2461`](https://github.com/gradido/gradido/pull/2461)
- refactor(admin): update yarn.lock file of admin. [`#2579`](https://github.com/gradido/gradido/pull/2579)
#### [1.17.1](https://github.com/gradido/gradido/compare/1.17.0...1.17.1)
> 20 January 2023
- chore(release): v1.17.1 [`#2588`](https://github.com/gradido/gradido/pull/2588)
- refactor(frontend): change contribution memo add word-break [`#2583`](https://github.com/gradido/gradido/pull/2583)
- refactor(admin): add text-break on all table memo fields [`#2584`](https://github.com/gradido/gradido/pull/2584)
- fix(frontend): throw proper frontend warning errors [`#2586`](https://github.com/gradido/gradido/pull/2586)

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=v1.2022-03-18
GRAPHQL_URI=http://localhost:4000/graphql
WALLET_AUTH_URL=http://localhost/authenticate?token={token}
WALLET_URL=http://localhost/login

View File

@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido",
"main": "index.js",
"author": "Moriz Wahl",
"version": "1.17.1",
"version": "1.18.2",
"license": "Apache-2.0",
"private": false,
"scripts": {
@ -86,5 +86,10 @@
"> 1%",
"last 2 versions",
"not ie <= 10"
]
],
"nodemonConfig": {
"ignore": [
"**/*.spec.js"
]
}
}

View File

@ -42,14 +42,30 @@ describe('ContributionLink', () => {
expect(wrapper.find('div.contribution-link').exists()).toBe(true)
})
it('emits toggle::collapse new Contribution', async () => {
wrapper.vm.editContributionLinkData()
expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy()
describe('function editContributionLinkData', () => {
beforeEach(() => {
wrapper.vm.editContributionLinkData()
})
it('emits toggle::collapse new Contribution', async () => {
await expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy()
})
})
it('emits toggle::collapse close Contribution-Form ', async () => {
wrapper.vm.closeContributionForm()
expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy()
describe('function closeContributionForm', () => {
beforeEach(async () => {
await wrapper.setData({ visible: true })
wrapper.vm.closeContributionForm()
})
it('emits toggle::collapse close Contribution-Form ', async () => {
await expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy()
})
it('editContributionLink is false', async () => {
await expect(wrapper.vm.editContributionLink).toBe(false)
})
it('contributionLinkData is empty', async () => {
await expect(wrapper.vm.contributionLinkData).toEqual({})
})
})
})
})

View File

@ -88,5 +88,16 @@ describe('CreationTransactionList', () => {
expect(toastErrorSpy).toBeCalledWith('OUCH!')
})
})
describe('watch currentPage', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setData({ currentPage: 2 })
})
it('returns the string in normal order if reversed property is not true', () => {
expect(wrapper.vm.currentPage).toBe(2)
})
})
})
})

View File

@ -46,39 +46,31 @@ describe('NavBar', () => {
})
describe('Navbar Menu', () => {
it('has a link to overview', () => {
expect(wrapper.findAll('.nav-item').at(0).find('a').attributes('href')).toBe('/')
})
it('has a link to /user', () => {
expect(wrapper.findAll('.nav-item').at(1).find('a').attributes('href')).toBe('/user')
})
it('has a link to /creation', () => {
expect(wrapper.findAll('.nav-item').at(2).find('a').attributes('href')).toBe('/creation')
expect(wrapper.findAll('.nav-item').at(0).find('a').attributes('href')).toBe('/user')
})
it('has a link to /creation-confirm', () => {
expect(wrapper.findAll('.nav-item').at(3).find('a').attributes('href')).toBe(
expect(wrapper.findAll('.nav-item').at(1).find('a').attributes('href')).toBe(
'/creation-confirm',
)
})
it('has a link to /contribution-links', () => {
expect(wrapper.findAll('.nav-item').at(4).find('a').attributes('href')).toBe(
expect(wrapper.findAll('.nav-item').at(2).find('a').attributes('href')).toBe(
'/contribution-links',
)
})
it('has a link to /statistic', () => {
expect(wrapper.findAll('.nav-item').at(5).find('a').attributes('href')).toBe('/statistic')
expect(wrapper.findAll('.nav-item').at(3).find('a').attributes('href')).toBe('/statistic')
})
})
describe('wallet', () => {
const assignLocationSpy = jest.fn()
beforeEach(async () => {
await wrapper.findAll('.nav-item').at(6).find('a').trigger('click')
await wrapper.findAll('.nav-item').at(5).find('a').trigger('click')
})
it.skip('changes window location to wallet', () => {
@ -97,7 +89,7 @@ describe('NavBar', () => {
window.location = {
assign: windowLocationMock,
}
await wrapper.findAll('.nav-item').at(7).find('a').trigger('click')
await wrapper.findAll('.nav-item').at(5).find('a').trigger('click')
})
it('redirects to /logout', () => {

View File

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

View File

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

View File

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

View File

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

View File

@ -32,7 +32,6 @@
"creation": "Creation",
"creationList": "Creation list",
"creation_form": {
"creation_failed": "Could not create pending creation for {email}",
"creation_for": "Active Basic Income for",
"enter_text": "Enter text",
"form": "Creation form",
@ -87,7 +86,6 @@
"lastname": "Lastname",
"math": {
"equals": "=",
"exclaim": "!",
"pipe": "|",
"plus": "+"
},
@ -95,15 +93,12 @@
"request": "Request has been sent."
},
"moderator": "Moderator",
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
"name": "Name",
"navbar": {
"automaticContributions": "Automatic Contributions",
"logout": "Logout",
"multi_creation": "Multiple creation",
"my-account": "My Account",
"open_creation": "Open creations",
"overview": "Overview",
"statistic": "Statistic",
"user_search": "User search"
},
@ -132,9 +127,7 @@
}
},
"redeemed": "redeemed",
"remove": "Remove",
"removeNotSelf": "As an admin/moderator, you cannot delete yourself.",
"remove_all": "Remove all users",
"save": "Speichern",
"statistic": {
"activeUsers": "Active members",

View File

@ -0,0 +1,18 @@
import locales from './index.js'
describe('locales', () => {
it('should contain 2 locales', () => {
expect(locales).toHaveLength(2)
})
it('should contain a German locale', () => {
expect(locales).toContainEqual(
expect.objectContaining({
name: 'Deutsch',
code: 'de',
iso: 'de-DE',
enabled: true,
}),
)
})
})

View File

@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils'
import ContributionLinks from './ContributionLinks.vue'
import { listContributionLinks } from '@/graphql/listContributionLinks.js'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue
@ -46,13 +47,31 @@ describe('ContributionLinks', () => {
beforeEach(() => {
wrapper = Wrapper()
})
describe('apollo returns', () => {
it('calls listContributionLinks', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
query: listContributionLinks,
}),
)
})
})
it('calls listContributionLinks', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
query: listContributionLinks,
}),
)
describe.skip('query transaction with error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({ message: 'OUCH!' })
wrapper = Wrapper()
})
it('calls the API', () => {
expect(apolloQueryMock).toBeCalled()
})
it('toast error', () => {
expect(toastErrorSpy).toBeCalledWith(
'listContributionLinks has no result, use default data',
)
})
})
})
})

View File

@ -1,337 +0,0 @@
import { mount } from '@vue/test-utils'
import Creation from './Creation.vue'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
searchUsers: {
userCount: 2,
userList: [
{
userId: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
emailChecked: true,
},
{
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
emailChecked: true,
},
],
},
},
})
const storeCommitMock = jest.fn()
const mocks = {
$t: jest.fn((t, options) => (options ? [t, options] : t)),
$d: jest.fn((d) => d),
$apollo: {
query: apolloQueryMock,
},
$store: {
commit: storeCommitMock,
state: {
userSelectedInMassCreation: [],
},
},
}
describe('Creation', () => {
let wrapper
const Wrapper = () => {
return mount(Creation, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('has a DIV element with the class.creation', () => {
expect(wrapper.find('div.creation').exists()).toBeTruthy()
})
describe('apollo returns user array', () => {
it('calls the searchUser query', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
filters: {
byActivated: true,
byDeleted: false,
},
},
}),
)
})
it('has two rows in the left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(2)
})
it('has nwo rows in the right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
})
it('has correct data in first row ', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain('Bibi')
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'Bloxberg',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'200 | 400 | 600',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'bibi@bloxberg.de',
)
})
it('has correct data in second row ', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'Benjamin',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'Blümchen',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'800 | 600 | 400',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'benjamin@bluemchen.de',
)
})
})
describe('push item', () => {
beforeEach(() => {
wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).find('button').trigger('click')
})
it('has one item in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(1)
})
it('has one item in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(1)
})
it('has the correct user in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'bibi@bloxberg.de',
)
})
it('has the correct user in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr').at(0).text()).toContain(
'benjamin@bluemchen.de',
)
})
it('updates userSelectedInMassCreation in store', () => {
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [
{
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
emailChecked: true,
},
])
})
describe('remove item', () => {
beforeEach(async () => {
await wrapper
.findAll('table')
.at(1)
.findAll('tbody > tr')
.at(0)
.find('button')
.trigger('click')
})
it('has two items in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(2)
})
it('has the removed user in first row', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'benjamin@bluemchen.de',
)
})
it('has no items in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
})
it('commits empty array as userSelectedInMassCreation', () => {
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [])
})
})
describe('remove all bookmarks', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('button.btn-light').trigger('click')
})
it('has no items in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
})
it('commits empty array to userSelectedInMassCreation', () => {
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [])
})
it('calls searchUsers', () => {
expect(apolloQueryMock).toBeCalled()
})
})
})
describe('store has items in userSelectedInMassCreation', () => {
beforeEach(() => {
mocks.$store.state.userSelectedInMassCreation = [
{
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
emailChecked: true,
},
]
wrapper = Wrapper()
})
it('has one item in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(1)
})
it('has one item in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(1)
})
it('has the stored user in second row', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr').at(0).text()).toContain(
'benjamin@bluemchen.de',
)
})
})
describe('failed creations', () => {
beforeEach(async () => {
await wrapper
.findComponent({ name: 'CreationFormular' })
.vm.$emit('toast-failed-creations', ['bibi@bloxberg.de', 'benjamin@bluemchen.de'])
})
it('toasts two error messages', () => {
expect(toastErrorSpy).toBeCalledWith([
'creation_form.creation_failed',
{ email: 'bibi@bloxberg.de' },
])
expect(toastErrorSpy).toBeCalledWith([
'creation_form.creation_failed',
{ email: 'benjamin@bluemchen.de' },
])
})
})
describe('watchers', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('search criteria', () => {
beforeEach(async () => {
await wrapper.setData({ criteria: 'XX' })
})
it('calls API when criteria changes', async () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: 'XX',
currentPage: 1,
pageSize: 25,
filters: {
byActivated: true,
byDeleted: false,
},
},
}),
)
})
describe('reset search criteria', () => {
it('calls the API', async () => {
jest.clearAllMocks()
await wrapper.find('.test-click-clear-criteria').trigger('click')
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
filters: {
byActivated: true,
byDeleted: false,
},
},
}),
)
})
})
})
it('calls API when currentPage changes', async () => {
await wrapper.setData({ currentPage: 2 })
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 2,
pageSize: 25,
filters: {
byActivated: true,
byDeleted: false,
},
},
}),
)
})
})
describe('apollo returns error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({
message: 'Ouch',
})
wrapper = Wrapper()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch')
})
})
})
})

View File

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

View File

@ -259,7 +259,7 @@ describe('CreationConfirm', () => {
describe('deny creation', () => {
beforeEach(async () => {
await wrapper.findAll('tr').at(1).findAll('button').at(2).trigger('click')
await wrapper.findAll('tr').at(1).findAll('button').at(1).trigger('click')
})
it('opens the overlay', () => {

View File

@ -129,6 +129,7 @@ export default {
fields() {
return [
{ key: 'bookmark', label: this.$t('delete') },
{ key: 'deny', label: this.$t('deny') },
{ key: 'email', label: this.$t('e_mail') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
@ -149,7 +150,6 @@ export default {
},
{ key: 'moderator', label: this.$t('moderator') },
{ key: 'editCreation', label: this.$t('edit') },
{ key: 'deny', label: this.$t('deny') },
{ key: 'confirm', label: this.$t('save') },
]
},

View File

@ -45,7 +45,7 @@ describe('router', () => {
describe('routes', () => {
it('has nine routes defined', () => {
expect(routes).toHaveLength(9)
expect(routes).toHaveLength(8)
})
it('has "/overview" as default', async () => {
@ -67,13 +67,6 @@ describe('router', () => {
})
})
describe('creation', () => {
it('loads the "Creation" component', async () => {
const component = await routes.find((r) => r.path === '/creation').component()
expect(component.default.name).toBe('Creation')
})
})
describe('creation-confirm', () => {
it('loads the "CreationConfirm" component', async () => {
const component = await routes.find((r) => r.path === '/creation-confirm').component()

View File

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

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=v14.2022-12-22
# Server
PORT=4000
JWT_SECRET=secret123
@ -55,9 +53,6 @@ EMAIL_CODE_REQUEST_TIME=10
# Webhook
WEBHOOK_ELOPAGE_SECRET=secret
# EventProtocol
EVENT_PROTOCOL_DISABLED=false
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
# LOG_LEVEL=info

View File

@ -54,9 +54,6 @@ EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME
# Webhook
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
# EventProtocol
EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED
# Federation
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED

View File

@ -1,6 +1,6 @@
{
"name": "gradido-backend",
"version": "1.17.1",
"version": "1.18.2",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend",
@ -15,7 +15,8 @@
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles",
"seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts",
"klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts"
"klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts",
"locales": "scripts/sort.sh"
},
"dependencies": {
"@hyperswarm/dht": "^6.2.0",
@ -72,5 +73,10 @@
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.14.0",
"typescript": "^4.3.4"
},
"nodemonConfig": {
"ignore": [
"**/*.test.ts"
]
}
}

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

@ -0,0 +1,25 @@
#!/bin/bash
ROOT_DIR=$(dirname "$0")/..
tmp=$(mktemp)
exit_code=0
for locale_file in $ROOT_DIR/src/locales/*.json
do
jq -f $(dirname "$0")/sort_filter.jq $locale_file > "$tmp"
if [ "$*" == "--fix" ]
then
mv "$tmp" $locale_file
else
if diff -q "$tmp" $locale_file > /dev/null ;
then
: # all good
else
exit_code=$?
echo "$(basename -- $locale_file) is not sorted by keys"
fi
fi
done
exit $exit_code

View File

@ -0,0 +1,13 @@
def walk(f):
. as $in
| if type == "object" then
reduce keys_unsorted[] as $key
( {}; . + { ($key): ($in[$key] | walk(f)) } ) | f
elif type == "array" then map( walk(f) ) | f
else f
end;
def keys_sort_by(f):
to_entries | sort_by(.key|f ) | from_entries;
walk(if type == "object" then keys_sort_by(ascii_upcase) else . end)

View File

@ -17,7 +17,7 @@ const constants = {
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v14.2022-12-22',
EXPECTED: 'v15.2023-02-07',
CURRENT: '',
},
}
@ -99,11 +99,6 @@ const webhook = {
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
}
const eventProtocol = {
// global switch to enable writing of EventProtocol-Entries
EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
}
// This is needed by graphql-directive-auth
process.env.APP_SECRET = server.JWT_SECRET
@ -139,7 +134,6 @@ const CONFIG = {
...email,
...loginServer,
...webhook,
...eventProtocol,
...federation,
}

View File

@ -10,6 +10,7 @@ import {
sendAccountMultiRegistrationEmail,
sendContributionConfirmedEmail,
sendContributionDeniedEmail,
sendContributionDeletedEmail,
sendResetPasswordEmail,
sendTransactionLinkRedeemedEmail,
sendTransactionReceivedEmail,
@ -438,6 +439,84 @@ describe('sendEmailVariants', () => {
})
})
describe('sendContributionDeletedEmail', () => {
beforeAll(async () => {
result = await sendContributionDeletedEmail({
firstName: 'Peter',
lastName: 'Lustig',
email: 'peter@lustig.de',
language: 'en',
senderFirstName: 'Bibi',
senderLastName: 'Bloxberg',
contributionMemo: 'My contribution.',
})
})
describe('calls "sendEmailTranslated"', () => {
it('with expected parameters', () => {
expect(sendEmailTranslated).toBeCalledWith({
receiver: {
to: 'Peter Lustig <peter@lustig.de>',
},
template: 'contributionDeleted',
locals: {
firstName: 'Peter',
lastName: 'Lustig',
locale: 'en',
senderFirstName: 'Bibi',
senderLastName: 'Bloxberg',
contributionMemo: 'My contribution.',
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
},
})
})
it('has expected result', () => {
expect(result).toMatchObject({
envelope: {
from: 'info@gradido.net',
to: ['peter@lustig.de'],
},
message: expect.any(String),
originalMessage: expect.objectContaining({
to: 'Peter Lustig <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: Your common good contribution was deleted',
html: expect.any(String),
text: expect.stringContaining('GRADIDO: YOUR COMMON GOOD CONTRIBUTION WAS DELETED'),
}),
})
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Your common good contribution was deleted</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Your common good contribution was deleted</h1>',
)
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
expect(result.originalMessage.html).toContain(
'Your public good contribution “My contribution.” was deleted by Bibi Bloxberg.',
)
expect(result.originalMessage.html).toContain(
'To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!',
)
expect(result.originalMessage.html).toContain(
`Link to your account: <a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
)
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
})
})
})
describe('sendResetPasswordEmail', () => {
beforeAll(async () => {
result = await sendResetPasswordEmail({

View File

@ -103,6 +103,32 @@ export const sendContributionConfirmedEmail = (data: {
})
}
export const sendContributionDeletedEmail = (data: {
firstName: string
lastName: string
email: string
language: string
senderFirstName: string
senderLastName: string
contributionMemo: string
}): Promise<Record<string, unknown> | null> => {
return sendEmailTranslated({
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
template: 'contributionDeleted',
locals: {
firstName: data.firstName,
lastName: data.lastName,
locale: data.language,
senderFirstName: data.senderFirstName,
senderLastName: data.senderLastName,
contributionMemo: data.contributionMemo,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
},
})
}
export const sendContributionDeniedEmail = (data: {
firstName: string
lastName: string

View File

@ -0,0 +1,16 @@
doctype html
html(lang=locale)
head
title= t('emails.contributionDeleted.subject')
body
h1(style='margin-bottom: 24px;')= t('emails.contributionDeleted.subject')
#container.col
include ../hello.pug
p= t('emails.contributionDeleted.commonGoodContributionDeleted', { senderFirstName, senderLastName, contributionMemo })
p= t('emails.contributionDeleted.toSeeContributionsAndMessages')
p
= t('emails.general.linkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply')
include ../greatingFormularImprint.pug

View File

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

View File

@ -1,4 +1,3 @@
import { EventProtocol } from '@entity/EventProtocol'
import decimal from 'decimal.js-light'
import { EventProtocolType } from './EventProtocolType'
@ -68,6 +67,7 @@ export class EventTransactionReceiveRedeem extends EventBasicTxX {}
export class EventContributionCreate extends EventBasicCt {}
export class EventAdminContributionCreate extends EventBasicCt {}
export class EventAdminContributionDelete extends EventBasicCt {}
export class EventAdminContributionDeny extends EventBasicCt {}
export class EventAdminContributionUpdate extends EventBasicCt {}
export class EventUserCreateContributionMessage extends EventBasicCtMsg {}
export class EventAdminCreateContributionMessage extends EventBasicCtMsg {}
@ -87,21 +87,6 @@ export class EventDeleteContributionLink extends EventBasicCt {}
export class EventUpdateContributionLink extends EventBasicCt {}
export class Event {
constructor()
constructor(event?: EventProtocol) {
if (event) {
this.id = event.id
this.type = event.type
this.createdAt = event.createdAt
this.userId = event.userId
this.xUserId = event.xUserId
this.xCommunityId = event.xCommunityId
this.transactionId = event.transactionId
this.contributionId = event.contributionId
this.amount = event.amount
}
}
public setEventBasic(): Event {
this.type = EventProtocolType.BASIC
this.createdAt = new Date()
@ -314,6 +299,13 @@ export class Event {
return this
}
public setEventAdminContributionDeny(ev: EventAdminContributionDeny): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_CONTRIBUTION_DENY
return this
}
public setEventAdminContributionUpdate(ev: EventAdminContributionUpdate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_CONTRIBUTION_UPDATE

View File

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

View File

@ -35,6 +35,7 @@ export enum EventProtocolType {
CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE',
ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE',
ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE',
ADMIN_CONTRIBUTION_DENY = 'ADMIN_CONTRIBUTION_DENY',
ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE',
USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE',
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',

View File

@ -15,6 +15,8 @@ import { calculateDecay } from '@/util/decay'
import { RIGHTS } from '@/auth/RIGHTS'
import { GdtResolver } from './GdtResolver'
import { getLastTransaction } from './util/getLastTransaction'
@Resolver()
export class BalanceResolver {
@Authorized([RIGHTS.BALANCE])
@ -32,7 +34,7 @@ export class BalanceResolver {
const lastTransaction = context.lastTransaction
? context.lastTransaction
: await dbTransaction.findOne({ userId: user.id }, { order: { id: 'DESC' } })
: await getLastTransaction(user.id)
logger.debug(`lastTransaction=${lastTransaction}`)

View File

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

View File

@ -40,20 +40,12 @@ export class ContributionLinkResolver {
}: ContributionLinkArgs,
): Promise<ContributionLink> {
isStartEndDateValid(validFrom, validTo)
// TODO: this should be enforced by the schema.
if (!name) {
throw new LogError('The name must be initialized')
}
if (name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS) {
throw new LogError('The value of name is too short', name.length)
}
if (name.length > CONTRIBUTIONLINK_NAME_MAX_CHARS) {
throw new LogError('The value of name is too long', name.length)
}
// TODO: this should be enforced by the schema.
if (!memo) {
throw new LogError('The memo must be initialized')
}
if (memo.length < MEMO_MIN_CHARS) {
throw new LogError('The value of memo is too short', memo.length)
}

View File

@ -99,14 +99,18 @@ describe('ContributionMessageResolver', () => {
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('ContributionMessage was not sent successfully')],
errors: [
new GraphQLError(
'ContributionMessage was not sent successfully: Error: Contribution not found',
),
],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'ContributionMessage was not sent successfully',
'ContributionMessage was not sent successfully: Error: Contribution not found',
new Error('Contribution not found'),
)
})
@ -135,14 +139,18 @@ describe('ContributionMessageResolver', () => {
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('ContributionMessage was not sent successfully')],
errors: [
new GraphQLError(
'ContributionMessage was not sent successfully: Error: Admin can not answer on his own contribution',
),
],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'ContributionMessage was not sent successfully',
'ContributionMessage was not sent successfully: Error: Admin can not answer on his own contribution',
new Error('Admin can not answer on his own contribution'),
)
})
@ -229,14 +237,18 @@ describe('ContributionMessageResolver', () => {
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('ContributionMessage was not sent successfully')],
errors: [
new GraphQLError(
'ContributionMessage was not sent successfully: Error: Contribution not found',
),
],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'ContributionMessage was not sent successfully',
'ContributionMessage was not sent successfully: Error: Contribution not found',
new Error('Contribution not found'),
)
})
@ -257,14 +269,18 @@ describe('ContributionMessageResolver', () => {
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('ContributionMessage was not sent successfully')],
errors: [
new GraphQLError(
'ContributionMessage was not sent successfully: Error: Can not send message to contribution of another user',
),
],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'ContributionMessage was not sent successfully',
'ContributionMessage was not sent successfully: Error: Can not send message to contribution of another user',
new Error('Can not send message to contribution of another user'),
)
})

View File

@ -54,7 +54,7 @@ export class ContributionMessageResolver {
await queryRunner.commitTransaction()
} catch (e) {
await queryRunner.rollbackTransaction()
throw new LogError('ContributionMessage was not sent successfully', e)
throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e)
} finally {
await queryRunner.release()
}
@ -144,7 +144,7 @@ export class ContributionMessageResolver {
await queryRunner.commitTransaction()
} catch (e) {
await queryRunner.rollbackTransaction()
throw new LogError('ContributionMessage was not sent successfully', e)
throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e)
} finally {
await queryRunner.release()
}

View File

@ -22,11 +22,7 @@ import {
listContributions,
listUnconfirmedContributions,
} from '@/seeds/graphql/queries'
import {
// sendAccountActivationEmail,
sendContributionConfirmedEmail,
// sendContributionRejectedEmail,
} from '@/emails/sendEmailVariants'
import { sendContributionConfirmedEmail } from '@/emails/sendEmailVariants'
import {
cleanDB,
resetToken,
@ -48,7 +44,6 @@ import { logger, i18n as localization } from '@test/testSetup'
import { UserInputError } from 'apollo-server-express'
import { ContributionStatus } from '../enum/ContributionStatus'
// mock account activation email to avoid console spam
// mock account activation email to avoid console spam
jest.mock('@/emails/sendEmailVariants', () => {
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
@ -536,7 +531,6 @@ describe('ContributionResolver', () => {
})
})
// TODO: why is this here - this is a different call `adminUpdateContribution` not `updateContribution`
describe('admin tries to update a user contribution', () => {
it('throws an error', async () => {
jest.clearAllMocks()

View File

@ -44,17 +44,21 @@ import {
EventContributionConfirm,
EventAdminContributionCreate,
EventAdminContributionDelete,
EventAdminContributionDeny,
EventAdminContributionUpdate,
} from '@/event/Event'
import { eventProtocol } from '@/event/EventProtocolEmitter'
import { writeEvent } from '@/event/EventProtocolEmitter'
import { calculateDecay } from '@/util/decay'
import {
sendContributionConfirmedEmail,
sendContributionDeletedEmail,
sendContributionDeniedEmail,
} from '@/emails/sendEmailVariants'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction'
@Resolver()
export class ContributionResolver {
@Authorized([RIGHTS.CREATE_CONTRIBUTION])
@ -95,7 +99,7 @@ export class ContributionResolver {
eventCreateContribution.userId = user.id
eventCreateContribution.amount = amount
eventCreateContribution.contributionId = contribution.id
await eventProtocol.writeEvent(event.setEventContributionCreate(eventCreateContribution))
await writeEvent(event.setEventContributionCreate(eventCreateContribution))
return new UnconfirmedContribution(contribution, user, creations)
}
@ -128,7 +132,7 @@ export class ContributionResolver {
eventDeleteContribution.userId = user.id
eventDeleteContribution.contributionId = contribution.id
eventDeleteContribution.amount = contribution.amount
await eventProtocol.writeEvent(event.setEventContributionDelete(eventDeleteContribution))
await writeEvent(event.setEventContributionDelete(eventDeleteContribution))
const res = await contribution.softRemove()
return !!res
@ -281,7 +285,7 @@ export class ContributionResolver {
eventUpdateContribution.userId = user.id
eventUpdateContribution.contributionId = contributionId
eventUpdateContribution.amount = amount
await eventProtocol.writeEvent(event.setEventContributionUpdate(eventUpdateContribution))
await writeEvent(event.setEventContributionUpdate(eventUpdateContribution))
return new UnconfirmedContribution(contributionToUpdate, user, creations)
}
@ -343,9 +347,7 @@ export class ContributionResolver {
eventAdminCreateContribution.userId = moderator.id
eventAdminCreateContribution.amount = amount
eventAdminCreateContribution.contributionId = contribution.id
await eventProtocol.writeEvent(
event.setEventAdminContributionCreate(eventAdminCreateContribution),
)
await writeEvent(event.setEventAdminContributionCreate(eventAdminCreateContribution))
return getUserCreation(emailContact.userId, clientTimezoneOffset)
}
@ -445,9 +447,7 @@ export class ContributionResolver {
eventAdminContributionUpdate.userId = emailContact.user.id
eventAdminContributionUpdate.amount = amount
eventAdminContributionUpdate.contributionId = contributionToUpdate.id
await eventProtocol.writeEvent(
event.setEventAdminContributionUpdate(eventAdminContributionUpdate),
)
await writeEvent(event.setEventAdminContributionUpdate(eventAdminContributionUpdate))
return result
}
@ -523,10 +523,8 @@ export class ContributionResolver {
eventAdminContributionDelete.userId = contribution.userId
eventAdminContributionDelete.amount = contribution.amount
eventAdminContributionDelete.contributionId = contribution.id
await eventProtocol.writeEvent(
event.setEventAdminContributionDelete(eventAdminContributionDelete),
)
sendContributionDeniedEmail({
await writeEvent(event.setEventAdminContributionDelete(eventAdminContributionDelete))
sendContributionDeletedEmail({
firstName: user.firstName,
lastName: user.lastName,
email: user.emailContact.email,
@ -582,16 +580,11 @@ export class ContributionResolver {
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
try {
const lastTransaction = await queryRunner.manager
.createQueryBuilder()
.select('transaction')
.from(DbTransaction, 'transaction')
.where('transaction.userId = :id', { id: contribution.userId })
.orderBy('transaction.id', 'DESC')
.getOne()
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
const lastTransaction = await getLastTransaction(contribution.userId)
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
try {
let newBalance = new Decimal(0)
let decay: Decay | null = null
if (lastTransaction) {
@ -647,7 +640,7 @@ export class ContributionResolver {
eventContributionConfirm.userId = user.id
eventContributionConfirm.amount = contribution.amount
eventContributionConfirm.contributionId = contribution.id
await eventProtocol.writeEvent(event.setEventContributionConfirm(eventContributionConfirm))
await writeEvent(event.setEventContributionConfirm(eventContributionConfirm))
} finally {
releaseLock()
}
@ -737,6 +730,13 @@ export class ContributionResolver {
contributionToUpdate.deniedAt = new Date()
const res = await contributionToUpdate.save()
const event = new Event()
const eventAdminContributionDeny = new EventAdminContributionDeny()
eventAdminContributionDeny.userId = contributionToUpdate.userId
eventAdminContributionDeny.amount = contributionToUpdate.amount
eventAdminContributionDeny.contributionId = contributionToUpdate.id
await writeEvent(event.setEventAdminContributionDeny(eventAdminContributionDeny))
sendContributionDeniedEmail({
firstName: user.firstName,
lastName: user.lastName,

View File

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

View File

@ -34,6 +34,8 @@ import QueryLinkResult from '@union/QueryLinkResult'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction'
// TODO: do not export, test it inside the resolver
export const transactionLinkCode = (date: Date): string => {
const time = date.getTime().toString(16)
@ -262,13 +264,7 @@ export class TransactionLinkResolver {
await queryRunner.manager.insert(DbContribution, contribution)
const lastTransaction = await queryRunner.manager
.createQueryBuilder()
.select('transaction')
.from(DbTransaction, 'transaction')
.where('transaction.userId = :id', { id: user.id })
.orderBy('transaction.id', 'DESC')
.getOne()
const lastTransaction = await getLastTransaction(user.id)
let newBalance = new Decimal(0)
let decay: Decay | null = null

View File

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

View File

@ -48,7 +48,7 @@ import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddle
import { klicktippSignIn } from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS'
import { hasElopageBuys } from '@/util/hasElopageBuys'
import { eventProtocol } from '@/event/EventProtocolEmitter'
import { writeEvent } from '@/event/EventProtocolEmitter'
import {
Event,
EventLogin,
@ -179,7 +179,7 @@ export class UserResolver {
})
const ev = new EventLogin()
ev.userId = user.id
eventProtocol.writeEvent(new Event().setEventLogin(ev))
writeEvent(new Event().setEventLogin(ev))
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
return user
}
@ -251,9 +251,7 @@ export class UserResolver {
})
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
eventSendAccountMultiRegistrationEmail.userId = foundUser.id
eventProtocol.writeEvent(
event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail),
)
writeEvent(event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail))
logger.info(
`sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`,
)
@ -336,7 +334,7 @@ export class UserResolver {
})
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
eventSendConfirmEmail.userId = dbUser.id
eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail))
writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail))
if (!emailSent) {
logger.debug(`Account confirmation link: ${activationLink}`)
@ -354,10 +352,10 @@ export class UserResolver {
if (redeemCode) {
eventRedeemRegister.userId = dbUser.id
await eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
await writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
} else {
eventRegister.userId = dbUser.id
await eventProtocol.writeEvent(event.setEventRegister(eventRegister))
await writeEvent(event.setEventRegister(eventRegister))
}
return new User(dbUser)
@ -477,7 +475,7 @@ export class UserResolver {
const eventActivateAccount = new EventActivateAccount()
eventActivateAccount.userId = user.id
eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount))
writeEvent(event.setEventActivateAccount(eventActivateAccount))
} catch (e) {
await queryRunner.rollbackTransaction()
throw new LogError('Error on writing User and User Contact data', e)
@ -824,9 +822,7 @@ export class UserResolver {
const event = new Event()
const eventSendConfirmationEmail = new EventSendConfirmationEmail()
eventSendConfirmationEmail.userId = user.id
await eventProtocol.writeEvent(
event.setEventSendConfirmationEmail(eventSendConfirmationEmail),
)
await writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmationEmail))
}
return true

View File

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

View File

@ -1,10 +1,5 @@
{
"emails": {
"addedContributionMessage": {
"commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.",
"subject": "Gradido: Nachricht zu deinem Gemeinwohl-Beitrag",
"toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
},
"accountActivation": {
"duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen:",
"emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.",
@ -19,12 +14,22 @@
"onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.",
"subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail"
},
"addedContributionMessage": {
"commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.",
"subject": "Gradido: Nachricht zu deinem Gemeinwohl-Beitrag",
"toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
},
"contributionConfirmed": {
"commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.",
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt"
},
"contributionRejected": {
"commonGoodContributionRejected": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} abgelehnt.",
"contributionDeleted": {
"commonGoodContributionDeleted": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} gelöscht.",
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde gelöscht",
"toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
},
"contributionDenied": {
"commonGoodContributionDenied": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} abgelehnt.",
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde abgelehnt",
"toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
},

View File

@ -1,10 +1,5 @@
{
"emails": {
"addedContributionMessage": {
"commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.",
"subject": "Gradido: Message about your common good contribution",
"toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!"
},
"accountActivation": {
"duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here:",
"emailRegistered": "Your email address has just been registered with Gradido.",
@ -19,10 +14,20 @@
"onForgottenPasswordCopyLink": "or copy the link above into your browser window.",
"subject": "Gradido: Try To Register Again With Your Email"
},
"addedContributionMessage": {
"commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.",
"subject": "Gradido: Message about your common good contribution",
"toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!"
},
"contributionConfirmed": {
"commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.",
"subject": "Gradido: Your contribution to the common good was confirmed"
},
"contributionDeleted": {
"commonGoodContributionDeleted": "Your public good contribution “{contributionMemo}” was deleted by {senderFirstName} {senderLastName}.",
"subject": "Gradido: Your common good contribution was deleted",
"toSeeContributionsAndMessages": "To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!"
},
"contributionDenied": {
"commonGoodContributionDenied": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.",
"subject": "Gradido: Your common good contribution was rejected",

View File

@ -1,6 +1,5 @@
import { UserInterface } from './UserInterface'
// TODO: the generated email_contact is not deleted
export const stephenHawking: UserInterface = {
email: 'stephen@hawking.uk',
firstName: 'Stephen',

View File

@ -1,10 +1,10 @@
import { calculateDecay } from './decay'
import Decimal from 'decimal.js-light'
import { Transaction } from '@entity/Transaction'
import { Decay } from '@model/Decay'
import { getCustomRepository } from '@dbTools/typeorm'
import { TransactionLinkRepository } from '@repository/TransactionLink'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { getLastTransaction } from '../graphql/resolver/util/getLastTransaction'
function isStringBoolean(value: string): boolean {
const lowerValue = value.toLowerCase()
@ -20,7 +20,7 @@ async function calculateBalance(
time: Date,
transactionLink?: dbTransactionLink | null,
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
const lastTransaction = await Transaction.findOne({ userId }, { order: { id: 'DESC' } })
const lastTransaction = await getLastTransaction(userId)
if (!lastTransaction) return null
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)

View File

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

View File

@ -16,17 +16,17 @@ export class EventProtocol extends BaseEntity {
@Column({ name: 'user_id', unsigned: true, nullable: false })
userId: number
@Column({ name: 'x_user_id', unsigned: true, nullable: true })
xUserId: number
@Column({ name: 'x_user_id', type: 'int', unsigned: true, nullable: true })
xUserId: number | null
@Column({ name: 'x_community_id', unsigned: true, nullable: true })
xCommunityId: number
@Column({ name: 'x_community_id', type: 'int', unsigned: true, nullable: true })
xCommunityId: number | null
@Column({ name: 'transaction_id', unsigned: true, nullable: true })
transactionId: number
@Column({ name: 'transaction_id', type: 'int', unsigned: true, nullable: true })
transactionId: number | null
@Column({ name: 'contribution_id', unsigned: true, nullable: true })
contributionId: number
@Column({ name: 'contribution_id', type: 'int', unsigned: true, nullable: true })
contributionId: number | null
@Column({
type: 'decimal',
@ -35,8 +35,8 @@ export class EventProtocol extends BaseEntity {
nullable: true,
transformer: DecimalTransformer,
})
amount: Decimal
amount: Decimal | null
@Column({ name: 'message_id', unsigned: true, nullable: true })
messageId: number
@Column({ name: 'message_id', type: 'int', unsigned: true, nullable: true })
messageId: number | null
}

View File

@ -1,6 +1,6 @@
{
"name": "gradido-database",
"version": "1.17.1",
"version": "1.18.2",
"description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database",

View File

@ -27,7 +27,7 @@ COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
# backend
BACKEND_CONFIG_VERSION=v14.2022-12-22
BACKEND_CONFIG_VERSION=v15.2023-02-07
JWT_EXPIRES_IN=10m
GDT_API_URL=https://gdt.gradido.net
@ -56,9 +56,6 @@ EMAIL_CODE_REQUEST_TIME=10
WEBHOOK_ELOPAGE_SECRET=secret
# EventProtocol
EVENT_PROTOCOL_DISABLED=false
# Federation
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
# on an hash created from this topic

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=v1.2023-01-01
# Database
DB_HOST=localhost
DB_PORT=3306
@ -8,9 +6,6 @@ DB_PASSWORD=
DB_DATABASE=gradido_community
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.dht-node.log
# EventProtocol
EVENT_PROTOCOL_DISABLED=false
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
# LOG_LEVEL=info

View File

@ -8,9 +8,6 @@ DB_PASSWORD=$DB_PASSWORD
DB_DATABASE=gradido_community
TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH
# EventProtocol
EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED
# Federation
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED

View File

@ -9,7 +9,7 @@ const constants = {
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v1.2023-01-01',
EXPECTED: 'v2.2023-02-07',
CURRENT: '',
},
}
@ -28,11 +28,6 @@ const database = {
process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.dht-node.log',
}
const eventProtocol = {
// global switch to enable writing of EventProtocol-Entries
EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
}
const federation = {
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB',
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
@ -55,7 +50,6 @@ const CONFIG = {
...constants,
...server,
...database,
...eventProtocol,
...federation,
}

View File

@ -52,10 +52,6 @@ const community = {
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
}
*/
// const eventProtocol = {
// global switch to enable writing of EventProtocol-Entries
// EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
// }
// This is needed by graphql-directive-auth
// process.env.APP_SECRET = server.JWT_SECRET

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "bootstrap-vue-gradido-wallet",
"version": "1.17.1",
"version": "1.18.2",
"private": true,
"scripts": {
"start": "node run/server.js",
@ -104,5 +104,10 @@
],
"author": "Gradido-Akademie - https://www.gradido.net/",
"license": "Apache-2.0",
"description": "Gradido, the Natural Economy of Life, is a way to worldwide prosperity and peace in harmony with nature. - Gradido, die Natürliche Ökonomie des lebens, ist ein Weg zu weltweitem Wohlstand und Frieden in Harmonie mit der Natur."
"description": "Gradido, the Natural Economy of Life, is a way to worldwide prosperity and peace in harmony with nature. - Gradido, die Natürliche Ökonomie des lebens, ist ein Weg zu weltweitem Wohlstand und Frieden in Harmonie mit der Natur.",
"nodemonConfig": {
"ignore": [
"**/*.spec.js"
]
}
}

View File

@ -24,11 +24,6 @@ describe('ContributionForm', () => {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
$n: jest.fn((n) => n),
$store: {
state: {
creation: ['1000', '1000', '1000'],
},
},
$i18n: {
locale: 'en',
},
@ -61,7 +56,7 @@ describe('ContributionForm', () => {
})
})
describe('dates', () => {
describe('dates and max amounts', () => {
beforeEach(async () => {
await wrapper.setData({
form: {
@ -73,204 +68,176 @@ describe('ContributionForm', () => {
})
})
describe('actual date', () => {
describe('same month', () => {
beforeEach(async () => {
const now = new Date().toISOString()
await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now)
describe('max amount reached for both months', () => {
beforeEach(() => {
wrapper.setProps({
maxGddLastMonth: 0,
maxGddThisMonth: 0,
})
describe('isThisMonth', () => {
it('has true', () => {
expect(wrapper.vm.isThisMonth).toBe(true)
})
wrapper.setData({
form: {
id: null,
date: 'set',
memo: '',
amount: '',
},
})
})
describe.skip('month before', () => {
beforeEach(async () => {
await wrapper
.findComponent({ name: 'BFormDatepicker' })
.vm.$emit('input', wrapper.vm.minimalDate)
})
describe('isThisMonth', () => {
it('has false', () => {
expect(wrapper.vm.isThisMonth).toBe(false)
})
})
it('shows message that no contributions are available', () => {
expect(wrapper.find('[data-test="contribtion-message"]').text()).toBe(
'contribution.noOpenCreation.allMonth',
)
})
})
describe.skip('date in middle of year', () => {
describe('same month', () => {
beforeEach(async () => {
// jest.useFakeTimers('modern')
// jest.setSystemTime(new Date('2020-07-06'))
// await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now)
await wrapper.setData({
maximalDate: new Date(2020, 6, 6),
form: { date: new Date(2020, 6, 6) },
})
})
describe('minimalDate', () => {
it('has "2020-06-01T00:00:00.000Z"', () => {
expect(wrapper.vm.minimalDate.toISOString()).toBe('2020-06-01T00:00:00.000Z')
})
})
describe('isThisMonth', () => {
it('has true', () => {
expect(wrapper.vm.isThisMonth).toBe(true)
})
describe('max amount reached for last month, no date selected', () => {
beforeEach(() => {
wrapper.setProps({
maxGddLastMonth: 0,
})
})
describe('month before', () => {
beforeEach(async () => {
// jest.useFakeTimers('modern')
// jest.setSystemTime(new Date('2020-07-06'))
// console.log('middle of year date now:', wrapper.vm.minimalDate)
// await wrapper
// .findComponent({ name: 'BFormDatepicker' })
// .vm.$emit('input', wrapper.vm.minimalDate)
await wrapper.setData({
maximalDate: new Date(2020, 6, 6),
form: { date: new Date(2020, 5, 6) },
})
})
describe('minimalDate', () => {
it('has "2020-06-01T00:00:00.000Z"', () => {
expect(wrapper.vm.minimalDate.toISOString()).toBe('2020-06-01T00:00:00.000Z')
})
})
describe('isThisMonth', () => {
it('has false', () => {
expect(wrapper.vm.isThisMonth).toBe(false)
})
})
it('shows no message', () => {
expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false)
})
})
describe.skip('date in january', () => {
describe('same month', () => {
beforeEach(async () => {
await wrapper.setData({
maximalDate: new Date(2020, 0, 6),
form: { date: new Date(2020, 0, 6) },
})
describe('max amount reached for last month, last month selected', () => {
beforeEach(async () => {
wrapper.setProps({
maxGddLastMonth: 0,
isThisMonth: false,
})
describe('minimalDate', () => {
it('has "2019-12-01T00:00:00.000Z"', () => {
expect(wrapper.vm.minimalDate.toISOString()).toBe('2019-12-01T00:00:00.000Z')
})
})
describe('isThisMonth', () => {
it('has true', () => {
expect(wrapper.vm.isThisMonth).toBe(true)
})
await wrapper.setData({
form: {
id: null,
date: 'set',
memo: '',
amount: '',
},
})
})
describe('month before', () => {
beforeEach(async () => {
// jest.useFakeTimers('modern')
// jest.setSystemTime(new Date('2020-07-06'))
// console.log('middle of year date now:', wrapper.vm.minimalDate)
// await wrapper
// .findComponent({ name: 'BFormDatepicker' })
// .vm.$emit('input', wrapper.vm.minimalDate)
await wrapper.setData({
maximalDate: new Date(2020, 0, 6),
form: { date: new Date(2019, 11, 6) },
})
})
describe('minimalDate', () => {
it('has "2019-12-01T00:00:00.000Z"', () => {
expect(wrapper.vm.minimalDate.toISOString()).toBe('2019-12-01T00:00:00.000Z')
})
})
describe('isThisMonth', () => {
it('has false', () => {
expect(wrapper.vm.isThisMonth).toBe(false)
})
})
it('shows message that no contributions are available for last month', () => {
expect(wrapper.find('[data-test="contribtion-message"]').text()).toBe(
'contribution.noOpenCreation.lastMonth',
)
})
})
describe.skip('date with the 31st day of the month', () => {
describe('same month', () => {
beforeEach(async () => {
await wrapper.setData({
maximalDate: new Date('2022-10-31T00:00:00.000Z'),
form: { date: new Date('2022-10-31T00:00:00.000Z') },
})
describe('max amount reached for last month, this month selected', () => {
beforeEach(async () => {
wrapper.setProps({
maxGddLastMonth: 0,
isThisMonth: true,
})
await wrapper.setData({
form: {
id: null,
date: 'set',
memo: '',
amount: '',
},
})
})
describe('minimalDate', () => {
it('has "2022-09-01T00:00:00.000Z"', () => {
expect(wrapper.vm.minimalDate.toISOString()).toBe('2022-09-01T00:00:00.000Z')
})
})
describe('isThisMonth', () => {
it('has true', () => {
expect(wrapper.vm.isThisMonth).toBe(true)
})
})
it('shows no message', () => {
expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false)
})
})
describe.skip('date with the 28th day of the month', () => {
describe('same month', () => {
beforeEach(async () => {
await wrapper.setData({
maximalDate: new Date('2023-02-28T00:00:00.000Z'),
form: { date: new Date('2023-02-28T00:00:00.000Z') },
})
describe('max amount reached for this month, no date selected', () => {
beforeEach(() => {
wrapper.setProps({
maxGddThisMonth: 0,
})
})
describe('minimalDate', () => {
it('has "2023-01-01T00:00:00.000Z"', () => {
expect(wrapper.vm.minimalDate.toISOString()).toBe('2023-01-01T00:00:00.000Z')
})
})
describe('isThisMonth', () => {
it('has true', () => {
expect(wrapper.vm.isThisMonth).toBe(true)
})
})
it('shows no message', () => {
expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false)
})
})
describe.skip('date with 29.02.2024 leap year', () => {
describe('same month', () => {
beforeEach(async () => {
await wrapper.setData({
maximalDate: new Date('2024-02-29T00:00:00.000Z'),
form: { date: new Date('2024-02-29T00:00:00.000Z') },
})
describe('max amount reached for this month, this month selected', () => {
beforeEach(async () => {
wrapper.setProps({
maxGddThisMonth: 0,
isThisMonth: true,
})
await wrapper.setData({
form: {
id: null,
date: 'set',
memo: '',
amount: '',
},
})
})
describe('minimalDate', () => {
it('has "2024-01-01T00:00:00.000Z"', () => {
expect(wrapper.vm.minimalDate.toISOString()).toBe('2024-01-01T00:00:00.000Z')
})
})
it('shows message that no contributions are available for last month', () => {
expect(wrapper.find('[data-test="contribtion-message"]').text()).toBe(
'contribution.noOpenCreation.thisMonth',
)
})
})
describe('isThisMonth', () => {
it('has true', () => {
expect(wrapper.vm.isThisMonth).toBe(true)
})
describe('max amount reached for this month, last month selected', () => {
beforeEach(async () => {
wrapper.setProps({
maxGddThisMonth: 0,
isThisMonth: false,
})
await wrapper.setData({
form: {
id: null,
date: 'set',
memo: '',
amount: '',
},
})
})
it('shows no message', () => {
expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false)
})
})
})
describe('default return message', () => {
it('returns an empty string', () => {
expect(wrapper.vm.noOpenCreation).toBe('')
})
})
describe('update amount', () => {
beforeEach(() => {
wrapper.findComponent({ name: 'InputHour' }).vm.$emit('updateAmount', 20)
})
it('updates form amount', () => {
expect(wrapper.vm.form.amount).toBe('400.00')
})
})
describe('watch value', () => {
beforeEach(() => {
wrapper.setProps({
value: {
id: 42,
date: 'set',
memo: 'Some Memo',
amount: '400.00',
},
})
})
it('updates form', () => {
expect(wrapper.vm.form).toEqual({
id: 42,
date: 'set',
memo: 'Some Memo',
amount: '400.00',
})
})
})
@ -477,24 +444,23 @@ describe('ContributionForm', () => {
})
})
describe.skip('on trigger submit', () => {
describe('on trigger submit', () => {
beforeEach(async () => {
await wrapper.find('form').trigger('submit')
})
it('emits "update-contribution"', () => {
expect(wrapper.emitted('update-contribution')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
id: 2,
date: now,
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
amount: '200',
},
]),
]),
)
expect(wrapper.emitted('update-contribution')).toEqual([
[
{
id: 2,
date: now,
hours: 0,
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
amount: '200',
},
],
])
})
})
})

View File

@ -23,10 +23,7 @@
<template #nav-next-year><span></span></template>
</b-form-datepicker>
<div
v-if="(isThisMonth && maxGddThisMonth <= 0) || (!isThisMonth && maxGddLastMonth <= 0)"
class="p-3"
>
<div v-if="showMessage" class="p-3" data-test="contribtion-message">
{{ noOpenCreation }}
</div>
<div v-else>
@ -118,8 +115,8 @@ export default {
}
},
methods: {
updateAmount(amount) {
this.form.amount = (amount * 20).toFixed(2).toString()
updateAmount(hours) {
this.form.amount = (hours * 20).toFixed(2).toString()
},
submit() {
this.$emit(this.form.id ? 'update-contribution' : 'set-contribution', { ...this.form })
@ -135,6 +132,15 @@ export default {
},
},
computed: {
showMessage() {
if (this.maxGddThisMonth <= 0 && this.maxGddLastMonth <= 0) return true
if (this.form.date)
return (
(this.isThisMonth && this.maxGddThisMonth <= 0) ||
(!this.isThisMonth && this.maxGddLastMonth <= 0)
)
return false
},
disabled() {
return (
this.form.date === '' ||

View File

@ -116,5 +116,15 @@ describe('ContributionList', () => {
expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 2 }]])
})
})
describe('update status', () => {
beforeEach(() => {
wrapper.findComponent({ name: 'ContributionListItem' }).vm.$emit('update-state', { id: 2 })
})
it('emits update status', () => {
expect(wrapper.emitted('update-state')).toEqual([[{ id: 2 }]])
})
})
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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