mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into archive_transform_valid_transactions
This commit is contained in:
commit
9473d075fa
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
.dbeaver
|
||||
.project
|
||||
*.log
|
||||
*.bak
|
||||
/node_modules/*
|
||||
messages.pot
|
||||
nbproject
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@ -4,8 +4,26 @@ 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.10.0](https://github.com/gradido/gradido/compare/1.9.0...1.10.0)
|
||||
|
||||
- frontend redeem contribution link [`#1988`](https://github.com/gradido/gradido/pull/1988)
|
||||
- change new start picture [`#1990`](https://github.com/gradido/gradido/pull/1990)
|
||||
- feat: Redeem Contribution Link [`#1987`](https://github.com/gradido/gradido/pull/1987)
|
||||
- fix: Max Amount on Slider for Edit Contribution [`#1986`](https://github.com/gradido/gradido/pull/1986)
|
||||
- CRUD contribution link admin interface [`#1981`](https://github.com/gradido/gradido/pull/1981)
|
||||
- fix: `.env` log level for apollo and backend category [`#1967`](https://github.com/gradido/gradido/pull/1967)
|
||||
- refactor: Admin Pending Creations Table to Contributions Table [`#1949`](https://github.com/gradido/gradido/pull/1949)
|
||||
- devops: Update Browser List for Unit Tests as Recomended [`#1984`](https://github.com/gradido/gradido/pull/1984)
|
||||
- feat: CRUD for Contribution Links in Admin Resolver [`#1979`](https://github.com/gradido/gradido/pull/1979)
|
||||
- 1920 feature create contribution link table [`#1957`](https://github.com/gradido/gradido/pull/1957)
|
||||
- refactor: 🍰 Delete `user_setting` Table From DB [`#1960`](https://github.com/gradido/gradido/pull/1960)
|
||||
- locales link german, english navbar [`#1969`](https://github.com/gradido/gradido/pull/1969)
|
||||
|
||||
#### [1.9.0](https://github.com/gradido/gradido/compare/1.8.3...1.9.0)
|
||||
|
||||
> 2 June 2022
|
||||
|
||||
- devops: Release Version 1.9.0 [`#1968`](https://github.com/gradido/gradido/pull/1968)
|
||||
- refactor: 🍰 Refactor To `filters` Object And Rename Filters Properties [`#1914`](https://github.com/gradido/gradido/pull/1914)
|
||||
- refactor register button position [`#1964`](https://github.com/gradido/gradido/pull/1964)
|
||||
- fixed redeem link is mobile start false [`#1958`](https://github.com/gradido/gradido/pull/1958)
|
||||
|
||||
@ -22,7 +22,7 @@ module.exports = {
|
||||
'^.+\\.(js|jsx)?$': 'babel-jest',
|
||||
'<rootDir>/node_modules/vee-validate/dist/rules': 'babel-jest',
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.js'],
|
||||
setupFiles: ['<rootDir>/test/testSetup.js', 'jest-canvas-mock'],
|
||||
testMatch: ['**/?(*.)+(spec|test).js?(x)'],
|
||||
// snapshotSerializers: ['jest-serializer-vue'],
|
||||
transformIgnorePatterns: ['<rootDir>/node_modules/(?!vee-validate/dist/rules)'],
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administraion Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Moriz Wahl",
|
||||
"version": "1.9.0",
|
||||
"version": "1.10.0",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
@ -38,7 +38,9 @@
|
||||
"graphql": "^15.6.1",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "26.6.3",
|
||||
"jest-canvas-mock": "^2.3.1",
|
||||
"portal-vue": "^2.1.7",
|
||||
"qrcanvas-vue": "2.1.1",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"stats-webpack-plugin": "^0.7.0",
|
||||
"vue": "^2.6.11",
|
||||
|
||||
BIN
admin/public/img/gdd-coin.png
Normal file
BIN
admin/public/img/gdd-coin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
254
admin/src/components/ChangeUserRoleFormular.spec.js
Normal file
254
admin/src/components/ChangeUserRoleFormular.spec.js
Normal file
@ -0,0 +1,254 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ChangeUserRoleFormular from './ChangeUserRoleFormular.vue'
|
||||
import { setUserRole } from '../graphql/setUserRole'
|
||||
import { toastSuccessSpy, toastErrorSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
setUserRole: null,
|
||||
},
|
||||
})
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
mutate: apolloMutateMock,
|
||||
},
|
||||
$store: {
|
||||
state: {
|
||||
moderator: {
|
||||
id: 0,
|
||||
name: 'test moderator',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
let propsData
|
||||
let wrapper
|
||||
|
||||
describe('ChangeUserRoleFormular', () => {
|
||||
const Wrapper = () => {
|
||||
return mount(ChangeUserRoleFormular, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('DOM has', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
item: {
|
||||
userId: 1,
|
||||
isAdmin: null,
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.delete-user-formular', () => {
|
||||
expect(wrapper.find('.change-user-role-formular').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('change own role', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
item: {
|
||||
userId: 0,
|
||||
isAdmin: null,
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has the text that you cannot change own role', () => {
|
||||
expect(wrapper.text()).toContain('userRole.notChangeYourSelf')
|
||||
})
|
||||
|
||||
it('has role select disabled', () => {
|
||||
expect(wrapper.find('select[disabled="disabled"]').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('change others role', () => {
|
||||
let rolesToSelect
|
||||
|
||||
describe('general', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
item: {
|
||||
userId: 1,
|
||||
isAdmin: null,
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
rolesToSelect = wrapper.find('select.role-select').findAll('option')
|
||||
})
|
||||
|
||||
it('has no text that you cannot change own role', () => {
|
||||
expect(wrapper.text()).not.toContain('userRole.notChangeYourSelf')
|
||||
})
|
||||
|
||||
it('has the select label', () => {
|
||||
expect(wrapper.text()).toContain('userRole.selectLabel')
|
||||
})
|
||||
|
||||
it('has a select', () => {
|
||||
expect(wrapper.find('select.role-select').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has role select enabled', () => {
|
||||
expect(wrapper.find('select.role-select[disabled="disabled"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
describe('on API error', () => {
|
||||
beforeEach(() => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
rolesToSelect.at(1).setSelected()
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user is usual user', () => {
|
||||
beforeEach(() => {
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
setUserRole: new Date(),
|
||||
},
|
||||
})
|
||||
propsData = {
|
||||
item: {
|
||||
userId: 1,
|
||||
isAdmin: null,
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
rolesToSelect = wrapper.find('select.role-select').findAll('option')
|
||||
})
|
||||
|
||||
it('has selected option set to "usual user"', () => {
|
||||
expect(wrapper.find('select.role-select').element.value).toBe('user')
|
||||
})
|
||||
|
||||
describe('change select to', () => {
|
||||
describe('same role', () => {
|
||||
it('does not call the API', () => {
|
||||
rolesToSelect.at(0).setSelected()
|
||||
expect(apolloMutateMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('new role', () => {
|
||||
beforeEach(() => {
|
||||
rolesToSelect.at(1).setSelected()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: setUserRole,
|
||||
variables: {
|
||||
userId: 1,
|
||||
isAdmin: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits "updateIsAdmin"', () => {
|
||||
expect(wrapper.emitted('updateIsAdmin')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
isAdmin: expect.any(Date),
|
||||
},
|
||||
]),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('toasts success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user is admin', () => {
|
||||
beforeEach(() => {
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
setUserRole: null,
|
||||
},
|
||||
})
|
||||
propsData = {
|
||||
item: {
|
||||
userId: 1,
|
||||
isAdmin: new Date(),
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
rolesToSelect = wrapper.find('select.role-select').findAll('option')
|
||||
})
|
||||
|
||||
it('has selected option set to "admin"', () => {
|
||||
expect(wrapper.find('select.role-select').element.value).toBe('admin')
|
||||
})
|
||||
|
||||
describe('change select to', () => {
|
||||
describe('same role', () => {
|
||||
it('does not call the API', () => {
|
||||
rolesToSelect.at(1).setSelected()
|
||||
expect(apolloMutateMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('new role', () => {
|
||||
beforeEach(() => {
|
||||
rolesToSelect.at(0).setSelected()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: setUserRole,
|
||||
variables: {
|
||||
userId: 1,
|
||||
isAdmin: false,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits "updateIsAdmin"', () => {
|
||||
expect(wrapper.emitted('updateIsAdmin')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
isAdmin: null,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('toasts success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
89
admin/src/components/ChangeUserRoleFormular.vue
Normal file
89
admin/src/components/ChangeUserRoleFormular.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="change-user-role-formular">
|
||||
<div class="shadow p-3 mb-5 bg-white rounded">
|
||||
<div v-if="item.userId === $store.state.moderator.id" class="m-3 mb-4">
|
||||
{{ $t('userRole.notChangeYourSelf') }}
|
||||
</div>
|
||||
<div class="m-3">
|
||||
<label for="role" class="mr-3">{{ $t('userRole.selectLabel') }}</label>
|
||||
<b-form-select
|
||||
class="role-select"
|
||||
v-model="roleSelected"
|
||||
:options="roles"
|
||||
:disabled="item.userId === $store.state.moderator.id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { setUserRole } from '../graphql/setUserRole'
|
||||
|
||||
const rolesValues = {
|
||||
admin: 'admin',
|
||||
user: 'user',
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'ChangeUserRoleFormular',
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
roleSelected: this.item.isAdmin ? rolesValues.admin : rolesValues.user,
|
||||
roles: [
|
||||
{ value: rolesValues.user, text: this.$t('userRole.selectRoles.user') },
|
||||
{ value: rolesValues.admin, text: this.$t('userRole.selectRoles.admin') },
|
||||
],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
roleSelected(newRole, oldRole) {
|
||||
if (newRole !== oldRole) {
|
||||
this.setUserRole(newRole, oldRole)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setUserRole(newRole, oldRole) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: setUserRole,
|
||||
variables: {
|
||||
userId: this.item.userId,
|
||||
isAdmin: newRole === rolesValues.admin,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit('updateIsAdmin', {
|
||||
userId: this.item.userId,
|
||||
isAdmin: result.data.setUserRole,
|
||||
})
|
||||
this.toastSuccess(
|
||||
this.$t('userRole.successfullyChangedTo', {
|
||||
role:
|
||||
result.data.setUserRole !== null
|
||||
? this.$t('userRole.selectRoles.admin')
|
||||
: this.$t('userRole.selectRoles.user'),
|
||||
}),
|
||||
)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.roleSelected = oldRole
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.role-select {
|
||||
width: 300pt;
|
||||
}
|
||||
</style>
|
||||
49
admin/src/components/ContributionLink.spec.js
Normal file
49
admin/src/components/ContributionLink.spec.js
Normal file
@ -0,0 +1,49 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ContributionLink from './ContributionLink.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Meditation',
|
||||
memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l',
|
||||
amount: '200',
|
||||
validFrom: '2022-04-01',
|
||||
validTo: '2022-08-01',
|
||||
cycle: 'täglich',
|
||||
maxPerCycle: '3',
|
||||
maxAmountPerMonth: 0,
|
||||
link: 'https://localhost/redeem/CL-1a2345678',
|
||||
},
|
||||
],
|
||||
count: 1,
|
||||
}
|
||||
|
||||
describe('ContributionLink', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(ContributionLink, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the Div Element ".contribution-link"', () => {
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
66
admin/src/components/ContributionLink.vue
Normal file
66
admin/src/components/ContributionLink.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="contribution-link">
|
||||
<b-card
|
||||
border-variant="success"
|
||||
:header="$t('contributionLink.contributionLinks')"
|
||||
header-bg-variant="success"
|
||||
header-text-variant="white"
|
||||
header-class="text-center"
|
||||
class="mt-5"
|
||||
>
|
||||
<b-button v-b-toggle.newContribution class="my-3 d-flex justify-content-left">
|
||||
{{ $t('math.plus') }} {{ $t('contributionLink.newContributionLink') }}
|
||||
</b-button>
|
||||
|
||||
<b-collapse v-model="visible" id="newContribution" class="mt-2">
|
||||
<b-card>
|
||||
<p class="h2 ml-5">{{ $t('contributionLink.contributionLinks') }}</p>
|
||||
<contribution-link-form :contributionLinkData="contributionLinkData" />
|
||||
</b-card>
|
||||
</b-collapse>
|
||||
|
||||
<b-card-text>
|
||||
<contribution-link-list
|
||||
v-if="count > 0"
|
||||
:items="items"
|
||||
@editContributionLinkData="editContributionLinkData"
|
||||
/>
|
||||
<div v-else>{{ $t('contributionLink.noContributionLinks') }}</div>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ContributionLinkForm from './ContributionLinkForm.vue'
|
||||
import ContributionLinkList from './ContributionLinkList.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContributionLink',
|
||||
components: {
|
||||
ContributionLinkForm,
|
||||
ContributionLinkList,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
count: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
visible: false,
|
||||
contributionLinkData: {},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editContributionLinkData(data) {
|
||||
if (!this.visible) this.$root.$emit('bv::toggle::collapse', 'newContribution')
|
||||
this.contributionLinkData = data
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
102
admin/src/components/ContributionLinkForm.spec.js
Normal file
102
admin/src/components/ContributionLinkForm.spec.js
Normal file
@ -0,0 +1,102 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ContributionLinkForm from './ContributionLinkForm.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
global.alert = jest.fn()
|
||||
|
||||
const propsData = {
|
||||
contributionLinkData: {},
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
}
|
||||
|
||||
// const mockAPIcall = jest.fn()
|
||||
|
||||
describe('ContributionLinkForm', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(ContributionLinkForm, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the Div Element ".contribution-link-form"', () => {
|
||||
expect(wrapper.find('div.contribution-link-form').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('call onReset', () => {
|
||||
it('form has the set data', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setData({
|
||||
form: {
|
||||
name: 'name',
|
||||
memo: 'memo',
|
||||
amount: 100,
|
||||
validFrom: 'validFrom',
|
||||
validTo: 'validTo',
|
||||
cycle: 'ONCE',
|
||||
maxPerCycle: 1,
|
||||
maxAmountPerMonth: 100,
|
||||
},
|
||||
})
|
||||
wrapper.vm.onReset()
|
||||
})
|
||||
expect(wrapper.vm.form).toEqual({
|
||||
amount: null,
|
||||
cycle: 'ONCE',
|
||||
validTo: null,
|
||||
maxAmountPerMonth: '0',
|
||||
memo: null,
|
||||
name: null,
|
||||
maxPerCycle: 1,
|
||||
validFrom: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('call onSubmit', () => {
|
||||
it('response with the contribution link url', () => {
|
||||
wrapper.vm.onSubmit()
|
||||
})
|
||||
})
|
||||
|
||||
// describe('successfull submit', () => {
|
||||
// beforeEach(async () => {
|
||||
// mockAPIcall.mockResolvedValue({
|
||||
// data: {
|
||||
// createContributionLink: {
|
||||
// link: 'https://localhost/redeem/CL-1a2345678',
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// await wrapper.find('input.test-validFrom').setValue('2022-6-18')
|
||||
// await wrapper.find('input.test-validTo').setValue('2022-7-18')
|
||||
// await wrapper.find('input.test-name').setValue('test name')
|
||||
// await wrapper.find('input.test-memo').setValue('test memo')
|
||||
// await wrapper.find('input.test-amount').setValue('100')
|
||||
// await wrapper.find('form').trigger('submit')
|
||||
// })
|
||||
|
||||
// it('calls the API', () => {
|
||||
// expect(mockAPIcall).toHaveBeenCalledWith(
|
||||
// expect.objectContaining({
|
||||
// variables: {
|
||||
// link: 'https://localhost/redeem/CL-1a2345678',
|
||||
// },
|
||||
// }),
|
||||
// )
|
||||
// })
|
||||
|
||||
// it('displays the new username', () => {
|
||||
// expect(wrapper.find('div.display-username').text()).toEqual('@username')
|
||||
// })
|
||||
// })
|
||||
})
|
||||
})
|
||||
218
admin/src/components/ContributionLinkForm.vue
Normal file
218
admin/src/components/ContributionLinkForm.vue
Normal file
@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<div class="contribution-link-form">
|
||||
<div v-if="updateData" class="text-light bg-info p-3">
|
||||
{{ updateData }}
|
||||
</div>
|
||||
<b-form class="m-5" @submit.prevent="onSubmit" ref="contributionLinkForm">
|
||||
<!-- Date -->
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-form-group :label="$t('contributionLink.validFrom')">
|
||||
<b-form-datepicker
|
||||
v-model="form.validFrom"
|
||||
size="lg"
|
||||
:min="min"
|
||||
class="mb-4 test-validFrom"
|
||||
reset-value=""
|
||||
:label-no-date-selected="$t('contributionLink.noDateSelected')"
|
||||
required
|
||||
></b-form-datepicker>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-group :label="$t('contributionLink.validTo')">
|
||||
<b-form-datepicker
|
||||
v-model="form.validTo"
|
||||
size="lg"
|
||||
:min="form.validFrom ? form.validFrom : min"
|
||||
class="mb-4 test-validTo"
|
||||
reset-value=""
|
||||
:label-no-date-selected="$t('contributionLink.noDateSelected')"
|
||||
required
|
||||
></b-form-datepicker>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<!-- Name -->
|
||||
<b-form-group :label="$t('contributionLink.name')">
|
||||
<b-form-input
|
||||
v-model="form.name"
|
||||
size="lg"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
required
|
||||
maxlength="100"
|
||||
class="test-name"
|
||||
></b-form-input>
|
||||
</b-form-group>
|
||||
<!-- Desc -->
|
||||
<b-form-group :label="$t('contributionLink.memo')">
|
||||
<b-form-textarea
|
||||
v-model="form.memo"
|
||||
size="lg"
|
||||
:placeholder="$t('contributionLink.memo')"
|
||||
required
|
||||
maxlength="255"
|
||||
class="test-memo"
|
||||
></b-form-textarea>
|
||||
</b-form-group>
|
||||
<!-- Amount -->
|
||||
<b-form-group :label="$t('contributionLink.amount')">
|
||||
<b-form-input
|
||||
v-model="form.amount"
|
||||
size="lg"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
required
|
||||
class="test-amount"
|
||||
></b-form-input>
|
||||
</b-form-group>
|
||||
<b-collapse id="collapse-2">
|
||||
<b-jumbotron>
|
||||
<b-row class="mb-4">
|
||||
<b-col>
|
||||
<!-- Cycle -->
|
||||
<label for="cycle">{{ $t('contributionLink.cycle') }}</label>
|
||||
<b-form-select
|
||||
v-model="form.cycle"
|
||||
:options="cycle"
|
||||
:disabled="disabled"
|
||||
class="mb-3"
|
||||
size="lg"
|
||||
></b-form-select>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<!-- maxPerCycle -->
|
||||
<label for="maxPerCycle">{{ $t('contributionLink.maxPerCycle') }}</label>
|
||||
<b-form-select
|
||||
v-model="form.maxPerCycle"
|
||||
:options="maxPerCycle"
|
||||
:disabled="disabled"
|
||||
class="mb-3"
|
||||
size="lg"
|
||||
></b-form-select>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<!-- Max amount -->
|
||||
<b-form-group :label="$t('contributionLink.maximumAmount')">
|
||||
<b-form-input
|
||||
v-model="form.maxAmountPerMonth"
|
||||
size="lg"
|
||||
:disabled="disabled"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
></b-form-input>
|
||||
</b-form-group>
|
||||
</b-jumbotron>
|
||||
</b-collapse>
|
||||
<div class="mt-6">
|
||||
<b-button type="submit" variant="primary">{{ $t('contributionLink.create') }}</b-button>
|
||||
<b-button type="reset" variant="danger" @click.prevent="onReset">
|
||||
{{ $t('contributionLink.clear') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-form>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { createContributionLink } from '@/graphql/createContributionLink.js'
|
||||
export default {
|
||||
name: 'ContributionLinkForm',
|
||||
props: {
|
||||
contributionLinkData: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
name: null,
|
||||
memo: null,
|
||||
amount: null,
|
||||
validFrom: null,
|
||||
validTo: null,
|
||||
cycle: 'ONCE',
|
||||
maxPerCycle: 1,
|
||||
maxAmountPerMonth: '0',
|
||||
},
|
||||
min: new Date(),
|
||||
cycle: [
|
||||
{ value: 'ONCE', text: this.$t('contributionLink.options.cycle.once') },
|
||||
{ value: 'hourly', text: this.$t('contributionLink.options.cycle.hourly') },
|
||||
{ value: 'daily', text: this.$t('contributionLink.options.cycle.daily') },
|
||||
{ value: 'weekly', text: this.$t('contributionLink.options.cycle.weekly') },
|
||||
{ value: 'monthly', text: this.$t('contributionLink.options.cycle.monthly') },
|
||||
{ value: 'yearly', text: this.$t('contributionLink.options.cycle.yearly') },
|
||||
],
|
||||
maxPerCycle: [
|
||||
{ value: '1', text: '1 x' },
|
||||
{ value: '2', text: '2 x' },
|
||||
{ value: '3', text: '3 x' },
|
||||
{ value: '4', text: '4 x' },
|
||||
{ value: '5', text: '5 x' },
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
if (this.form.validFrom === null)
|
||||
return this.toastError(this.$t('contributionLink.noStartDate'))
|
||||
if (this.form.validTo === null) return this.toastError(this.$t('contributionLink.noEndDate'))
|
||||
// alert(JSON.stringify(this.form))
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
validFrom: this.form.validFrom,
|
||||
validTo: this.form.validTo,
|
||||
name: this.form.name,
|
||||
amount: this.form.amount,
|
||||
memo: this.form.memo,
|
||||
cycle: this.form.cycle,
|
||||
maxPerCycle: this.form.maxPerCycle,
|
||||
maxAmountPerMonth: this.form.maxAmountPerMonth,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.link = result.data.createContributionLink.link
|
||||
this.toastSuccess(this.link)
|
||||
this.onReset()
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
onReset() {
|
||||
this.$refs.contributionLinkForm.reset()
|
||||
this.form.validFrom = null
|
||||
this.form.validTo = null
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
updateData() {
|
||||
return this.contributionLinkData
|
||||
},
|
||||
disabled() {
|
||||
if (this.form.cycle === 'ONCE') return true
|
||||
return false
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
contributionLinkData() {
|
||||
this.form.name = this.contributionLinkData.name
|
||||
this.form.memo = this.contributionLinkData.memo
|
||||
this.form.amount = this.contributionLinkData.amount
|
||||
this.form.validFrom = this.contributionLinkData.validFrom
|
||||
this.form.validTo = this.contributionLinkData.validTo
|
||||
this.form.cycle = this.contributionLinkData.cycle
|
||||
this.form.maxPerCycle = this.contributionLinkData.maxPerCycle
|
||||
this.form.maxAmountPerMonth = this.contributionLinkData.maxAmountPerMonth
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
147
admin/src/components/ContributionLinkList.spec.js
Normal file
147
admin/src/components/ContributionLinkList.spec.js
Normal file
@ -0,0 +1,147 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ContributionLinkList from './ContributionLinkList.vue'
|
||||
import { toastSuccessSpy, toastErrorSpy } from '../../test/testSetup'
|
||||
// import { deleteContributionLink } from '../graphql/deleteContributionLink'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const mockAPIcall = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
mutate: mockAPIcall,
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Meditation',
|
||||
memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l',
|
||||
amount: '200',
|
||||
validFrom: '2022-04-01',
|
||||
validTo: '2022-08-01',
|
||||
cycle: 'täglich',
|
||||
maxPerCycle: '3',
|
||||
maxAmountPerMonth: 0,
|
||||
link: 'https://localhost/redeem/CL-1a2345678',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
describe('ContributionLinkList', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(ContributionLinkList, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the Div Element ".contribution-link-list"', () => {
|
||||
expect(wrapper.find('div.contribution-link-list').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders table with contribution link', () => {
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
|
||||
'Meditation',
|
||||
)
|
||||
})
|
||||
|
||||
describe('edit contribution link', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.vm.editContributionLink()
|
||||
})
|
||||
|
||||
it('emits editContributionLinkData', async () => {
|
||||
expect(wrapper.vm.$emit('editContributionLinkData')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete contribution link', () => {
|
||||
let spy
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
wrapper.vm.deleteContributionLink()
|
||||
})
|
||||
|
||||
describe('with success', () => {
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve('some value'))
|
||||
mockAPIcall.mockResolvedValue()
|
||||
await wrapper.find('.test-delete-link').trigger('click')
|
||||
})
|
||||
|
||||
it('opens the modal ', () => {
|
||||
expect(spy).toBeCalled()
|
||||
})
|
||||
|
||||
it.skip('calls the API', () => {
|
||||
// expect(mockAPIcall).toBeCalledWith(
|
||||
// expect.objectContaining({
|
||||
// mutation: deleteContributionLink,
|
||||
// variables: {
|
||||
// id: 1,
|
||||
// },
|
||||
// }),
|
||||
// )
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('TODO: request message deleted ')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with error', () => {
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve('some value'))
|
||||
mockAPIcall.mockRejectedValue({ message: 'Something went wrong :(' })
|
||||
await wrapper.find('.test-delete-link').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Something went wrong :(')
|
||||
})
|
||||
})
|
||||
|
||||
describe('cancel delete', () => {
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve(false))
|
||||
mockAPIcall.mockResolvedValue()
|
||||
await wrapper.find('.test-delete-link').trigger('click')
|
||||
})
|
||||
|
||||
it('does not call the API', () => {
|
||||
expect(mockAPIcall).not.toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('onClick showButton', () => {
|
||||
it('modelData contains contribution link', () => {
|
||||
wrapper.find('button.test-show').trigger('click')
|
||||
expect(wrapper.vm.modalData).toEqual({
|
||||
amount: '200',
|
||||
cycle: 'täglich',
|
||||
id: 1,
|
||||
link: 'https://localhost/redeem/CL-1a2345678',
|
||||
maxAmountPerMonth: 0,
|
||||
maxPerCycle: '3',
|
||||
memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l',
|
||||
name: 'Meditation',
|
||||
validFrom: '2022-04-01',
|
||||
validTo: '2022-08-01',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
106
admin/src/components/ContributionLinkList.vue
Normal file
106
admin/src/components/ContributionLinkList.vue
Normal file
@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="contribution-link-list">
|
||||
<b-table striped hover :items="items" :fields="fields">
|
||||
<template #cell(delete)>
|
||||
<b-button
|
||||
variant="danger"
|
||||
size="md"
|
||||
class="mr-2 test-delete-link"
|
||||
@click="deleteContributionLink"
|
||||
>
|
||||
<b-icon icon="trash" variant="light"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
<template #cell(edit)="data">
|
||||
<b-button variant="success" size="md" class="mr-2" @click="editContributionLink(data.item)">
|
||||
<b-icon icon="pencil" variant="light"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
<template #cell(show)="data">
|
||||
<b-button
|
||||
variant="info"
|
||||
size="md"
|
||||
class="mr-2 test-show"
|
||||
@click="showContributionLink(data.item)"
|
||||
>
|
||||
<b-icon icon="eye" variant="light"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table>
|
||||
|
||||
<b-modal ref="my-modal" ok-only hide-header-close>
|
||||
<b-card header-tag="header" footer-tag="footer">
|
||||
<template #header>
|
||||
<h6 class="mb-0">{{ modalData ? modalData.name : '' }}</h6>
|
||||
</template>
|
||||
<b-card-text>
|
||||
{{ modalData }}
|
||||
<figure-qr-code :link="modalData ? modalData.link : ''" />
|
||||
</b-card-text>
|
||||
<template #footer>
|
||||
<em>{{ modalData ? modalData.link : '' }}</em>
|
||||
</template>
|
||||
</b-card>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { deleteContributionLink } from '@/graphql/deleteContributionLink.js'
|
||||
import FigureQrCode from './FigureQrCode.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContributionLinkList',
|
||||
components: {
|
||||
FigureQrCode,
|
||||
},
|
||||
props: {
|
||||
items: { type: Array, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fields: [
|
||||
'name',
|
||||
'memo',
|
||||
'amount',
|
||||
{ key: 'cycle', label: this.$t('contributionLink.cycle') },
|
||||
{ key: 'maxPerCycle', label: this.$t('contributionLink.maxPerCycle') },
|
||||
{ key: 'validFrom', label: this.$t('contributionLink.validFrom') },
|
||||
{ key: 'validTo', label: this.$t('contributionLink.validTo') },
|
||||
'delete',
|
||||
'edit',
|
||||
'show',
|
||||
],
|
||||
modalData: null,
|
||||
modalDataLink: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteContributionLink() {
|
||||
this.$bvModal.msgBoxConfirm(this.$t('contributionLink.deleteNow')).then(async (value) => {
|
||||
if (value)
|
||||
await this.$apollo
|
||||
.mutate({
|
||||
mutation: deleteContributionLink,
|
||||
variables: {
|
||||
id: this.id,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.toastSuccess('TODO: request message deleted ')
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
})
|
||||
})
|
||||
},
|
||||
editContributionLink(row) {
|
||||
this.$emit('editContributionLinkData', row)
|
||||
},
|
||||
|
||||
showContributionLink(row) {
|
||||
this.modalData = row
|
||||
this.$refs['my-modal'].show()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,14 +1,14 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CreationFormular from './CreationFormular.vue'
|
||||
import { createPendingCreation } from '../graphql/createPendingCreation'
|
||||
import { createPendingCreations } from '../graphql/createPendingCreations'
|
||||
import { adminCreateContribution } from '../graphql/adminCreateContribution'
|
||||
import { adminCreateContributions } from '../graphql/adminCreateContributions'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
createPendingCreation: [0, 0, 0],
|
||||
adminCreateContribution: [0, 0, 0],
|
||||
},
|
||||
})
|
||||
const stateCommitMock = jest.fn()
|
||||
@ -110,7 +110,7 @@ describe('CreationFormular', () => {
|
||||
it('sends ... to apollo', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: createPendingCreation,
|
||||
mutation: adminCreateContribution,
|
||||
variables: {
|
||||
email: 'benjamin@bluemchen.de',
|
||||
creationDate: getCreationDate(2),
|
||||
@ -334,10 +334,10 @@ describe('CreationFormular', () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
createPendingCreations: {
|
||||
adminCreateContributions: {
|
||||
success: true,
|
||||
successfulCreation: ['bob@baumeister.de', 'bibi@bloxberg.de'],
|
||||
failedCreation: [],
|
||||
successfulContribution: ['bob@baumeister.de', 'bibi@bloxberg.de'],
|
||||
failedContribution: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -355,7 +355,7 @@ describe('CreationFormular', () => {
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: createPendingCreations,
|
||||
mutation: adminCreateContributions,
|
||||
variables: {
|
||||
pendingCreations: [
|
||||
{
|
||||
@ -390,10 +390,10 @@ describe('CreationFormular', () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
createPendingCreations: {
|
||||
adminCreateContributions: {
|
||||
success: true,
|
||||
successfulCreation: [],
|
||||
failedCreation: ['bob@baumeister.de', 'bibi@bloxberg.de'],
|
||||
successfulContribution: [],
|
||||
failedContribution: ['bob@baumeister.de', 'bibi@bloxberg.de'],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -85,8 +85,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { createPendingCreation } from '../graphql/createPendingCreation'
|
||||
import { createPendingCreations } from '../graphql/createPendingCreations'
|
||||
import { adminCreateContribution } from '../graphql/adminCreateContribution'
|
||||
import { adminCreateContributions } from '../graphql/adminCreateContributions'
|
||||
import { creationMonths } from '../mixins/creationMonths'
|
||||
export default {
|
||||
name: 'CreationFormular',
|
||||
@ -158,25 +158,25 @@ export default {
|
||||
})
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: createPendingCreations,
|
||||
mutation: adminCreateContributions,
|
||||
variables: {
|
||||
pendingCreations: submitObj,
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
})
|
||||
.then((result) => {
|
||||
const failedCreations = []
|
||||
const failedContributions = []
|
||||
this.$store.commit(
|
||||
'openCreationsPlus',
|
||||
result.data.createPendingCreations.successfulCreation.length,
|
||||
result.data.adminCreateContributions.successfulContribution.length,
|
||||
)
|
||||
if (result.data.createPendingCreations.failedCreation.length > 0) {
|
||||
result.data.createPendingCreations.failedCreation.forEach((email) => {
|
||||
failedCreations.push(email)
|
||||
if (result.data.adminCreateContributions.failedContribution.length > 0) {
|
||||
result.data.adminCreateContributions.failedContribution.forEach((email) => {
|
||||
failedContributions.push(email)
|
||||
})
|
||||
}
|
||||
this.$emit('remove-all-bookmark')
|
||||
this.$emit('toast-failed-creations', failedCreations)
|
||||
this.$emit('toast-failed-creations', failedContributions)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
@ -190,11 +190,11 @@ export default {
|
||||
}
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: createPendingCreation,
|
||||
mutation: adminCreateContribution,
|
||||
variables: submitObj,
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit('update-user-data', this.item, result.data.createPendingCreation)
|
||||
this.$emit('update-user-data', this.item, result.data.adminCreateContribution)
|
||||
this.$store.commit('openCreationsPlus', 1)
|
||||
this.toastSuccess(
|
||||
this.$t('creation_form.toasted', {
|
||||
|
||||
@ -47,200 +47,200 @@ describe('DeletedUserFormular', () => {
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.delete-user-formular', () => {
|
||||
expect(wrapper.find('.deleted-user-formular').exists()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete self', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 0,
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.deleted-user-formular').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('shows a text that you cannot delete yourself', () => {
|
||||
expect(wrapper.text()).toBe('removeNotSelf')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete other user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 1,
|
||||
deletedAt: null,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('has a checkbox', () => {
|
||||
expect(wrapper.find('input[type="checkbox"]').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows the text "delete_user"', () => {
|
||||
expect(wrapper.text()).toBe('delete_user')
|
||||
})
|
||||
|
||||
describe('click on checkbox', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked()
|
||||
})
|
||||
|
||||
it('has a confirmation button', () => {
|
||||
expect(wrapper.find('button').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has the button text "delete_user"', () => {
|
||||
expect(wrapper.find('button').text()).toBe('delete_user')
|
||||
})
|
||||
|
||||
describe('confirm delete with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: deleteUser,
|
||||
variables: {
|
||||
userId: 1,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('unchecks the checkbox', () => {
|
||||
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm delete with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('click on checkbox again', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked(false)
|
||||
})
|
||||
|
||||
it('has no confirmation button anymore', () => {
|
||||
expect(wrapper.find('button').exists()).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('recover user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('has a checkbox', () => {
|
||||
expect(wrapper.find('input[type="checkbox"]').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows the text "undelete_user"', () => {
|
||||
expect(wrapper.text()).toBe('undelete_user')
|
||||
})
|
||||
|
||||
describe('click on checkbox', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
unDeleteUser: null,
|
||||
describe('delete self', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 0,
|
||||
},
|
||||
})
|
||||
await wrapper.find('input[type="checkbox"]').setChecked()
|
||||
})
|
||||
|
||||
it('has a confirmation button', () => {
|
||||
expect(wrapper.find('button').exists()).toBeTruthy()
|
||||
it('shows a text that you cannot delete yourself', () => {
|
||||
expect(wrapper.text()).toBe('removeNotSelf')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete other user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 1,
|
||||
deletedAt: null,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('has the button text "undelete_user"', () => {
|
||||
expect(wrapper.find('button').text()).toBe('undelete_user')
|
||||
it('has a checkbox', () => {
|
||||
expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('confirm recover with success', () => {
|
||||
it('shows the text "delete_user"', () => {
|
||||
expect(wrapper.text()).toBe('delete_user')
|
||||
})
|
||||
|
||||
describe('click on checkbox', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
await wrapper.find('input[type="checkbox"]').setChecked()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: unDeleteUser,
|
||||
variables: {
|
||||
userId: 1,
|
||||
},
|
||||
}),
|
||||
)
|
||||
it('has a confirmation button', () => {
|
||||
expect(wrapper.find('button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
it('has the button text "delete_user"', () => {
|
||||
expect(wrapper.find('button').text()).toBe('delete_user')
|
||||
})
|
||||
|
||||
describe('confirm delete with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: deleteUser,
|
||||
variables: {
|
||||
userId: 1,
|
||||
deletedAt: null,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('unchecks the checkbox', () => {
|
||||
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
it('unchecks the checkbox', () => {
|
||||
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
|
||||
describe('confirm delete with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('click on checkbox again', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked(false)
|
||||
})
|
||||
|
||||
it('has no confirmation button anymore', () => {
|
||||
expect(wrapper.find('button').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('recover user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm recover with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
it('has a checkbox', () => {
|
||||
expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('click on checkbox again', () => {
|
||||
it('shows the text "undelete_user"', () => {
|
||||
expect(wrapper.text()).toBe('undelete_user')
|
||||
})
|
||||
|
||||
describe('click on checkbox', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked(false)
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
unDeleteUser: null,
|
||||
},
|
||||
})
|
||||
await wrapper.find('input[type="checkbox"]').setChecked()
|
||||
})
|
||||
|
||||
it('has no confirmation button anymore', () => {
|
||||
expect(wrapper.find('button').exists()).toBeFalsy()
|
||||
it('has a confirmation button', () => {
|
||||
expect(wrapper.find('button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has the button text "undelete_user"', () => {
|
||||
expect(wrapper.find('button').text()).toBe('undelete_user')
|
||||
})
|
||||
|
||||
describe('confirm recover with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: unDeleteUser,
|
||||
variables: {
|
||||
userId: 1,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
deletedAt: null,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('unchecks the checkbox', () => {
|
||||
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm recover with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('click on checkbox again', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked(false)
|
||||
})
|
||||
|
||||
it('has no confirmation button anymore', () => {
|
||||
expect(wrapper.find('button').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -28,6 +28,7 @@ export default {
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
||||
@ -6,7 +6,7 @@ const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
updatePendingCreation: {
|
||||
adminUpdateContribution: {
|
||||
creation: [0, 0, 0],
|
||||
amount: 500,
|
||||
date: new Date(),
|
||||
|
||||
@ -73,7 +73,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { updatePendingCreation } from '../graphql/updatePendingCreation'
|
||||
import { adminUpdateContribution } from '../graphql/adminUpdateContribution'
|
||||
import { creationMonths } from '../mixins/creationMonths'
|
||||
|
||||
export default {
|
||||
@ -103,7 +103,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
text: !this.creationUserData.memo ? '' : this.creationUserData.memo,
|
||||
value: !this.creationUserData.amount ? 0 : this.creationUserData.amount,
|
||||
value: !this.creationUserData.amount ? 0 : Number(this.creationUserData.amount),
|
||||
rangeMin: 0,
|
||||
rangeMax: 1000,
|
||||
selected: '',
|
||||
@ -113,7 +113,7 @@ export default {
|
||||
submitCreation() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: updatePendingCreation,
|
||||
mutation: adminUpdateContribution,
|
||||
variables: {
|
||||
id: this.item.id,
|
||||
email: this.item.email,
|
||||
@ -123,11 +123,11 @@ export default {
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit('update-user-data', this.item, result.data.updatePendingCreation.creation)
|
||||
this.$emit('update-user-data', this.item, result.data.adminUpdateContribution.creation)
|
||||
this.$emit('update-creation-data', {
|
||||
amount: Number(result.data.updatePendingCreation.amount),
|
||||
date: result.data.updatePendingCreation.date,
|
||||
memo: result.data.updatePendingCreation.memo,
|
||||
amount: Number(result.data.adminUpdateContribution.amount),
|
||||
date: result.data.adminUpdateContribution.date,
|
||||
memo: result.data.adminUpdateContribution.memo,
|
||||
row: this.row,
|
||||
})
|
||||
this.toastSuccess(
|
||||
@ -155,7 +155,7 @@ export default {
|
||||
const month = this.$d(new Date(this.creationUserData.date), 'month')
|
||||
const index = this.radioOptions.findIndex((obj) => obj.item.short === month)
|
||||
this.selected = this.radioOptions[index].item
|
||||
this.rangeMax = this.creation[index] + this.creationUserData.amount
|
||||
this.rangeMax = Number(this.creation[index]) + Number(this.creationUserData.amount)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
30
admin/src/components/FigureQrCode.spec.js
Normal file
30
admin/src/components/FigureQrCode.spec.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import FigureQrCode from './FigureQrCode.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const propsData = {
|
||||
link: '',
|
||||
}
|
||||
|
||||
describe('FigureQrCode', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(FigureQrCode, { localVue, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the Div Element ".figure-qr-code"', () => {
|
||||
expect(wrapper.find('div.figure-qr-code').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders the QRCanvas Element ".canvas"', () => {
|
||||
expect(wrapper.find('.canvas').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
56
admin/src/components/FigureQrCode.vue
Normal file
56
admin/src/components/FigureQrCode.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="figure-qr-code">
|
||||
<div class="qrbox">
|
||||
<q-r-canvas :options="options" class="canvas" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { QRCanvas } from 'qrcanvas-vue'
|
||||
|
||||
export default {
|
||||
name: 'FigureQrCode',
|
||||
components: {
|
||||
QRCanvas,
|
||||
},
|
||||
props: {
|
||||
link: { type: String, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
options: {
|
||||
cellSize: 8,
|
||||
correctLevel: 'H',
|
||||
data: this.link,
|
||||
logo: {
|
||||
image: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const image = new Image()
|
||||
image.src = 'img/gdd-coin.png'
|
||||
image.onload = () => {
|
||||
this.options = {
|
||||
...this.options,
|
||||
logo: {
|
||||
image,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.qrbox {
|
||||
padding: 20px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
.canvas {
|
||||
width: 90%;
|
||||
max-width: 300px;
|
||||
padding: 5px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
</style>
|
||||
@ -69,6 +69,7 @@ const propsData = {
|
||||
{ key: 'edit_creation', label: 'edit' },
|
||||
{ key: 'confirm', label: 'save' },
|
||||
],
|
||||
toggleDetails: false,
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
@ -101,7 +102,7 @@ describe('OpenCreationsTable', () => {
|
||||
})
|
||||
|
||||
it('has a DIV element with the class .open-creations-table', () => {
|
||||
expect(wrapper.find('div.open-creations-table').exists()).toBeTruthy()
|
||||
expect(wrapper.find('div.open-creations-table').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has a table with three rows', () => {
|
||||
@ -109,7 +110,7 @@ describe('OpenCreationsTable', () => {
|
||||
})
|
||||
|
||||
it('find first button.bi-pencil-square for open EditCreationFormular ', () => {
|
||||
expect(wrapper.findAll('tr').at(1).find('.bi-pencil-square').exists()).toBeTruthy()
|
||||
expect(wrapper.findAll('tr').at(1).find('.bi-pencil-square').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('show edit details', () => {
|
||||
@ -122,7 +123,15 @@ describe('OpenCreationsTable', () => {
|
||||
})
|
||||
|
||||
it.skip('renders the component component-edit-creation-formular', () => {
|
||||
expect(wrapper.find('div.component-edit-creation-formular').exists()).toBeTruthy()
|
||||
expect(wrapper.find('div.component-edit-creation-formular').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('call updateUserData', () => {
|
||||
it('user creations has updated data', async () => {
|
||||
wrapper.vm.updateUserData(propsData.items[0], [444, 555, 666])
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.vm.items[0].creation).toEqual([444, 555, 666])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -70,12 +70,23 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
creationUserData: {
|
||||
amount: null,
|
||||
date: null,
|
||||
memo: null,
|
||||
moderator: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateCreationData(data) {
|
||||
this.creationUserData.amount = data.amount
|
||||
this.creationUserData.date = data.date
|
||||
this.creationUserData.memo = data.memo
|
||||
this.creationUserData.moderator = data.moderator
|
||||
this.creationUserData = data
|
||||
// this.creationUserData.amount = data.amount
|
||||
// this.creationUserData.date = data.date
|
||||
// this.creationUserData.memo = data.memo
|
||||
// this.creationUserData.moderator = data.moderator
|
||||
data.row.toggleDetails()
|
||||
},
|
||||
updateUserData(rowItem, newCreation) {
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import SearchUserTable from './SearchUserTable.vue'
|
||||
|
||||
const date = new Date()
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({})
|
||||
@ -96,16 +94,29 @@ describe('SearchUserTable', () => {
|
||||
await wrapper.findAll('tbody > tr').at(1).trigger('click')
|
||||
})
|
||||
|
||||
describe('isAdmin', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('div.change-user-role-formular').vm.$emit('updateIsAdmin', {
|
||||
userId: 1,
|
||||
isAdmin: new Date(),
|
||||
})
|
||||
})
|
||||
|
||||
it('emits updateIsAdmin', () => {
|
||||
expect(wrapper.emitted('updateIsAdmin')).toEqual([[1, expect.any(Date)]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleted at', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('div.deleted-user-formular').vm.$emit('updateDeletedAt', {
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
deletedAt: new Date(),
|
||||
})
|
||||
})
|
||||
|
||||
it('emits updateDeletedAt', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual([[1, date]])
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual([[1, expect.any(Date)]])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
<template #cell(status)="row">
|
||||
<div class="text-right">
|
||||
<b-avatar v-if="row.item.deletedAt" class="mr-3" variant="light">
|
||||
<b-avatar v-if="row.item.deletedAt" class="mr-3 test-deleted-icon" variant="light">
|
||||
<b-iconstack font-scale="2">
|
||||
<b-icon stacked icon="person" variant="info" scale="0.75"></b-icon>
|
||||
<b-icon stacked icon="slash-circle" variant="danger"></b-icon>
|
||||
@ -79,6 +79,9 @@
|
||||
<b-tab :title="$t('transactionlink.name')" :disabled="row.item.deletedAt !== null">
|
||||
<transaction-link-list v-if="!row.item.deletedAt" :userId="row.item.userId" />
|
||||
</b-tab>
|
||||
<b-tab :title="$t('userRole.tabTitle')">
|
||||
<change-user-role-formular :item="row.item" @updateIsAdmin="updateIsAdmin" />
|
||||
</b-tab>
|
||||
<b-tab :title="$t('delete_user')">
|
||||
<deleted-user-formular :item="row.item" @updateDeletedAt="updateDeletedAt" />
|
||||
</b-tab>
|
||||
@ -93,6 +96,7 @@ import CreationFormular from '../CreationFormular.vue'
|
||||
import ConfirmRegisterMailFormular from '../ConfirmRegisterMailFormular.vue'
|
||||
import CreationTransactionList from '../CreationTransactionList.vue'
|
||||
import TransactionLinkList from '../TransactionLinkList.vue'
|
||||
import ChangeUserRoleFormular from '../ChangeUserRoleFormular.vue'
|
||||
import DeletedUserFormular from '../DeletedUserFormular.vue'
|
||||
|
||||
export default {
|
||||
@ -102,6 +106,7 @@ export default {
|
||||
ConfirmRegisterMailFormular,
|
||||
CreationTransactionList,
|
||||
TransactionLinkList,
|
||||
ChangeUserRoleFormular,
|
||||
DeletedUserFormular,
|
||||
},
|
||||
props: {
|
||||
@ -123,6 +128,9 @@ export default {
|
||||
updateUserData(rowItem, newCreation) {
|
||||
rowItem.creation = newCreation
|
||||
},
|
||||
updateIsAdmin({ userId, isAdmin }) {
|
||||
this.$emit('updateIsAdmin', userId, isAdmin)
|
||||
},
|
||||
updateDeletedAt({ userId, deletedAt }) {
|
||||
this.$emit('updateDeletedAt', userId, deletedAt)
|
||||
},
|
||||
|
||||
12
admin/src/graphql/adminCreateContribution.js
Normal file
12
admin/src/graphql/adminCreateContribution.js
Normal file
@ -0,0 +1,12 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const adminCreateContribution = gql`
|
||||
mutation ($email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||
adminCreateContribution(
|
||||
email: $email
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
creationDate: $creationDate
|
||||
)
|
||||
}
|
||||
`
|
||||
11
admin/src/graphql/adminCreateContributions.js
Normal file
11
admin/src/graphql/adminCreateContributions.js
Normal file
@ -0,0 +1,11 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const adminCreateContributions = gql`
|
||||
mutation ($pendingCreations: [AdminCreateContributionArgs!]!) {
|
||||
adminCreateContributions(pendingCreations: $pendingCreations) {
|
||||
success
|
||||
successfulContribution
|
||||
failedContribution
|
||||
}
|
||||
}
|
||||
`
|
||||
7
admin/src/graphql/adminDeleteContribution.js
Normal file
7
admin/src/graphql/adminDeleteContribution.js
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const adminDeleteContribution = gql`
|
||||
mutation ($id: Int!) {
|
||||
adminDeleteContribution(id: $id)
|
||||
}
|
||||
`
|
||||
@ -1,8 +1,8 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const updatePendingCreation = gql`
|
||||
export const adminUpdateContribution = gql`
|
||||
mutation ($id: Int!, $email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||
updatePendingCreation(
|
||||
adminUpdateContribution(
|
||||
id: $id
|
||||
email: $email
|
||||
amount: $amount
|
||||
7
admin/src/graphql/confirmContribution.js
Normal file
7
admin/src/graphql/confirmContribution.js
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const confirmContribution = gql`
|
||||
mutation ($id: Int!) {
|
||||
confirmContribution(id: $id)
|
||||
}
|
||||
`
|
||||
@ -1,7 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const confirmPendingCreation = gql`
|
||||
mutation ($id: Int!) {
|
||||
confirmPendingCreation(id: $id)
|
||||
}
|
||||
`
|
||||
27
admin/src/graphql/createContributionLink.js
Normal file
27
admin/src/graphql/createContributionLink.js
Normal file
@ -0,0 +1,27 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const createContributionLink = gql`
|
||||
mutation (
|
||||
$validFrom: String!
|
||||
$validTo: String!
|
||||
$name: String!
|
||||
$amount: Decimal!
|
||||
$memo: String!
|
||||
$cycle: String!
|
||||
$maxPerCycle: Int! = 1
|
||||
$maxAmountPerMonth: Decimal
|
||||
) {
|
||||
createContributionLink(
|
||||
validFrom: $validFrom
|
||||
validTo: $validTo
|
||||
name: $name
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
cycle: $cycle
|
||||
maxPerCycle: $maxPerCycle
|
||||
maxAmountPerMonth: $maxAmountPerMonth
|
||||
) {
|
||||
link
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,7 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const createPendingCreation = gql`
|
||||
mutation ($email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||
createPendingCreation(email: $email, amount: $amount, memo: $memo, creationDate: $creationDate)
|
||||
}
|
||||
`
|
||||
@ -1,11 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const createPendingCreations = gql`
|
||||
mutation ($pendingCreations: [CreatePendingCreationArgs!]!) {
|
||||
createPendingCreations(pendingCreations: $pendingCreations) {
|
||||
success
|
||||
successfulCreation
|
||||
failedCreation
|
||||
}
|
||||
}
|
||||
`
|
||||
7
admin/src/graphql/deleteContributionLink.js
Normal file
7
admin/src/graphql/deleteContributionLink.js
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const deleteContributionLink = gql`
|
||||
mutation ($id: Int!) {
|
||||
deleteContributionLink(id: $id)
|
||||
}
|
||||
`
|
||||
@ -1,7 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const deletePendingCreation = gql`
|
||||
mutation ($id: Int!) {
|
||||
deletePendingCreation(id: $id)
|
||||
}
|
||||
`
|
||||
23
admin/src/graphql/listContributionLinks.js
Normal file
23
admin/src/graphql/listContributionLinks.js
Normal file
@ -0,0 +1,23 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const listContributionLinks = gql`
|
||||
query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
|
||||
listContributionLinks(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
|
||||
links {
|
||||
id
|
||||
amount
|
||||
name
|
||||
memo
|
||||
code
|
||||
link
|
||||
createdAt
|
||||
validFrom
|
||||
validTo
|
||||
maxAmountPerMonth
|
||||
cycle
|
||||
maxPerCycle
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,8 +1,8 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const getPendingCreations = gql`
|
||||
export const listUnconfirmedContributions = gql`
|
||||
query {
|
||||
getPendingCreations {
|
||||
listUnconfirmedContributions {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
@ -19,6 +19,7 @@ export const searchUsers = gql`
|
||||
hasElopage
|
||||
emailConfirmationSend
|
||||
deletedAt
|
||||
isAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
admin/src/graphql/setUserRole.js
Normal file
7
admin/src/graphql/setUserRole.js
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const setUserRole = gql`
|
||||
mutation ($userId: Int!, $isAdmin: Boolean!) {
|
||||
setUserRole(userId: $userId, isAdmin: $isAdmin)
|
||||
}
|
||||
`
|
||||
18
admin/src/graphql/showContributionLink.js
Normal file
18
admin/src/graphql/showContributionLink.js
Normal file
@ -0,0 +1,18 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const showContributionLink = gql`
|
||||
query ($id: Int!) {
|
||||
showContributionLink {
|
||||
id
|
||||
validFrom
|
||||
validTo
|
||||
name
|
||||
memo
|
||||
amount
|
||||
cycle
|
||||
maxPerCycle
|
||||
maxAmountPerMonth
|
||||
code
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,6 +1,35 @@
|
||||
{
|
||||
"all_emails": "Alle Nutzer",
|
||||
"back": "zurück",
|
||||
"contributionLink": {
|
||||
"amount": "Betrag",
|
||||
"clear": "Löschen",
|
||||
"contributionLinks": "Beitragslinks",
|
||||
"create": "Anlegen",
|
||||
"cycle": "Zyklus",
|
||||
"deleteNow": "Automatische Creations wirklich löschen?",
|
||||
"maximumAmount": "maximaler Betrag",
|
||||
"maxPerCycle": "Wiederholungen",
|
||||
"memo": "Nachricht",
|
||||
"name": "Name",
|
||||
"newContributionLink": "Neuer Beitragslink",
|
||||
"noContributionLinks": "Es sind keine Beitragslinks angelegt.",
|
||||
"noDateSelected": "Kein Datum ausgewählt",
|
||||
"noEndDate": "Kein Enddatum gewählt.",
|
||||
"noStartDate": "Kein Startdatum gewählt.",
|
||||
"options": {
|
||||
"cycle": {
|
||||
"daily": "täglich",
|
||||
"hourly": "stündlich",
|
||||
"monthly": "monatlich",
|
||||
"once": "einmalig",
|
||||
"weekly": "wöchentlich",
|
||||
"yearly": "jährlich"
|
||||
}
|
||||
},
|
||||
"validFrom": "Startdatum",
|
||||
"validTo": "Enddatum"
|
||||
},
|
||||
"creation": "Schöpfung",
|
||||
"creationList": "Schöpfungsliste",
|
||||
"creation_form": {
|
||||
@ -44,7 +73,8 @@
|
||||
"lastname": "Nachname",
|
||||
"math": {
|
||||
"exclaim": "!",
|
||||
"pipe": "|"
|
||||
"pipe": "|",
|
||||
"plus": "+"
|
||||
},
|
||||
"moderator": "Moderator",
|
||||
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
|
||||
@ -71,7 +101,7 @@
|
||||
},
|
||||
"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",
|
||||
"status": "Status",
|
||||
@ -101,6 +131,16 @@
|
||||
"text_false": " Die letzte Email wurde am {date} Uhr an das Mitglied ({email}) gesendet.",
|
||||
"text_true": " Die Email wurde bestätigt."
|
||||
},
|
||||
"userRole": {
|
||||
"notChangeYourSelf": "Als Admin/Moderator kannst du nicht selber deine Rolle ändern.",
|
||||
"selectLabel": "Rolle:",
|
||||
"selectRoles": {
|
||||
"admin": "Administrator",
|
||||
"user": "einfacher Nutzer"
|
||||
},
|
||||
"successfullyChangedTo": "Nutzer ist jetzt „{role}“.",
|
||||
"tabTitle": "Nutzer-Rolle"
|
||||
},
|
||||
"user_deleted": "Nutzer ist gelöscht.",
|
||||
"user_recovered": "Nutzer ist wiederhergestellt.",
|
||||
"user_search": "Nutzer-Suche"
|
||||
|
||||
@ -1,6 +1,35 @@
|
||||
{
|
||||
"all_emails": "All users",
|
||||
"back": "back",
|
||||
"contributionLink": {
|
||||
"amount": "Amount",
|
||||
"clear": "Clear",
|
||||
"contributionLinks": "Contribution Links",
|
||||
"create": "Create",
|
||||
"cycle": "Cycle",
|
||||
"deleteNow": "Do you really delete automatic creations?",
|
||||
"maximumAmount": "Maximum amount",
|
||||
"maxPerCycle": "Repetition",
|
||||
"memo": "Memo",
|
||||
"name": "Name",
|
||||
"newContributionLink": "New contribution link",
|
||||
"noContributionLinks": "No contribution link has been created.",
|
||||
"noDateSelected": "No date selected",
|
||||
"noEndDate": "No end-date",
|
||||
"noStartDate": "No start-date",
|
||||
"options": {
|
||||
"cycle": {
|
||||
"daily": "daily",
|
||||
"hourly": "hourly",
|
||||
"monthly": "monthly",
|
||||
"once": "once",
|
||||
"weekly": "weekly",
|
||||
"yearly": "yearly"
|
||||
}
|
||||
},
|
||||
"validFrom": "Start-date",
|
||||
"validTo": "End-Date"
|
||||
},
|
||||
"creation": "Creation",
|
||||
"creationList": "Creation list",
|
||||
"creation_form": {
|
||||
@ -44,7 +73,8 @@
|
||||
"lastname": "Lastname",
|
||||
"math": {
|
||||
"exclaim": "!",
|
||||
"pipe": "|"
|
||||
"pipe": "|",
|
||||
"plus": "+"
|
||||
},
|
||||
"moderator": "Moderator",
|
||||
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
|
||||
@ -71,7 +101,7 @@
|
||||
},
|
||||
"redeemed": "redeemed",
|
||||
"remove": "Remove",
|
||||
"removeNotSelf": "As admin / moderator you cannot delete yourself.",
|
||||
"removeNotSelf": "As an admin/moderator, you cannot delete yourself.",
|
||||
"remove_all": "Remove all users",
|
||||
"save": "Speichern",
|
||||
"status": "Status",
|
||||
@ -101,6 +131,16 @@
|
||||
"text_false": "The last email was sent to the member ({email}) on {date}.",
|
||||
"text_true": "The email was confirmed."
|
||||
},
|
||||
"userRole": {
|
||||
"notChangeYourSelf": "As an admin/moderator, you cannot change your own role.",
|
||||
"selectLabel": "Role:",
|
||||
"selectRoles": {
|
||||
"admin": "administrator",
|
||||
"user": "usual user"
|
||||
},
|
||||
"successfullyChangedTo": "User is now \"{role}\".",
|
||||
"tabTitle": "User Role"
|
||||
},
|
||||
"user_deleted": "User is deleted.",
|
||||
"user_recovered": "User is recovered.",
|
||||
"user_search": "User search"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CreationConfirm from './CreationConfirm.vue'
|
||||
import { deletePendingCreation } from '../graphql/deletePendingCreation'
|
||||
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
|
||||
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
|
||||
import { confirmContribution } from '../graphql/confirmContribution'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
@ -9,7 +9,7 @@ const localVue = global.localVue
|
||||
const storeCommitMock = jest.fn()
|
||||
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
getPendingCreations: [
|
||||
listUnconfirmedContributions: [
|
||||
{
|
||||
id: 1,
|
||||
firstName: 'Bibi',
|
||||
@ -84,9 +84,9 @@ describe('CreationConfirm', () => {
|
||||
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('calls the deletePendingCreation mutation', () => {
|
||||
it('calls the adminDeleteContribution mutation', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith({
|
||||
mutation: deletePendingCreation,
|
||||
mutation: adminDeleteContribution,
|
||||
variables: { id: 1 },
|
||||
})
|
||||
})
|
||||
@ -141,9 +141,9 @@ describe('CreationConfirm', () => {
|
||||
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
|
||||
})
|
||||
|
||||
it('calls the confirmPendingCreation mutation', () => {
|
||||
it('calls the confirmContribution mutation', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith({
|
||||
mutation: confirmPendingCreation,
|
||||
mutation: confirmContribution,
|
||||
variables: { id: 2 },
|
||||
})
|
||||
})
|
||||
|
||||
@ -15,9 +15,9 @@
|
||||
<script>
|
||||
import Overlay from '../components/Overlay.vue'
|
||||
import OpenCreationsTable from '../components/Tables/OpenCreationsTable.vue'
|
||||
import { getPendingCreations } from '../graphql/getPendingCreations'
|
||||
import { deletePendingCreation } from '../graphql/deletePendingCreation'
|
||||
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
|
||||
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions'
|
||||
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
|
||||
import { confirmContribution } from '../graphql/confirmContribution'
|
||||
|
||||
export default {
|
||||
name: 'CreationConfirm',
|
||||
@ -36,7 +36,7 @@ export default {
|
||||
removeCreation(item) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: deletePendingCreation,
|
||||
mutation: adminDeleteContribution,
|
||||
variables: {
|
||||
id: item.id,
|
||||
},
|
||||
@ -52,7 +52,7 @@ export default {
|
||||
confirmCreation() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: confirmPendingCreation,
|
||||
mutation: confirmContribution,
|
||||
variables: {
|
||||
id: this.item.id,
|
||||
},
|
||||
@ -70,13 +70,13 @@ export default {
|
||||
getPendingCreations() {
|
||||
this.$apollo
|
||||
.query({
|
||||
query: getPendingCreations,
|
||||
query: listUnconfirmedContributions,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
.then((result) => {
|
||||
this.$store.commit('resetOpenCreations')
|
||||
this.pendingCreations = result.data.getPendingCreations
|
||||
this.$store.commit('setOpenCreations', result.data.getPendingCreations.length)
|
||||
this.pendingCreations = result.data.listUnconfirmedContributions
|
||||
this.$store.commit('setOpenCreations', result.data.listUnconfirmedContributions.length)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
|
||||
@ -5,7 +5,7 @@ const localVue = global.localVue
|
||||
|
||||
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
getPendingCreations: [
|
||||
listUnconfirmedContributions: [
|
||||
{
|
||||
pending: true,
|
||||
},
|
||||
@ -46,7 +46,7 @@ describe('Overview', () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('calls getPendingCreations', () => {
|
||||
it('calls listUnconfirmedContributions', () => {
|
||||
expect(apolloQueryMock).toBeCalled()
|
||||
})
|
||||
|
||||
|
||||
@ -28,27 +28,54 @@
|
||||
</b-link>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
<contribution-link :items="items" :count="count" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getPendingCreations } from '../graphql/getPendingCreations'
|
||||
import { listContributionLinks } from '@/graphql/listContributionLinks.js'
|
||||
import ContributionLink from '../components/ContributionLink.vue'
|
||||
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions'
|
||||
|
||||
export default {
|
||||
name: 'overview',
|
||||
components: {
|
||||
ContributionLink,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
count: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getPendingCreations() {
|
||||
this.$apollo
|
||||
.query({
|
||||
query: getPendingCreations,
|
||||
query: listUnconfirmedContributions,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
.then((result) => {
|
||||
this.$store.commit('setOpenCreations', result.data.getPendingCreations.length)
|
||||
this.$store.commit('setOpenCreations', result.data.listUnconfirmedContributions.length)
|
||||
})
|
||||
},
|
||||
async getContributionLinks() {
|
||||
this.$apollo
|
||||
.query({
|
||||
query: listContributionLinks,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
.then((result) => {
|
||||
this.count = result.data.listContributionLinks.count
|
||||
this.items = result.data.listContributionLinks.links
|
||||
})
|
||||
.catch(() => {
|
||||
this.toastError('listContributionLinks has no result, use default data')
|
||||
})
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getPendingCreations()
|
||||
this.getContributionLinks()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -199,14 +199,43 @@ describe('UserSearch', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('change user role', () => {
|
||||
const userId = 4
|
||||
|
||||
describe('to admin', () => {
|
||||
it('updates user role to admin', async () => {
|
||||
await wrapper
|
||||
.findComponent({ name: 'SearchUserTable' })
|
||||
.vm.$emit('updateIsAdmin', userId, new Date())
|
||||
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).isAdmin).toEqual(
|
||||
expect.any(Date),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('to usual user', () => {
|
||||
it('updates user role to usual user', async () => {
|
||||
await wrapper
|
||||
.findComponent({ name: 'SearchUserTable' })
|
||||
.vm.$emit('updateIsAdmin', userId, null)
|
||||
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).isAdmin).toEqual(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete user', () => {
|
||||
const now = new Date()
|
||||
beforeEach(async () => {
|
||||
wrapper.findComponent({ name: 'SearchUserTable' }).vm.$emit('updateDeletedAt', 4, now)
|
||||
const userId = 4
|
||||
beforeEach(() => {
|
||||
wrapper
|
||||
.findComponent({ name: 'SearchUserTable' })
|
||||
.vm.$emit('updateDeletedAt', userId, new Date())
|
||||
})
|
||||
|
||||
it('marks the user as deleted', () => {
|
||||
expect(wrapper.vm.searchResult.find((obj) => obj.userId === 4).deletedAt).toEqual(now)
|
||||
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).deletedAt).toEqual(
|
||||
expect.any(Date),
|
||||
)
|
||||
expect(wrapper.find('.test-deleted-icon').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
type="PageUserSearch"
|
||||
:items="searchResult"
|
||||
:fields="fields"
|
||||
@updateIsAdmin="updateIsAdmin"
|
||||
@updateDeletedAt="updateDeletedAt"
|
||||
/>
|
||||
<b-pagination
|
||||
@ -111,6 +112,9 @@ export default {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
updateIsAdmin(userId, isAdmin) {
|
||||
this.searchResult.find((obj) => obj.userId === userId).isAdmin = isAdmin
|
||||
},
|
||||
updateDeletedAt(userId, deletedAt) {
|
||||
this.searchResult.find((obj) => obj.userId === userId).deletedAt = deletedAt
|
||||
this.toastSuccess(deletedAt ? this.$t('user_deleted') : this.$t('user_recovered'))
|
||||
|
||||
@ -932,6 +932,13 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.11.2", "@babel/runtime@^7.16.0":
|
||||
version "7.18.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4"
|
||||
integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.14.0":
|
||||
version "7.17.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.7.tgz#a5f3328dc41ff39d803f311cfe17703418cf9825"
|
||||
@ -4082,9 +4089,9 @@ caniuse-api@^3.0.0:
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001271:
|
||||
version "1.0.30001271"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz#0dda0c9bcae2cf5407cd34cac304186616cc83e8"
|
||||
integrity sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA==
|
||||
version "1.0.30001354"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001354.tgz"
|
||||
integrity sha512-mImKeCkyGDAHNywYFA4bqnLAzTUvVkqPvhY4DV47X+Gl2c5Z8c3KNETnXp14GQt11LvxE8AwjzGxJ+rsikiOzg==
|
||||
|
||||
capture-exit@^2.0.0:
|
||||
version "2.0.0"
|
||||
@ -4397,7 +4404,7 @@ color-name@1.1.3:
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||
|
||||
color-name@^1.0.0, color-name@~1.1.4:
|
||||
color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
@ -4845,6 +4852,11 @@ cssesc@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
|
||||
|
||||
cssfontparser@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3"
|
||||
integrity sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==
|
||||
|
||||
cssnano-preset-default@^4.0.0, cssnano-preset-default@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz#920622b1fc1e95a34e8838203f1397a504f2d3ff"
|
||||
@ -7821,6 +7833,14 @@ javascript-stringify@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz#27c76539be14d8bd128219a2d731b09337904e79"
|
||||
integrity sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==
|
||||
|
||||
jest-canvas-mock@^2.3.1:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.4.0.tgz#947b71442d7719f8e055decaecdb334809465341"
|
||||
integrity sha512-mmMpZzpmLzn5vepIaHk5HoH3Ka4WykbSoLuG/EKoJd0x0ID/t+INo1l8ByfcUJuDM+RIsL4QDg/gDnBbrj2/IQ==
|
||||
dependencies:
|
||||
cssfontparser "^1.2.1"
|
||||
moo-color "^1.0.2"
|
||||
|
||||
jest-changed-files@^24.9.0:
|
||||
version "24.9.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039"
|
||||
@ -9478,6 +9498,13 @@ mkdirp@0.x, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
|
||||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
|
||||
moo-color@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.3.tgz#d56435f8359c8284d83ac58016df7427febece74"
|
||||
integrity sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==
|
||||
dependencies:
|
||||
color-name "^1.1.4"
|
||||
|
||||
move-concurrently@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
||||
@ -10959,6 +10986,27 @@ q@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
||||
|
||||
qrcanvas-vue@2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/qrcanvas-vue/-/qrcanvas-vue-2.1.1.tgz#27b449f99eaf46f324b300215469bfdf8ef77d88"
|
||||
integrity sha512-86NMjOJ5XJGrrqrD2t+zmZxZKNuW1Is7o88UOiM8qFxDBjuTyfq9VJE9/2rN5XxThsjBuY4bRrQqL9blVwnI9w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.0"
|
||||
qrcanvas "^3.1.2"
|
||||
|
||||
qrcanvas@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/qrcanvas/-/qrcanvas-3.1.2.tgz#81a25e91b2c27e9ace91da95591cbfb100d68702"
|
||||
integrity sha512-lNcAyCHN0Eno/mJ5eBc7lHV/5ejAJxII0UELthG3bNnlLR+u8hCc7CR+hXBawbYUf96kNIosXfG2cJzx92ZWKg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.2"
|
||||
qrcode-generator "^1.4.4"
|
||||
|
||||
qrcode-generator@^1.4.4:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.4.4.tgz#63f771224854759329a99048806a53ed278740e7"
|
||||
integrity sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==
|
||||
|
||||
qs@6.7.0:
|
||||
version "6.7.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
CONFIG_VERSION=v6.2022-04-21
|
||||
CONFIG_VERSION=v8.2022-06-20
|
||||
|
||||
# Server
|
||||
PORT=4000
|
||||
@ -28,6 +28,7 @@ COMMUNITY_NAME=Gradido Entwicklung
|
||||
COMMUNITY_URL=http://localhost/
|
||||
COMMUNITY_REGISTER_URL=http://localhost/register
|
||||
COMMUNITY_REDEEM_URL=http://localhost/redeem/{code}
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code}
|
||||
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
|
||||
|
||||
# Login Server
|
||||
|
||||
@ -27,6 +27,7 @@ COMMUNITY_NAME=$COMMUNITY_NAME
|
||||
COMMUNITY_URL=$COMMUNITY_URL
|
||||
COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL
|
||||
COMMUNITY_REDEEM_URL=$COMMUNITY_REDEEM_URL
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL=$COMMUNITY_REDEEM_CONTRIBUTION_URL
|
||||
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
|
||||
|
||||
# Login Server
|
||||
@ -42,6 +43,7 @@ EMAIL_SMTP_URL=$EMAIL_SMTP_URL
|
||||
EMAIL_SMTP_PORT=587
|
||||
EMAIL_LINK_VERIFICATION=$EMAIL_LINK_VERIFICATION
|
||||
EMAIL_LINK_SETPASSWORD=$EMAIL_LINK_SETPASSWORD
|
||||
EMAIL_LINK_FORGOTPASSWORD=$EMAIL_LINK_FORGOTPASSWORD
|
||||
EMAIL_LINK_OVERVIEW=$EMAIL_LINK_OVERVIEW
|
||||
EMAIL_CODE_VALID_TIME=$EMAIL_CODE_VALID_TIME
|
||||
EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-backend",
|
||||
"version": "1.9.0",
|
||||
"version": "1.10.0",
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
|
||||
@ -27,14 +27,20 @@ export enum RIGHTS {
|
||||
GDT_BALANCE = 'GDT_BALANCE',
|
||||
// Admin
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION',
|
||||
UPDATE_PENDING_CREATION = 'UPDATE_PENDING_CREATION',
|
||||
SEARCH_PENDING_CREATION = 'SEARCH_PENDING_CREATION',
|
||||
DELETE_PENDING_CREATION = 'DELETE_PENDING_CREATION',
|
||||
CONFIRM_PENDING_CREATION = 'CONFIRM_PENDING_CREATION',
|
||||
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
|
||||
SET_USER_ROLE = 'SET_USER_ROLE',
|
||||
DELETE_USER = 'DELETE_USER',
|
||||
UNDELETE_USER = 'UNDELETE_USER',
|
||||
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
|
||||
ADMIN_CREATE_CONTRIBUTIONS = 'ADMIN_CREATE_CONTRIBUTIONS',
|
||||
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
|
||||
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
|
||||
LIST_UNCONFIRMED_CONTRIBUTIONS = 'LIST_UNCONFIRMED_CONTRIBUTIONS',
|
||||
CONFIRM_CONTRIBUTION = 'CONFIRM_CONTRIBUTION',
|
||||
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
|
||||
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
|
||||
LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
|
||||
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
|
||||
LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS',
|
||||
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
|
||||
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
|
||||
}
|
||||
|
||||
@ -10,14 +10,14 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0037-drop_server_user_table',
|
||||
DB_VERSION: '0040-add_contribution_link_id_to_user',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v6.2022-04-21',
|
||||
EXPECTED: 'v8.2022-06-20',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -54,6 +54,8 @@ const community = {
|
||||
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/',
|
||||
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
|
||||
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}',
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL:
|
||||
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}',
|
||||
COMMUNITY_DESCRIPTION:
|
||||
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import Decimal from 'decimal.js-light'
|
||||
|
||||
@InputType()
|
||||
@ArgsType()
|
||||
export default class CreatePendingCreationArgs {
|
||||
export default class AdminCreateContributionArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@ -2,7 +2,7 @@ import { ArgsType, Field, Int } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ArgsType()
|
||||
export default class UpdatePendingCreationArgs {
|
||||
export default class AdminUpdateContributionArgs {
|
||||
@Field(() => Int)
|
||||
id: number
|
||||
|
||||
29
backend/src/graphql/arg/ContributionLinkArgs.ts
Normal file
29
backend/src/graphql/arg/ContributionLinkArgs.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ArgsType()
|
||||
export default class ContributionLinkArgs {
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
name: string
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@Field(() => String)
|
||||
cycle: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
validFrom?: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
validTo?: string | null
|
||||
|
||||
@Field(() => Decimal, { nullable: true })
|
||||
maxAmountPerMonth: Decimal | null
|
||||
|
||||
@Field(() => Int)
|
||||
maxPerCycle: number
|
||||
}
|
||||
28
backend/src/graphql/enum/ContributionCycleType.ts
Normal file
28
backend/src/graphql/enum/ContributionCycleType.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum ContributionCycleType {
|
||||
ONCE = 'once',
|
||||
HOUR = 'hour',
|
||||
TWO_HOURS = 'two_hours',
|
||||
FOUR_HOURS = 'four_hours',
|
||||
EIGHT_HOURS = 'eight_hours',
|
||||
HALF_DAY = 'half_day',
|
||||
DAY = 'day',
|
||||
TWO_DAYS = 'two_days',
|
||||
THREE_DAYS = 'three_days',
|
||||
FOUR_DAYS = 'four_days',
|
||||
FIVE_DAYS = 'five_days',
|
||||
SIX_DAYS = 'six_days',
|
||||
WEEK = 'week',
|
||||
TWO_WEEKS = 'two_weeks',
|
||||
MONTH = 'month',
|
||||
TWO_MONTH = 'two_month',
|
||||
QUARTER = 'quarter',
|
||||
HALF_YEAR = 'half_year',
|
||||
YEAR = 'year',
|
||||
}
|
||||
|
||||
registerEnumType(ContributionCycleType, {
|
||||
name: 'ContributionCycleType', // this one is mandatory
|
||||
description: 'Name of the Type of the ContributionCycle', // this one is optional
|
||||
})
|
||||
@ -1,19 +1,19 @@
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class CreatePendingCreations {
|
||||
export class AdminCreateContributions {
|
||||
constructor() {
|
||||
this.success = false
|
||||
this.successfulCreation = []
|
||||
this.failedCreation = []
|
||||
this.successfulContribution = []
|
||||
this.failedContribution = []
|
||||
}
|
||||
|
||||
@Field(() => Boolean)
|
||||
success: boolean
|
||||
|
||||
@Field(() => [String])
|
||||
successfulCreation: string[]
|
||||
successfulContribution: string[]
|
||||
|
||||
@Field(() => [String])
|
||||
failedCreation: string[]
|
||||
failedContribution: string[]
|
||||
}
|
||||
@ -2,7 +2,7 @@ import { ObjectType, Field } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ObjectType()
|
||||
export class UpdatePendingCreation {
|
||||
export class AdminUpdateContribution {
|
||||
@Field(() => Date)
|
||||
date: Date
|
||||
|
||||
62
backend/src/graphql/model/ContributionLink.ts
Normal file
62
backend/src/graphql/model/ContributionLink.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
@ObjectType()
|
||||
export class ContributionLink {
|
||||
constructor(contributionLink: dbContributionLink) {
|
||||
this.id = contributionLink.id
|
||||
this.amount = contributionLink.amount
|
||||
this.name = contributionLink.name
|
||||
this.memo = contributionLink.memo
|
||||
this.createdAt = contributionLink.createdAt
|
||||
this.deletedAt = contributionLink.deletedAt
|
||||
this.validFrom = contributionLink.validFrom
|
||||
this.validTo = contributionLink.validTo
|
||||
this.maxAmountPerMonth = contributionLink.maxAmountPerMonth
|
||||
this.cycle = contributionLink.cycle
|
||||
this.maxPerCycle = contributionLink.maxPerCycle
|
||||
this.code = contributionLink.code
|
||||
this.link = CONFIG.COMMUNITY_REDEEM_CONTRIBUTION_URL.replace(/{code}/g, this.code)
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
id: number
|
||||
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
name: string
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@Field(() => String)
|
||||
code: string
|
||||
|
||||
@Field(() => String)
|
||||
link: string
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt: Date | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
validFrom: Date | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
validTo: Date | null
|
||||
|
||||
@Field(() => Decimal, { nullable: true })
|
||||
maxAmountPerMonth: Decimal | null
|
||||
|
||||
@Field(() => String)
|
||||
cycle: string
|
||||
|
||||
@Field(() => Int)
|
||||
maxPerCycle: number
|
||||
}
|
||||
11
backend/src/graphql/model/ContributionLinkList.ts
Normal file
11
backend/src/graphql/model/ContributionLinkList.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
|
||||
@ObjectType()
|
||||
export class ContributionLinkList {
|
||||
@Field(() => [ContributionLink])
|
||||
links: ContributionLink[]
|
||||
|
||||
@Field(() => Number)
|
||||
count: number
|
||||
}
|
||||
@ -2,7 +2,7 @@ import { ObjectType, Field, Int } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ObjectType()
|
||||
export class PendingCreation {
|
||||
export class UnconfirmedContribution {
|
||||
@Field(() => String)
|
||||
firstName: string
|
||||
|
||||
@ -14,6 +14,7 @@ export class UserAdmin {
|
||||
this.hasElopage = hasElopage
|
||||
this.deletedAt = user.deletedAt
|
||||
this.emailConfirmationSend = emailConfirmationSend
|
||||
this.isAdmin = user.isAdmin
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
@ -42,6 +43,9 @@ export class UserAdmin {
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
emailConfirmationSend?: string
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
isAdmin: Date | null
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,5 @@
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx, Int } from 'type-graphql'
|
||||
import {
|
||||
getCustomRepository,
|
||||
@ -11,21 +12,25 @@ import {
|
||||
FindOperator,
|
||||
} from '@dbTools/typeorm'
|
||||
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
|
||||
import { PendingCreation } from '@model/PendingCreation'
|
||||
import { CreatePendingCreations } from '@model/CreatePendingCreations'
|
||||
import { UpdatePendingCreation } from '@model/UpdatePendingCreation'
|
||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||
import { AdminCreateContributions } from '@model/AdminCreateContributions'
|
||||
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
import { ContributionLinkList } from '@model/ContributionLinkList'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { UserRepository } from '@repository/User'
|
||||
import CreatePendingCreationArgs from '@arg/CreatePendingCreationArgs'
|
||||
import UpdatePendingCreationArgs from '@arg/UpdatePendingCreationArgs'
|
||||
import AdminCreateContributionArgs from '@arg/AdminCreateContributionArgs'
|
||||
import AdminUpdateContributionArgs from '@arg/AdminUpdateContributionArgs'
|
||||
import SearchUsersArgs from '@arg/SearchUsersArgs'
|
||||
import ContributionLinkArgs from '@arg/ContributionLinkArgs'
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { Transaction } from '@model/Transaction'
|
||||
import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { TransactionRepository } from '@repository/Transaction'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
@ -39,12 +44,17 @@ import { Order } from '@enum/Order'
|
||||
import { communityUser } from '@/util/communityUser'
|
||||
import { checkOptInCode, activationLink, printTimeDuration } from './UserResolver'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
// const EMAIL_OPT_IN_REGISTER = 1
|
||||
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
||||
const MAX_CREATION_AMOUNT = new Decimal(1000)
|
||||
const FULL_CREATION_AVAILABLE = [MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT]
|
||||
const CONTRIBUTIONLINK_NAME_MAX_CHARS = 100
|
||||
const CONTRIBUTIONLINK_NAME_MIN_CHARS = 5
|
||||
const CONTRIBUTIONLINK_MEMO_MAX_CHARS = 255
|
||||
const CONTRIBUTIONLINK_MEMO_MIN_CHARS = 5
|
||||
|
||||
@Resolver()
|
||||
export class AdminResolver {
|
||||
@ -67,7 +77,15 @@ export class AdminResolver {
|
||||
}
|
||||
}
|
||||
|
||||
const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked', 'deletedAt']
|
||||
const userFields = [
|
||||
'id',
|
||||
'firstName',
|
||||
'lastName',
|
||||
'email',
|
||||
'emailChecked',
|
||||
'deletedAt',
|
||||
'isAdmin',
|
||||
]
|
||||
const [users, count] = await userRepository.findBySearchCriteriaPagedFiltered(
|
||||
userFields.map((fieldName) => {
|
||||
return 'user.' + fieldName
|
||||
@ -127,6 +145,48 @@ export class AdminResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.SET_USER_ROLE])
|
||||
@Mutation(() => Date, { nullable: true })
|
||||
async setUserRole(
|
||||
@Arg('userId', () => Int)
|
||||
userId: number,
|
||||
@Arg('isAdmin', () => Boolean)
|
||||
isAdmin: boolean,
|
||||
@Ctx()
|
||||
context: Context,
|
||||
): Promise<Date | null> {
|
||||
const user = await dbUser.findOne({ id: userId })
|
||||
// user exists ?
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with userId: ${userId}`)
|
||||
}
|
||||
// administrator user changes own role?
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === userId) {
|
||||
throw new Error('Administrator can not change his own role!')
|
||||
}
|
||||
// change isAdmin
|
||||
switch (user.isAdmin) {
|
||||
case null:
|
||||
if (isAdmin === true) {
|
||||
user.isAdmin = new Date()
|
||||
} else {
|
||||
throw new Error('User is already a usual user!')
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (isAdmin === false) {
|
||||
user.isAdmin = null
|
||||
} else {
|
||||
throw new Error('User is already admin!')
|
||||
}
|
||||
break
|
||||
}
|
||||
await user.save()
|
||||
const newUser = await dbUser.findOne({ id: userId })
|
||||
return newUser ? newUser.isAdmin : null
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.DELETE_USER])
|
||||
@Mutation(() => Date, { nullable: true })
|
||||
async deleteUser(
|
||||
@ -163,10 +223,10 @@ export class AdminResolver {
|
||||
return null
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CREATE_PENDING_CREATION])
|
||||
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTION])
|
||||
@Mutation(() => [Number])
|
||||
async createPendingCreation(
|
||||
@Args() { email, amount, memo, creationDate }: CreatePendingCreationArgs,
|
||||
async adminCreateContribution(
|
||||
@Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<Decimal[]> {
|
||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
||||
@ -174,61 +234,64 @@ export class AdminResolver {
|
||||
throw new Error(`Could not find user with email: ${email}`)
|
||||
}
|
||||
if (user.deletedAt) {
|
||||
throw new Error('This user was deleted. Cannot make a creation.')
|
||||
throw new Error('This user was deleted. Cannot create a contribution.')
|
||||
}
|
||||
if (!user.emailChecked) {
|
||||
throw new Error('Creation could not be saved, Email is not activated')
|
||||
throw new Error('Contribution could not be saved, Email is not activated')
|
||||
}
|
||||
const moderator = getUser(context)
|
||||
logger.trace('moderator: ', moderator.id)
|
||||
const creations = await getUserCreation(user.id)
|
||||
logger.trace('creations', creations)
|
||||
const creationDateObj = new Date(creationDate)
|
||||
if (isCreationValid(creations, amount, creationDateObj)) {
|
||||
const adminPendingCreation = AdminPendingCreation.create()
|
||||
adminPendingCreation.userId = user.id
|
||||
adminPendingCreation.amount = amount
|
||||
adminPendingCreation.created = new Date()
|
||||
adminPendingCreation.date = creationDateObj
|
||||
adminPendingCreation.memo = memo
|
||||
adminPendingCreation.moderator = moderator.id
|
||||
if (isContributionValid(creations, amount, creationDateObj)) {
|
||||
const contribution = Contribution.create()
|
||||
contribution.userId = user.id
|
||||
contribution.amount = amount
|
||||
contribution.createdAt = new Date()
|
||||
contribution.contributionDate = creationDateObj
|
||||
contribution.memo = memo
|
||||
contribution.moderatorId = moderator.id
|
||||
|
||||
await AdminPendingCreation.save(adminPendingCreation)
|
||||
logger.trace('contribution to save', contribution)
|
||||
await Contribution.save(contribution)
|
||||
}
|
||||
return getUserCreation(user.id)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CREATE_PENDING_CREATION])
|
||||
@Mutation(() => CreatePendingCreations)
|
||||
async createPendingCreations(
|
||||
@Arg('pendingCreations', () => [CreatePendingCreationArgs])
|
||||
pendingCreations: CreatePendingCreationArgs[],
|
||||
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTIONS])
|
||||
@Mutation(() => AdminCreateContributions)
|
||||
async adminCreateContributions(
|
||||
@Arg('pendingCreations', () => [AdminCreateContributionArgs])
|
||||
contributions: AdminCreateContributionArgs[],
|
||||
@Ctx() context: Context,
|
||||
): Promise<CreatePendingCreations> {
|
||||
): Promise<AdminCreateContributions> {
|
||||
let success = false
|
||||
const successfulCreation: string[] = []
|
||||
const failedCreation: string[] = []
|
||||
for (const pendingCreation of pendingCreations) {
|
||||
await this.createPendingCreation(pendingCreation, context)
|
||||
const successfulContribution: string[] = []
|
||||
const failedContribution: string[] = []
|
||||
for (const contribution of contributions) {
|
||||
await this.adminCreateContribution(contribution, context)
|
||||
.then(() => {
|
||||
successfulCreation.push(pendingCreation.email)
|
||||
successfulContribution.push(contribution.email)
|
||||
success = true
|
||||
})
|
||||
.catch(() => {
|
||||
failedCreation.push(pendingCreation.email)
|
||||
failedContribution.push(contribution.email)
|
||||
})
|
||||
}
|
||||
return {
|
||||
success,
|
||||
successfulCreation,
|
||||
failedCreation,
|
||||
successfulContribution,
|
||||
failedContribution,
|
||||
}
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.UPDATE_PENDING_CREATION])
|
||||
@Mutation(() => UpdatePendingCreation)
|
||||
async updatePendingCreation(
|
||||
@Args() { id, email, amount, memo, creationDate }: UpdatePendingCreationArgs,
|
||||
@Authorized([RIGHTS.ADMIN_UPDATE_CONTRIBUTION])
|
||||
@Mutation(() => AdminUpdateContribution)
|
||||
async adminUpdateContribution(
|
||||
@Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<UpdatePendingCreation> {
|
||||
): Promise<AdminUpdateContribution> {
|
||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with email: ${email}`)
|
||||
@ -239,59 +302,65 @@ export class AdminResolver {
|
||||
|
||||
const moderator = getUser(context)
|
||||
|
||||
const pendingCreationToUpdate = await AdminPendingCreation.findOne({ id })
|
||||
const contributionToUpdate = await Contribution.findOne({
|
||||
where: { id, confirmedAt: IsNull() },
|
||||
})
|
||||
|
||||
if (!pendingCreationToUpdate) {
|
||||
throw new Error('No creation found to given id.')
|
||||
if (!contributionToUpdate) {
|
||||
throw new Error('No contribution found to given id.')
|
||||
}
|
||||
|
||||
if (pendingCreationToUpdate.userId !== user.id) {
|
||||
throw new Error('user of the pending creation and send user does not correspond')
|
||||
if (contributionToUpdate.userId !== user.id) {
|
||||
throw new Error('user of the pending contribution and send user does not correspond')
|
||||
}
|
||||
|
||||
const creationDateObj = new Date(creationDate)
|
||||
let creations = await getUserCreation(user.id)
|
||||
if (pendingCreationToUpdate.date.getMonth() === creationDateObj.getMonth()) {
|
||||
creations = updateCreations(creations, pendingCreationToUpdate)
|
||||
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
|
||||
creations = updateCreations(creations, contributionToUpdate)
|
||||
}
|
||||
|
||||
// all possible cases not to be true are thrown in this function
|
||||
isCreationValid(creations, amount, creationDateObj)
|
||||
pendingCreationToUpdate.amount = amount
|
||||
pendingCreationToUpdate.memo = memo
|
||||
pendingCreationToUpdate.date = new Date(creationDate)
|
||||
pendingCreationToUpdate.moderator = moderator.id
|
||||
isContributionValid(creations, amount, creationDateObj)
|
||||
contributionToUpdate.amount = amount
|
||||
contributionToUpdate.memo = memo
|
||||
contributionToUpdate.contributionDate = new Date(creationDate)
|
||||
contributionToUpdate.moderatorId = moderator.id
|
||||
|
||||
await AdminPendingCreation.save(pendingCreationToUpdate)
|
||||
const result = new UpdatePendingCreation()
|
||||
await Contribution.save(contributionToUpdate)
|
||||
const result = new AdminUpdateContribution()
|
||||
result.amount = amount
|
||||
result.memo = pendingCreationToUpdate.memo
|
||||
result.date = pendingCreationToUpdate.date
|
||||
result.memo = contributionToUpdate.memo
|
||||
result.date = contributionToUpdate.contributionDate
|
||||
|
||||
result.creation = await getUserCreation(user.id)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.SEARCH_PENDING_CREATION])
|
||||
@Query(() => [PendingCreation])
|
||||
async getPendingCreations(): Promise<PendingCreation[]> {
|
||||
const pendingCreations = await AdminPendingCreation.find()
|
||||
if (pendingCreations.length === 0) {
|
||||
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
|
||||
@Query(() => [UnconfirmedContribution])
|
||||
async listUnconfirmedContributions(): Promise<UnconfirmedContribution[]> {
|
||||
const contributions = await Contribution.find({ where: { confirmedAt: IsNull() } })
|
||||
if (contributions.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const userIds = pendingCreations.map((p) => p.userId)
|
||||
const userIds = contributions.map((p) => p.userId)
|
||||
const userCreations = await getUserCreations(userIds)
|
||||
const users = await dbUser.find({ where: { id: In(userIds) }, withDeleted: true })
|
||||
|
||||
return pendingCreations.map((pendingCreation) => {
|
||||
const user = users.find((u) => u.id === pendingCreation.userId)
|
||||
const creation = userCreations.find((c) => c.id === pendingCreation.userId)
|
||||
return contributions.map((contribution) => {
|
||||
const user = users.find((u) => u.id === contribution.userId)
|
||||
const creation = userCreations.find((c) => c.id === contribution.userId)
|
||||
|
||||
return {
|
||||
...pendingCreation,
|
||||
amount: pendingCreation.amount,
|
||||
id: contribution.id,
|
||||
userId: contribution.userId,
|
||||
date: contribution.contributionDate,
|
||||
memo: contribution.memo,
|
||||
amount: contribution.amount,
|
||||
moderator: contribution.moderatorId,
|
||||
firstName: user ? user.firstName : '',
|
||||
lastName: user ? user.lastName : '',
|
||||
email: user ? user.email : '',
|
||||
@ -300,69 +369,93 @@ export class AdminResolver {
|
||||
})
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.DELETE_PENDING_CREATION])
|
||||
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])
|
||||
@Mutation(() => Boolean)
|
||||
async deletePendingCreation(@Arg('id', () => Int) id: number): Promise<boolean> {
|
||||
const pendingCreation = await AdminPendingCreation.findOne(id)
|
||||
if (!pendingCreation) {
|
||||
throw new Error('Creation not found for given id.')
|
||||
async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise<boolean> {
|
||||
const contribution = await Contribution.findOne(id)
|
||||
if (!contribution) {
|
||||
throw new Error('Contribution not found for given id.')
|
||||
}
|
||||
const res = await AdminPendingCreation.delete(pendingCreation)
|
||||
const res = await contribution.softRemove()
|
||||
return !!res
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CONFIRM_PENDING_CREATION])
|
||||
@Authorized([RIGHTS.CONFIRM_CONTRIBUTION])
|
||||
@Mutation(() => Boolean)
|
||||
async confirmPendingCreation(
|
||||
async confirmContribution(
|
||||
@Arg('id', () => Int) id: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const pendingCreation = await AdminPendingCreation.findOne(id)
|
||||
if (!pendingCreation) {
|
||||
throw new Error('Creation not found to given id.')
|
||||
const contribution = await Contribution.findOne(id)
|
||||
if (!contribution) {
|
||||
throw new Error('Contribution not found to given id.')
|
||||
}
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === pendingCreation.userId)
|
||||
throw new Error('Moderator can not confirm own pending creation')
|
||||
if (moderatorUser.id === contribution.userId)
|
||||
throw new Error('Moderator can not confirm own contribution')
|
||||
|
||||
const user = await dbUser.findOneOrFail({ id: pendingCreation.userId }, { withDeleted: true })
|
||||
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a creation.')
|
||||
const user = await dbUser.findOneOrFail({ id: contribution.userId }, { withDeleted: true })
|
||||
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a contribution.')
|
||||
|
||||
const creations = await getUserCreation(pendingCreation.userId, false)
|
||||
if (!isCreationValid(creations, pendingCreation.amount, pendingCreation.date)) {
|
||||
const creations = await getUserCreation(contribution.userId, false)
|
||||
if (!isContributionValid(creations, contribution.amount, contribution.contributionDate)) {
|
||||
throw new Error('Creation is not valid!!')
|
||||
}
|
||||
|
||||
const receivedCallDate = new Date()
|
||||
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const lastTransaction = await transactionRepository.findLastForUser(pendingCreation.userId)
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
try {
|
||||
const lastTransaction = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('transaction')
|
||||
.from(DbTransaction, 'transaction')
|
||||
.where('transaction.userId = :id', { id: contribution.userId })
|
||||
.orderBy('transaction.balanceDate', 'DESC')
|
||||
.getOne()
|
||||
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
|
||||
|
||||
let newBalance = new Decimal(0)
|
||||
let decay: Decay | null = null
|
||||
if (lastTransaction) {
|
||||
decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, receivedCallDate)
|
||||
newBalance = decay.balance
|
||||
let newBalance = new Decimal(0)
|
||||
let decay: Decay | null = null
|
||||
if (lastTransaction) {
|
||||
decay = calculateDecay(
|
||||
lastTransaction.balance,
|
||||
lastTransaction.balanceDate,
|
||||
receivedCallDate,
|
||||
)
|
||||
newBalance = decay.balance
|
||||
}
|
||||
newBalance = newBalance.add(contribution.amount.toString())
|
||||
|
||||
const transaction = new DbTransaction()
|
||||
transaction.typeId = TransactionTypeId.CREATION
|
||||
transaction.memo = contribution.memo
|
||||
transaction.userId = contribution.userId
|
||||
transaction.previous = lastTransaction ? lastTransaction.id : null
|
||||
transaction.amount = contribution.amount
|
||||
transaction.creationDate = contribution.contributionDate
|
||||
transaction.balance = newBalance
|
||||
transaction.balanceDate = receivedCallDate
|
||||
transaction.decay = decay ? decay.decay : new Decimal(0)
|
||||
transaction.decayStart = decay ? decay.start : null
|
||||
await queryRunner.manager.insert(DbTransaction, transaction)
|
||||
|
||||
contribution.confirmedAt = receivedCallDate
|
||||
contribution.confirmedBy = moderatorUser.id
|
||||
contribution.transactionId = transaction.id
|
||||
await queryRunner.manager.update(Contribution, { id: contribution.id }, contribution)
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info('creation commited successfuly.')
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error(`Creation was not successful: ${e}`)
|
||||
throw new Error(`Creation was not successful.`)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
newBalance = newBalance.add(pendingCreation.amount.toString())
|
||||
|
||||
const transaction = new DbTransaction()
|
||||
transaction.typeId = TransactionTypeId.CREATION
|
||||
transaction.memo = pendingCreation.memo
|
||||
transaction.userId = pendingCreation.userId
|
||||
transaction.previous = lastTransaction ? lastTransaction.id : null
|
||||
transaction.amount = pendingCreation.amount
|
||||
transaction.creationDate = pendingCreation.date
|
||||
transaction.balance = newBalance
|
||||
transaction.balanceDate = receivedCallDate
|
||||
transaction.decay = decay ? decay.decay : new Decimal(0)
|
||||
transaction.decayStart = decay ? decay.start : null
|
||||
await transaction.save().catch(() => {
|
||||
throw new Error('Unable to confirm creation.')
|
||||
})
|
||||
|
||||
await AdminPendingCreation.delete(pendingCreation)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@ -460,6 +553,135 @@ export class AdminResolver {
|
||||
linkList: transactionLinks.map((tl) => new TransactionLink(tl, new User(user))),
|
||||
}
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CREATE_CONTRIBUTION_LINK])
|
||||
@Mutation(() => ContributionLink)
|
||||
async createContributionLink(
|
||||
@Args()
|
||||
{
|
||||
amount,
|
||||
name,
|
||||
memo,
|
||||
cycle,
|
||||
validFrom,
|
||||
validTo,
|
||||
maxAmountPerMonth,
|
||||
maxPerCycle,
|
||||
}: ContributionLinkArgs,
|
||||
): Promise<ContributionLink> {
|
||||
isStartEndDateValid(validFrom, validTo)
|
||||
if (!name) {
|
||||
logger.error(`The name must be initialized!`)
|
||||
throw new Error(`The name must be initialized!`)
|
||||
}
|
||||
if (
|
||||
name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS ||
|
||||
name.length > CONTRIBUTIONLINK_NAME_MAX_CHARS
|
||||
) {
|
||||
const msg = `The value of 'name' with a length of ${name.length} did not fulfill the requested bounderies min=${CONTRIBUTIONLINK_NAME_MIN_CHARS} and max=${CONTRIBUTIONLINK_NAME_MAX_CHARS}`
|
||||
logger.error(`${msg}`)
|
||||
throw new Error(`${msg}`)
|
||||
}
|
||||
if (!memo) {
|
||||
logger.error(`The memo must be initialized!`)
|
||||
throw new Error(`The memo must be initialized!`)
|
||||
}
|
||||
if (
|
||||
memo.length < CONTRIBUTIONLINK_MEMO_MIN_CHARS ||
|
||||
memo.length > CONTRIBUTIONLINK_MEMO_MAX_CHARS
|
||||
) {
|
||||
const msg = `The value of 'memo' with a length of ${memo.length} did not fulfill the requested bounderies min=${CONTRIBUTIONLINK_MEMO_MIN_CHARS} and max=${CONTRIBUTIONLINK_MEMO_MAX_CHARS}`
|
||||
logger.error(`${msg}`)
|
||||
throw new Error(`${msg}`)
|
||||
}
|
||||
if (!amount) {
|
||||
logger.error(`The amount must be initialized!`)
|
||||
throw new Error('The amount must be initialized!')
|
||||
}
|
||||
if (!new Decimal(amount).isPositive()) {
|
||||
logger.error(`The amount=${amount} must be initialized with a positiv value!`)
|
||||
throw new Error(`The amount=${amount} must be initialized with a positiv value!`)
|
||||
}
|
||||
const dbContributionLink = new DbContributionLink()
|
||||
dbContributionLink.amount = amount
|
||||
dbContributionLink.name = name
|
||||
dbContributionLink.memo = memo
|
||||
dbContributionLink.createdAt = new Date()
|
||||
dbContributionLink.code = contributionLinkCode(dbContributionLink.createdAt)
|
||||
dbContributionLink.cycle = cycle
|
||||
if (validFrom) dbContributionLink.validFrom = new Date(validFrom)
|
||||
if (validTo) dbContributionLink.validTo = new Date(validTo)
|
||||
dbContributionLink.maxAmountPerMonth = maxAmountPerMonth
|
||||
dbContributionLink.maxPerCycle = maxPerCycle
|
||||
await dbContributionLink.save()
|
||||
logger.debug(`createContributionLink successful!`)
|
||||
return new ContributionLink(dbContributionLink)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LIST_CONTRIBUTION_LINKS])
|
||||
@Query(() => ContributionLinkList)
|
||||
async listContributionLinks(
|
||||
@Args()
|
||||
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||
): Promise<ContributionLinkList> {
|
||||
const [links, count] = await DbContributionLink.findAndCount({
|
||||
order: { createdAt: order },
|
||||
skip: (currentPage - 1) * pageSize,
|
||||
take: pageSize,
|
||||
})
|
||||
return {
|
||||
links: links.map((link: DbContributionLink) => new ContributionLink(link)),
|
||||
count,
|
||||
}
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.DELETE_CONTRIBUTION_LINK])
|
||||
@Mutation(() => Date, { nullable: true })
|
||||
async deleteContributionLink(@Arg('id', () => Int) id: number): Promise<Date | null> {
|
||||
const contributionLink = await DbContributionLink.findOne(id)
|
||||
if (!contributionLink) {
|
||||
logger.error(`Contribution Link not found to given id: ${id}`)
|
||||
throw new Error('Contribution Link not found to given id.')
|
||||
}
|
||||
await contributionLink.softRemove()
|
||||
logger.debug(`deleteContributionLink successful!`)
|
||||
const newContributionLink = await DbContributionLink.findOne({ id }, { withDeleted: true })
|
||||
return newContributionLink ? newContributionLink.deletedAt : null
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.UPDATE_CONTRIBUTION_LINK])
|
||||
@Mutation(() => ContributionLink)
|
||||
async updateContributionLink(
|
||||
@Args()
|
||||
{
|
||||
amount,
|
||||
name,
|
||||
memo,
|
||||
cycle,
|
||||
validFrom,
|
||||
validTo,
|
||||
maxAmountPerMonth,
|
||||
maxPerCycle,
|
||||
}: ContributionLinkArgs,
|
||||
@Arg('id', () => Int) id: number,
|
||||
): Promise<ContributionLink> {
|
||||
const dbContributionLink = await DbContributionLink.findOne(id)
|
||||
if (!dbContributionLink) {
|
||||
logger.error(`Contribution Link not found to given id: ${id}`)
|
||||
throw new Error('Contribution Link not found to given id.')
|
||||
}
|
||||
dbContributionLink.amount = amount
|
||||
dbContributionLink.name = name
|
||||
dbContributionLink.memo = memo
|
||||
dbContributionLink.cycle = cycle
|
||||
if (validFrom) dbContributionLink.validFrom = new Date(validFrom)
|
||||
if (validTo) dbContributionLink.validTo = new Date(validTo)
|
||||
dbContributionLink.maxAmountPerMonth = maxAmountPerMonth
|
||||
dbContributionLink.maxPerCycle = maxPerCycle
|
||||
await dbContributionLink.save()
|
||||
logger.debug(`updateContributionLink successful!`)
|
||||
return new ContributionLink(dbContributionLink)
|
||||
}
|
||||
}
|
||||
|
||||
interface CreationMap {
|
||||
@ -467,25 +689,30 @@ interface CreationMap {
|
||||
creations: Decimal[]
|
||||
}
|
||||
|
||||
async function getUserCreation(id: number, includePending = true): Promise<Decimal[]> {
|
||||
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
||||
logger.trace('getUserCreation', id, includePending)
|
||||
const creations = await getUserCreations([id], includePending)
|
||||
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
||||
}
|
||||
|
||||
async function getUserCreations(ids: number[], includePending = true): Promise<CreationMap[]> {
|
||||
logger.trace('getUserCreations:', ids, includePending)
|
||||
const months = getCreationMonths()
|
||||
logger.trace('getUserCreations months', months)
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
|
||||
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
||||
logger.trace('getUserCreations dateFilter', dateFilter)
|
||||
|
||||
const unionString = includePending
|
||||
? `
|
||||
UNION
|
||||
SELECT date AS date, amount AS amount, userId AS userId FROM admin_pending_creations
|
||||
WHERE userId IN (${ids.toString()})
|
||||
AND date >= ${dateFilter}`
|
||||
SELECT contribution_date AS date, amount AS amount, user_id AS userId FROM contributions
|
||||
WHERE user_id IN (${ids.toString()})
|
||||
AND contribution_date >= ${dateFilter}
|
||||
AND confirmed_at IS NULL AND deleted_at IS NULL`
|
||||
: ''
|
||||
|
||||
const unionQuery = await queryRunner.manager.query(`
|
||||
@ -515,17 +742,22 @@ async function getUserCreations(ids: number[], includePending = true): Promise<C
|
||||
})
|
||||
}
|
||||
|
||||
function updateCreations(creations: Decimal[], pendingCreation: AdminPendingCreation): Decimal[] {
|
||||
const index = getCreationIndex(pendingCreation.date.getMonth())
|
||||
function updateCreations(creations: Decimal[], contribution: Contribution): Decimal[] {
|
||||
const index = getCreationIndex(contribution.contributionDate.getMonth())
|
||||
|
||||
if (index < 0) {
|
||||
throw new Error('You cannot create GDD for a month older than the last three months.')
|
||||
}
|
||||
creations[index] = creations[index].plus(pendingCreation.amount.toString())
|
||||
creations[index] = creations[index].plus(contribution.amount.toString())
|
||||
return creations
|
||||
}
|
||||
|
||||
function isCreationValid(creations: Decimal[], amount: Decimal, creationDate: Date) {
|
||||
export const isContributionValid = (
|
||||
creations: Decimal[],
|
||||
amount: Decimal,
|
||||
creationDate: Date,
|
||||
): boolean => {
|
||||
logger.trace('isContributionValid', creations, amount, creationDate)
|
||||
const index = getCreationIndex(creationDate.getMonth())
|
||||
|
||||
if (index < 0) {
|
||||
@ -541,6 +773,27 @@ function isCreationValid(creations: Decimal[], amount: Decimal, creationDate: Da
|
||||
return true
|
||||
}
|
||||
|
||||
const isStartEndDateValid = (
|
||||
startDate: string | null | undefined,
|
||||
endDate: string | null | undefined,
|
||||
): void => {
|
||||
if (!startDate) {
|
||||
logger.error('Start-Date is not initialized. A Start-Date must be set!')
|
||||
throw new Error('Start-Date is not initialized. A Start-Date must be set!')
|
||||
}
|
||||
|
||||
if (!endDate) {
|
||||
logger.error('End-Date is not initialized. An End-Date must be set!')
|
||||
throw new Error('End-Date is not initialized. An End-Date must be set!')
|
||||
}
|
||||
|
||||
// check if endDate is before startDate
|
||||
if (new Date(endDate).getTime() - new Date(startDate).getTime() < 0) {
|
||||
logger.error(`The value of validFrom must before or equals the validTo!`)
|
||||
throw new Error(`The value of validFrom must before or equals the validTo!`)
|
||||
}
|
||||
}
|
||||
|
||||
const getCreationMonths = (): number[] => {
|
||||
const now = new Date(Date.now())
|
||||
return [
|
||||
|
||||
@ -1,7 +1,21 @@
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import { Resolver, Args, Arg, Authorized, Ctx, Mutation, Query, Int } from 'type-graphql'
|
||||
import { getConnection } from '@dbTools/typeorm'
|
||||
import {
|
||||
Resolver,
|
||||
Args,
|
||||
Arg,
|
||||
Authorized,
|
||||
Ctx,
|
||||
Mutation,
|
||||
Query,
|
||||
Int,
|
||||
createUnionType,
|
||||
} from 'type-graphql'
|
||||
import { TransactionLink } from '@model/TransactionLink'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import TransactionLinkArgs from '@arg/TransactionLinkArgs'
|
||||
import Paginated from '@arg/Paginated'
|
||||
@ -12,6 +26,17 @@ import { User } from '@model/User'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
import { executeTransaction } from './TransactionResolver'
|
||||
import { Order } from '@enum/Order'
|
||||
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { getUserCreation, isContributionValid } from './AdminResolver'
|
||||
import { Decay } from '@model/Decay'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||
|
||||
const QueryLinkResult = createUnionType({
|
||||
name: 'QueryLinkResult', // the name of the GraphQL union
|
||||
types: () => [TransactionLink, ContributionLink] as const, // function that returns tuple of object types classes
|
||||
})
|
||||
|
||||
// TODO: do not export, test it inside the resolver
|
||||
export const transactionLinkCode = (date: Date): string => {
|
||||
@ -95,15 +120,23 @@ export class TransactionLinkResolver {
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.QUERY_TRANSACTION_LINK])
|
||||
@Query(() => TransactionLink)
|
||||
async queryTransactionLink(@Arg('code') code: string): Promise<TransactionLink> {
|
||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
|
||||
const user = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
||||
let redeemedBy: User | null = null
|
||||
if (transactionLink && transactionLink.redeemedBy) {
|
||||
redeemedBy = new User(await dbUser.findOneOrFail({ id: transactionLink.redeemedBy }))
|
||||
@Query(() => QueryLinkResult)
|
||||
async queryTransactionLink(@Arg('code') code: string): Promise<typeof QueryLinkResult> {
|
||||
if (code.match(/^CL-/)) {
|
||||
const contributionLink = await DbContributionLink.findOneOrFail(
|
||||
{ code: code.replace('CL-', '') },
|
||||
{ withDeleted: true },
|
||||
)
|
||||
return new ContributionLink(contributionLink)
|
||||
} else {
|
||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
|
||||
const user = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
||||
let redeemedBy: User | null = null
|
||||
if (transactionLink && transactionLink.redeemedBy) {
|
||||
redeemedBy = new User(await dbUser.findOneOrFail({ id: transactionLink.redeemedBy }))
|
||||
}
|
||||
return new TransactionLink(transactionLink, new User(user), redeemedBy)
|
||||
}
|
||||
return new TransactionLink(transactionLink, new User(user), redeemedBy)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS])
|
||||
@ -137,31 +170,143 @@ export class TransactionLinkResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const user = getUser(context)
|
||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
||||
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
||||
|
||||
const now = new Date()
|
||||
|
||||
if (user.id === linkedUser.id) {
|
||||
throw new Error('Cannot redeem own transaction link.')
|
||||
if (code.match(/^CL-/)) {
|
||||
logger.info('redeem contribution link...')
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('SERIALIZABLE')
|
||||
try {
|
||||
const contributionLink = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('contributionLink')
|
||||
.from(DbContributionLink, 'contributionLink')
|
||||
.where('contributionLink.code = :code', { code: code.replace('CL-', '') })
|
||||
.getOne()
|
||||
if (!contributionLink) {
|
||||
logger.error('no contribution link found to given code:', code)
|
||||
throw new Error('No contribution link found')
|
||||
}
|
||||
logger.info('...contribution link found with id', contributionLink.id)
|
||||
if (new Date(contributionLink.validFrom).getTime() > now.getTime()) {
|
||||
logger.error(
|
||||
'contribution link is not valid yet. Valid from: ',
|
||||
contributionLink.validFrom,
|
||||
)
|
||||
throw new Error('Contribution link not valid yet')
|
||||
}
|
||||
if (contributionLink.validTo) {
|
||||
if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) {
|
||||
logger.error('contribution link is depricated. Valid to: ', contributionLink.validTo)
|
||||
throw new Error('Contribution link is depricated')
|
||||
}
|
||||
}
|
||||
if (contributionLink.cycle !== 'ONCE') {
|
||||
logger.error('contribution link has unknown cycle', contributionLink.cycle)
|
||||
throw new Error('Contribution link has unknown cycle')
|
||||
}
|
||||
// Test ONCE rule
|
||||
const alreadyRedeemed = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('contribution')
|
||||
.from(DbContribution, 'contribution')
|
||||
.where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', {
|
||||
linkId: contributionLink.id,
|
||||
id: user.id,
|
||||
})
|
||||
.getOne()
|
||||
if (alreadyRedeemed) {
|
||||
logger.error('contribution link with rule ONCE already redeemed by user with id', user.id)
|
||||
throw new Error('Contribution link already redeemed')
|
||||
}
|
||||
|
||||
const creations = await getUserCreation(user.id, false)
|
||||
logger.info('open creations', creations)
|
||||
if (!isContributionValid(creations, contributionLink.amount, now)) {
|
||||
logger.error(
|
||||
'Amount of Contribution link exceeds available amount for this month',
|
||||
contributionLink.amount,
|
||||
)
|
||||
throw new Error('Amount of Contribution link exceeds available amount')
|
||||
}
|
||||
const contribution = new DbContribution()
|
||||
contribution.userId = user.id
|
||||
contribution.createdAt = now
|
||||
contribution.contributionDate = now
|
||||
contribution.memo = contributionLink.memo
|
||||
contribution.amount = contributionLink.amount
|
||||
contribution.contributionLinkId = contributionLink.id
|
||||
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.balanceDate', 'DESC')
|
||||
.getOne()
|
||||
let newBalance = new Decimal(0)
|
||||
|
||||
let decay: Decay | null = null
|
||||
if (lastTransaction) {
|
||||
decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now)
|
||||
newBalance = decay.balance
|
||||
}
|
||||
newBalance = newBalance.add(contributionLink.amount.toString())
|
||||
|
||||
const transaction = new DbTransaction()
|
||||
transaction.typeId = TransactionTypeId.CREATION
|
||||
transaction.memo = contribution.memo
|
||||
transaction.userId = contribution.userId
|
||||
transaction.previous = lastTransaction ? lastTransaction.id : null
|
||||
transaction.amount = contribution.amount
|
||||
transaction.creationDate = contribution.contributionDate
|
||||
transaction.balance = newBalance
|
||||
transaction.balanceDate = now
|
||||
transaction.decay = decay ? decay.decay : new Decimal(0)
|
||||
transaction.decayStart = decay ? decay.start : null
|
||||
await queryRunner.manager.insert(DbTransaction, transaction)
|
||||
|
||||
contribution.confirmedAt = now
|
||||
contribution.transactionId = transaction.id
|
||||
await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info('creation from contribution link commited successfuly.')
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error(`Creation from contribution link was not successful: ${e}`)
|
||||
throw new Error(`Creation from contribution link was not successful. ${e}`)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
||||
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
||||
|
||||
if (user.id === linkedUser.id) {
|
||||
throw new Error('Cannot redeem own transaction link.')
|
||||
}
|
||||
|
||||
if (transactionLink.validUntil.getTime() < now.getTime()) {
|
||||
throw new Error('Transaction Link is not valid anymore.')
|
||||
}
|
||||
|
||||
if (transactionLink.redeemedBy) {
|
||||
throw new Error('Transaction Link already redeemed.')
|
||||
}
|
||||
|
||||
await executeTransaction(
|
||||
transactionLink.amount,
|
||||
transactionLink.memo,
|
||||
linkedUser,
|
||||
user,
|
||||
transactionLink,
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if (transactionLink.validUntil.getTime() < now.getTime()) {
|
||||
throw new Error('Transaction Link is not valid anymore.')
|
||||
}
|
||||
|
||||
if (transactionLink.redeemedBy) {
|
||||
throw new Error('Transaction Link already redeemed.')
|
||||
}
|
||||
|
||||
await executeTransaction(
|
||||
transactionLink.amount,
|
||||
transactionLink.memo,
|
||||
linkedUser,
|
||||
user,
|
||||
transactionLink,
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,8 +11,13 @@ import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { User } from '@entity/User'
|
||||
import CONFIG from '@/config'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
||||
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
||||
import { printTimeDuration, activationLink } from './UserResolver'
|
||||
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
||||
// import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
// import { TransactionLink } from '@entity/TransactionLink'
|
||||
|
||||
import { logger } from '@test/testSetup'
|
||||
|
||||
@ -25,6 +30,13 @@ jest.mock('@/mailer/sendAccountActivationEmail', () => {
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/mailer/sendAccountMultiRegistrationEmail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendAccountMultiRegistrationEmail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/mailer/sendResetPasswordEmail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
@ -69,6 +81,7 @@ describe('UserResolver', () => {
|
||||
|
||||
let result: any
|
||||
let emailOptIn: string
|
||||
let user: User[]
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
@ -86,7 +99,6 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
describe('valid input data', () => {
|
||||
let user: User[]
|
||||
let loginEmailOptIn: LoginEmailOptIn[]
|
||||
beforeAll(async () => {
|
||||
user = await User.find()
|
||||
@ -114,6 +126,7 @@ describe('UserResolver', () => {
|
||||
deletedAt: null,
|
||||
publisherId: 1234,
|
||||
referrerId: null,
|
||||
contributionLinkId: null,
|
||||
},
|
||||
])
|
||||
})
|
||||
@ -151,14 +164,33 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
describe('email already exists', () => {
|
||||
it('throws and logs an error', async () => {
|
||||
const mutation = await mutate({ mutation: createUser, variables })
|
||||
let mutation: User
|
||||
beforeAll(async () => {
|
||||
mutation = await mutate({ mutation: createUser, variables })
|
||||
})
|
||||
|
||||
it('logs an info', async () => {
|
||||
expect(logger.info).toBeCalledWith('User already exists with this email=peter@lustig.de')
|
||||
})
|
||||
|
||||
it('sends an account multi registration email', () => {
|
||||
expect(sendAccountMultiRegistrationEmail).toBeCalledWith({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
})
|
||||
})
|
||||
|
||||
it('results with partly faked user with random "id"', async () => {
|
||||
expect(mutation).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('User already exists.')],
|
||||
data: {
|
||||
createUser: {
|
||||
id: expect.any(Number),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('User already exists with this email=peter@lustig.de')
|
||||
})
|
||||
})
|
||||
|
||||
@ -195,6 +227,72 @@ describe('UserResolver', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('redeem codes', () => {
|
||||
describe('contribution link', () => {
|
||||
let link: ContributionLink
|
||||
beforeAll(async () => {
|
||||
// activate account of admin Peter Lustig
|
||||
await mutate({
|
||||
mutation: setPassword,
|
||||
variables: { code: emailOptIn, password: 'Aa12345_' },
|
||||
})
|
||||
// make Peter Lustig Admin
|
||||
const peter = await User.findOneOrFail({ id: user[0].id })
|
||||
peter.isAdmin = new Date()
|
||||
await peter.save()
|
||||
// factory logs in as Peter Lustig
|
||||
link = await contributionLinkFactory(testEnv, {
|
||||
name: 'Dokumenta 2022',
|
||||
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022',
|
||||
amount: 200,
|
||||
validFrom: new Date(2022, 5, 18),
|
||||
validTo: new Date(2022, 8, 25),
|
||||
})
|
||||
resetToken()
|
||||
await mutate({
|
||||
mutation: createUser,
|
||||
variables: { ...variables, email: 'ein@besucher.de', redeemCode: 'CL-' + link.code },
|
||||
})
|
||||
})
|
||||
|
||||
it('sets the contribution link id', async () => {
|
||||
await expect(User.findOne({ email: 'ein@besucher.de' })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
contributionLinkId: link.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
/* A transaction link requires GDD on account
|
||||
describe('transaction link', () => {
|
||||
let code: string
|
||||
beforeAll(async () => {
|
||||
// factory logs in as Peter Lustig
|
||||
await transactionLinkFactory(testEnv, {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 19.99,
|
||||
memo: `Kein Trick, keine Zauberrei,
|
||||
bei Gradidio sei dabei!`,
|
||||
})
|
||||
const transactionLink = await TransactionLink.findOneOrFail()
|
||||
resetToken()
|
||||
await mutate({
|
||||
mutation: createUser,
|
||||
variables: { ...variables, email: 'neuer@user.de', redeemCode: transactionLink.code },
|
||||
})
|
||||
})
|
||||
|
||||
it('sets the referrer id to Peter Lustigs id', async () => {
|
||||
await expect(User.findOne({ email: 'neuer@user.de' })).resolves.toEqual(expect.objectContaining({
|
||||
referrerId: user[0].id,
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
*/
|
||||
})
|
||||
})
|
||||
|
||||
describe('setPassword', () => {
|
||||
|
||||
@ -7,7 +7,9 @@ import { getConnection } from '@dbTools/typeorm'
|
||||
import CONFIG from '@/config'
|
||||
import { User } from '@model/User'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { communityDbUser } from '@/util/communityUser'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
||||
import { encode } from '@/auth/JWT'
|
||||
import CreateUserArgs from '@arg/CreateUserArgs'
|
||||
import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
|
||||
@ -17,6 +19,7 @@ import { OptInType } from '@enum/OptInType'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
||||
import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||
@ -327,10 +330,35 @@ export class UserResolver {
|
||||
// TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes
|
||||
const userFound = await DbUser.findOne({ email }, { withDeleted: true })
|
||||
logger.info(`DbUser.findOne(email=${email}) = ${userFound}`)
|
||||
|
||||
if (userFound) {
|
||||
logger.error('User already exists with this email=' + email)
|
||||
logger.info('User already exists with this email=' + email)
|
||||
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
||||
throw new Error(`User already exists.`)
|
||||
|
||||
const user = new User(communityDbUser)
|
||||
user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in?
|
||||
user.email = email
|
||||
user.firstName = firstName
|
||||
user.lastName = lastName
|
||||
user.language = language
|
||||
user.publisherId = publisherId
|
||||
logger.debug('partly faked user=' + user)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const emailSent = await sendAccountMultiRegistrationEmail({
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
})
|
||||
logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`)
|
||||
/* uncomment this, when you need the activation link on the console */
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
logger.debug(`Email not send!`)
|
||||
}
|
||||
logger.info('createUser() faked and send multi registration mail...')
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
const passphrase = PassphraseGenerate()
|
||||
@ -349,10 +377,20 @@ export class UserResolver {
|
||||
dbUser.passphrase = passphrase.join(' ')
|
||||
logger.debug('new dbUser=' + dbUser)
|
||||
if (redeemCode) {
|
||||
const transactionLink = await dbTransactionLink.findOne({ code: redeemCode })
|
||||
logger.info('redeemCode found transactionLink=' + transactionLink)
|
||||
if (transactionLink) {
|
||||
dbUser.referrerId = transactionLink.userId
|
||||
if (redeemCode.match(/^CL-/)) {
|
||||
const contributionLink = await dbContributionLink.findOne({
|
||||
code: redeemCode.replace('CL-', ''),
|
||||
})
|
||||
logger.info('redeemCode found contributionLink=' + contributionLink)
|
||||
if (contributionLink) {
|
||||
dbUser.contributionLinkId = contributionLink.id
|
||||
}
|
||||
} else {
|
||||
const transactionLink = await dbTransactionLink.findOne({ code: redeemCode })
|
||||
logger.info('redeemCode found transactionLink=' + transactionLink)
|
||||
if (transactionLink) {
|
||||
dbUser.referrerId = transactionLink.userId
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO this field has no null allowed unlike the loginServer table
|
||||
@ -406,6 +444,7 @@ export class UserResolver {
|
||||
await queryRunner.release()
|
||||
}
|
||||
logger.info('createUser() successful...')
|
||||
|
||||
return new User(dbUser)
|
||||
}
|
||||
|
||||
|
||||
31
backend/src/mailer/sendAccountMultiRegistrationEmail.test.ts
Normal file
31
backend/src/mailer/sendAccountMultiRegistrationEmail.test.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import CONFIG from '@/config'
|
||||
import { sendAccountMultiRegistrationEmail } from './sendAccountMultiRegistrationEmail'
|
||||
import { sendEMail } from './sendEMail'
|
||||
|
||||
jest.mock('./sendEMail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendEMail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('sendAccountMultiRegistrationEmail', () => {
|
||||
beforeEach(async () => {
|
||||
await sendAccountMultiRegistrationEmail({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls sendEMail', () => {
|
||||
expect(sendEMail).toBeCalledWith({
|
||||
to: `Peter Lustig <peter@lustig.de>`,
|
||||
subject: 'Gradido: Erneuter Registrierungsversuch mit deiner E-Mail',
|
||||
text:
|
||||
expect.stringContaining('Hallo Peter Lustig') &&
|
||||
expect.stringContaining(CONFIG.EMAIL_LINK_FORGOTPASSWORD) &&
|
||||
expect.stringContaining('https://gradido.net/de/contact/'),
|
||||
})
|
||||
})
|
||||
})
|
||||
18
backend/src/mailer/sendAccountMultiRegistrationEmail.ts
Normal file
18
backend/src/mailer/sendAccountMultiRegistrationEmail.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { accountMultiRegistration } from './text/accountMultiRegistration'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
export const sendAccountMultiRegistrationEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
}): Promise<boolean> => {
|
||||
return sendEMail({
|
||||
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
||||
subject: accountMultiRegistration.de.subject,
|
||||
text: accountMultiRegistration.de.text({
|
||||
...data,
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
}),
|
||||
})
|
||||
}
|
||||
@ -31,6 +31,7 @@ describe('sendEMail', () => {
|
||||
beforeEach(async () => {
|
||||
result = await sendEMail({
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
@ -50,6 +51,7 @@ describe('sendEMail', () => {
|
||||
CONFIG.EMAIL = true
|
||||
result = await sendEMail({
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
@ -72,6 +74,7 @@ describe('sendEMail', () => {
|
||||
expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({
|
||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
|
||||
@ -5,10 +5,15 @@ import CONFIG from '@/config'
|
||||
|
||||
export const sendEMail = async (emailDef: {
|
||||
to: string
|
||||
cc?: string
|
||||
subject: string
|
||||
text: string
|
||||
}): Promise<boolean> => {
|
||||
logger.info(`send Email: to=${emailDef.to}, subject=${emailDef.subject}, text=${emailDef.text}`)
|
||||
logger.info(
|
||||
`send Email: to=${emailDef.to}` +
|
||||
(emailDef.cc ? `, cc=${emailDef.cc}` : '') +
|
||||
`, subject=${emailDef.subject}, text=${emailDef.text}`,
|
||||
)
|
||||
|
||||
if (!CONFIG.EMAIL) {
|
||||
logger.info(`Emails are disabled via config...`)
|
||||
|
||||
25
backend/src/mailer/text/accountMultiRegistration.ts
Normal file
25
backend/src/mailer/text/accountMultiRegistration.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export const accountMultiRegistration = {
|
||||
de: {
|
||||
subject: 'Gradido: Erneuter Registrierungsversuch mit deiner E-Mail',
|
||||
text: (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
resendLink: string
|
||||
}): string =>
|
||||
`Hallo ${data.firstName} ${data.lastName},
|
||||
|
||||
Deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.
|
||||
Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto.
|
||||
|
||||
Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest:
|
||||
${data.resendLink}
|
||||
oder kopiere den obigen Link in dein Browserfenster.
|
||||
|
||||
Wenn du nicht derjenige bist, der sich versucht hat erneut zu registrieren, wende dich bitte an unseren support:
|
||||
https://gradido.net/de/contact/
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
dein Gradido-Team`,
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
export interface ContributionLinkInterface {
|
||||
amount: number
|
||||
name: string
|
||||
memo: string
|
||||
validFrom?: Date
|
||||
validTo?: Date
|
||||
}
|
||||
18
backend/src/seeds/contributionLink/index.ts
Normal file
18
backend/src/seeds/contributionLink/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { ContributionLinkInterface } from './ContributionLinkInterface'
|
||||
|
||||
export const contributionLinks: ContributionLinkInterface[] = [
|
||||
{
|
||||
name: 'Dokumenta 2017',
|
||||
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2017',
|
||||
amount: 200,
|
||||
validFrom: new Date(2017, 3, 8),
|
||||
validTo: new Date(2017, 6, 16),
|
||||
},
|
||||
{
|
||||
name: 'Dokumenta 2022',
|
||||
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022',
|
||||
amount: 200,
|
||||
validFrom: new Date(2022, 5, 18),
|
||||
validTo: new Date(2022, 8, 25),
|
||||
},
|
||||
]
|
||||
29
backend/src/seeds/factory/contributionLink.ts
Normal file
29
backend/src/seeds/factory/contributionLink.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { createContributionLink } from '@/seeds/graphql/mutations'
|
||||
import { login } from '@/seeds/graphql/queries'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface'
|
||||
|
||||
export const contributionLinkFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
contributionLink: ContributionLinkInterface,
|
||||
): Promise<ContributionLink> => {
|
||||
const { mutate, query } = client
|
||||
|
||||
// login as admin
|
||||
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||
|
||||
const variables = {
|
||||
amount: contributionLink.amount,
|
||||
memo: contributionLink.memo,
|
||||
name: contributionLink.name,
|
||||
cycle: 'ONCE',
|
||||
maxPerCycle: 1,
|
||||
maxAmountPerMonth: 200,
|
||||
validFrom: contributionLink.validFrom ? contributionLink.validFrom.toISOString() : undefined,
|
||||
validTo: contributionLink.validTo ? contributionLink.validTo.toISOString() : undefined,
|
||||
}
|
||||
|
||||
const result = await mutate({ mutation: createContributionLink, variables })
|
||||
return result.data.createContributionLink
|
||||
}
|
||||
@ -1,13 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { createPendingCreation, confirmPendingCreation } from '@/seeds/graphql/mutations'
|
||||
import { adminCreateContribution, confirmContribution } from '@/seeds/graphql/mutations'
|
||||
import { login } from '@/seeds/graphql/queries'
|
||||
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { User } from '@entity/User'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
// import CONFIG from '@/config/index'
|
||||
|
||||
export const nMonthsBefore = (date: Date, months = 1): string => {
|
||||
@ -17,23 +17,23 @@ export const nMonthsBefore = (date: Date, months = 1): string => {
|
||||
export const creationFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
creation: CreationInterface,
|
||||
): Promise<AdminPendingCreation | void> => {
|
||||
): Promise<Contribution | void> => {
|
||||
const { mutate, query } = client
|
||||
|
||||
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||
|
||||
// TODO it would be nice to have this mutation return the id
|
||||
await mutate({ mutation: createPendingCreation, variables: { ...creation } })
|
||||
await mutate({ mutation: adminCreateContribution, variables: { ...creation } })
|
||||
|
||||
const user = await User.findOneOrFail({ where: { email: creation.email } })
|
||||
|
||||
const pendingCreation = await AdminPendingCreation.findOneOrFail({
|
||||
const pendingCreation = await Contribution.findOneOrFail({
|
||||
where: { userId: user.id, amount: creation.amount },
|
||||
order: { created: 'DESC' },
|
||||
order: { createdAt: 'DESC' },
|
||||
})
|
||||
|
||||
if (creation.confirmed) {
|
||||
await mutate({ mutation: confirmPendingCreation, variables: { id: pendingCreation.id } })
|
||||
await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } })
|
||||
|
||||
if (creation.moveCreationDate) {
|
||||
const transaction = await Transaction.findOneOrFail({
|
||||
|
||||
@ -81,15 +81,26 @@ export const createTransactionLink = gql`
|
||||
|
||||
// from admin interface
|
||||
|
||||
export const createPendingCreation = gql`
|
||||
export const adminCreateContribution = gql`
|
||||
mutation ($email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||
createPendingCreation(email: $email, amount: $amount, memo: $memo, creationDate: $creationDate)
|
||||
adminCreateContribution(
|
||||
email: $email
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
creationDate: $creationDate
|
||||
)
|
||||
}
|
||||
`
|
||||
|
||||
export const confirmPendingCreation = gql`
|
||||
export const confirmContribution = gql`
|
||||
mutation ($id: Int!) {
|
||||
confirmPendingCreation(id: $id)
|
||||
confirmContribution(id: $id)
|
||||
}
|
||||
`
|
||||
|
||||
export const setUserRole = gql`
|
||||
mutation ($userId: Int!, $isAdmin: Boolean!) {
|
||||
setUserRole(userId: $userId, isAdmin: $isAdmin)
|
||||
}
|
||||
`
|
||||
|
||||
@ -105,19 +116,19 @@ export const unDeleteUser = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const createPendingCreations = gql`
|
||||
mutation ($pendingCreations: [CreatePendingCreationArgs!]!) {
|
||||
createPendingCreations(pendingCreations: $pendingCreations) {
|
||||
export const adminCreateContributions = gql`
|
||||
mutation ($pendingCreations: [AdminCreateContributionArgs!]!) {
|
||||
adminCreateContributions(pendingCreations: $pendingCreations) {
|
||||
success
|
||||
successfulCreation
|
||||
failedCreation
|
||||
successfulContribution
|
||||
failedContribution
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const updatePendingCreation = gql`
|
||||
export const adminUpdateContribution = gql`
|
||||
mutation ($id: Int!, $email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||
updatePendingCreation(
|
||||
adminUpdateContribution(
|
||||
id: $id
|
||||
email: $email
|
||||
amount: $amount
|
||||
@ -132,8 +143,90 @@ export const updatePendingCreation = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const deletePendingCreation = gql`
|
||||
export const adminDeleteContribution = gql`
|
||||
mutation ($id: Int!) {
|
||||
deletePendingCreation(id: $id)
|
||||
adminDeleteContribution(id: $id)
|
||||
}
|
||||
`
|
||||
|
||||
export const createContributionLink = gql`
|
||||
mutation (
|
||||
$amount: Decimal!
|
||||
$name: String!
|
||||
$memo: String!
|
||||
$cycle: String!
|
||||
$validFrom: String
|
||||
$validTo: String
|
||||
$maxAmountPerMonth: Decimal
|
||||
$maxPerCycle: Int! = 1
|
||||
) {
|
||||
createContributionLink(
|
||||
amount: $amount
|
||||
name: $name
|
||||
memo: $memo
|
||||
cycle: $cycle
|
||||
validFrom: $validFrom
|
||||
validTo: $validTo
|
||||
maxAmountPerMonth: $maxAmountPerMonth
|
||||
maxPerCycle: $maxPerCycle
|
||||
) {
|
||||
id
|
||||
amount
|
||||
name
|
||||
memo
|
||||
code
|
||||
link
|
||||
createdAt
|
||||
validFrom
|
||||
validTo
|
||||
maxAmountPerMonth
|
||||
cycle
|
||||
maxPerCycle
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const updateContributionLink = gql`
|
||||
mutation (
|
||||
$amount: Decimal!
|
||||
$name: String!
|
||||
$memo: String!
|
||||
$cycle: String!
|
||||
$validFrom: String
|
||||
$validTo: String
|
||||
$maxAmountPerMonth: Decimal
|
||||
$maxPerCycle: Int! = 1
|
||||
$id: Int!
|
||||
) {
|
||||
updateContributionLink(
|
||||
amount: $amount
|
||||
name: $name
|
||||
memo: $memo
|
||||
cycle: $cycle
|
||||
validFrom: $validFrom
|
||||
validTo: $validTo
|
||||
maxAmountPerMonth: $maxAmountPerMonth
|
||||
maxPerCycle: $maxPerCycle
|
||||
id: $id
|
||||
) {
|
||||
id
|
||||
amount
|
||||
name
|
||||
memo
|
||||
code
|
||||
link
|
||||
createdAt
|
||||
validFrom
|
||||
validTo
|
||||
maxAmountPerMonth
|
||||
cycle
|
||||
maxPerCycle
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const deleteContributionLink = gql`
|
||||
mutation ($id: Int!) {
|
||||
deleteContributionLink(id: $id)
|
||||
}
|
||||
`
|
||||
|
||||
@ -110,6 +110,7 @@ export const searchUsers = gql`
|
||||
hasElopage
|
||||
emailConfirmationSend
|
||||
deletedAt
|
||||
isAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -173,9 +174,9 @@ export const queryTransactionLink = gql`
|
||||
|
||||
// from admin interface
|
||||
|
||||
export const getPendingCreations = gql`
|
||||
export const listUnconfirmedContributions = gql`
|
||||
query {
|
||||
getPendingCreations {
|
||||
listUnconfirmedContributions {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
@ -217,3 +218,25 @@ export const listTransactionLinksAdmin = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const listContributionLinks = gql`
|
||||
query ($pageSize: Int = 25, $currentPage: Int = 1, $order: Order) {
|
||||
listContributionLinks(pageSize: $pageSize, currentPage: $currentPage, order: $order) {
|
||||
links {
|
||||
id
|
||||
amount
|
||||
name
|
||||
memo
|
||||
code
|
||||
link
|
||||
createdAt
|
||||
validFrom
|
||||
validTo
|
||||
maxAmountPerMonth
|
||||
cycle
|
||||
maxPerCycle
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -9,9 +9,11 @@ import { name, internet, datatype } from 'faker'
|
||||
import { users } from './users/index'
|
||||
import { creations } from './creation/index'
|
||||
import { transactionLinks } from './transactionLink/index'
|
||||
import { contributionLinks } from './contributionLink/index'
|
||||
import { userFactory } from './factory/user'
|
||||
import { creationFactory } from './factory/creation'
|
||||
import { transactionLinkFactory } from './factory/transactionLink'
|
||||
import { contributionLinkFactory } from './factory/contributionLink'
|
||||
import { entities } from '@entity/index'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
@ -77,6 +79,11 @@ const run = async () => {
|
||||
await transactionLinkFactory(seedClient, transactionLinks[i])
|
||||
}
|
||||
|
||||
// create Contribution Links
|
||||
for (let i = 0; i < contributionLinks.length; i++) {
|
||||
await contributionLinkFactory(seedClient, contributionLinks[i])
|
||||
}
|
||||
|
||||
await con.close()
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,8 @@ import { readFileSync } from 'fs'
|
||||
|
||||
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
|
||||
|
||||
options.categories.default.level = CONFIG.LOG_LEVEL
|
||||
options.categories.backend.level = CONFIG.LOG_LEVEL
|
||||
options.categories.apollo.level = CONFIG.LOG_LEVEL
|
||||
|
||||
log4js.configure(options)
|
||||
|
||||
|
||||
@ -37,9 +37,11 @@ ${mutation || query}variables: ${JSON.stringify(filterVariables(variables), null
|
||||
return {
|
||||
willSendResponse(requestContext: any) {
|
||||
if (requestContext.context.user) logger.info(`User ID: ${requestContext.context.user.id}`)
|
||||
if (requestContext.response.data)
|
||||
logger.info(`Response-Data:
|
||||
if (requestContext.response.data) {
|
||||
logger.info('Response Success!')
|
||||
logger.trace(`Response-Data:
|
||||
${JSON.stringify(requestContext.response.data, null, 2)}`)
|
||||
}
|
||||
if (requestContext.response.errors)
|
||||
logger.error(`Response-Errors:
|
||||
${JSON.stringify(requestContext.response.errors, null, 2)}`)
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm'
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
|
||||
@Entity('contribution_links')
|
||||
export class ContributionLink extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ length: 100, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
name: string
|
||||
|
||||
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
memo: string
|
||||
|
||||
@Column({ name: 'valid_from', type: 'datetime', nullable: false })
|
||||
validFrom: Date
|
||||
|
||||
@Column({ name: 'valid_to', type: 'datetime', nullable: true, default: null })
|
||||
validTo: Date | null
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
amount: Decimal
|
||||
|
||||
@Column({ length: 12, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
cycle: string
|
||||
|
||||
@Column({ name: 'max_per_cycle', unsigned: true, nullable: false, default: 1 })
|
||||
maxPerCycle: number
|
||||
|
||||
@Column({
|
||||
name: 'max_amount_per_month',
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: true,
|
||||
default: null,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
maxAmountPerMonth: Decimal | null
|
||||
|
||||
@Column({
|
||||
name: 'total_max_count_of_contribution',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
totalMaxCountOfContribution: number | null
|
||||
|
||||
@Column({
|
||||
name: 'max_account_balance',
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: true,
|
||||
default: null,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
maxAccountBalance: Decimal | null
|
||||
|
||||
@Column({
|
||||
name: 'min_gap_hours',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
minGapHours: number | null
|
||||
|
||||
@Column({ name: 'created_at', type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt: Date
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at' })
|
||||
deletedAt: Date | null
|
||||
|
||||
@Column({ length: 24, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
code: string
|
||||
|
||||
@Column({ name: 'link_enabled', type: 'boolean', default: true })
|
||||
linkEnabled: boolean
|
||||
}
|
||||
48
database/entity/0039-contributions_table/Contribution.ts
Normal file
48
database/entity/0039-contributions_table/Contribution.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, DeleteDateColumn } from 'typeorm'
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
|
||||
@Entity('contributions')
|
||||
export class Contribution extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ unsigned: true, nullable: false, name: 'user_id' })
|
||||
userId: number
|
||||
|
||||
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
|
||||
createdAt: Date
|
||||
|
||||
@Column({ type: 'datetime', nullable: false, name: 'contribution_date' })
|
||||
contributionDate: Date
|
||||
|
||||
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
memo: string
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
amount: Decimal
|
||||
|
||||
@Column({ unsigned: true, nullable: true, name: 'moderator_id' })
|
||||
moderatorId: number
|
||||
|
||||
@Column({ unsigned: true, nullable: true, name: 'contribution_link_id' })
|
||||
contributionLinkId: number
|
||||
|
||||
@Column({ unsigned: true, nullable: true, name: 'confirmed_by' })
|
||||
confirmedBy: number
|
||||
|
||||
@Column({ nullable: true, name: 'confirmed_at' })
|
||||
confirmedAt: Date
|
||||
|
||||
@Column({ unsigned: true, nullable: true, name: 'transaction_id' })
|
||||
transactionId: number
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at' })
|
||||
deletedAt: Date | null
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm'
|
||||
|
||||
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
|
||||
pubKey: Buffer
|
||||
|
||||
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
|
||||
privKey: Buffer
|
||||
|
||||
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
email: string
|
||||
|
||||
@Column({
|
||||
name: 'first_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
firstName: string
|
||||
|
||||
@Column({
|
||||
name: 'last_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
lastName: string
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt: Date | null
|
||||
|
||||
@Column({ type: 'bigint', default: 0, unsigned: true })
|
||||
password: BigInt
|
||||
|
||||
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
|
||||
emailHash: Buffer
|
||||
|
||||
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
|
||||
createdAt: Date
|
||||
|
||||
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
|
||||
emailChecked: boolean
|
||||
|
||||
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
|
||||
language: string
|
||||
|
||||
@Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
|
||||
isAdmin: Date | null
|
||||
|
||||
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||
referrerId?: number | null
|
||||
|
||||
@Column({
|
||||
name: 'contribution_link_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
contributionLinkId?: number | null
|
||||
|
||||
@Column({ name: 'publisher_id', default: 0 })
|
||||
publisherId: number
|
||||
|
||||
@Column({
|
||||
type: 'text',
|
||||
name: 'passphrase',
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
passphrase: string
|
||||
}
|
||||
1
database/entity/Contribution.ts
Normal file
1
database/entity/Contribution.ts
Normal file
@ -0,0 +1 @@
|
||||
export { Contribution } from './0039-contributions_table/Contribution'
|
||||
1
database/entity/ContributionLink.ts
Normal file
1
database/entity/ContributionLink.ts
Normal file
@ -0,0 +1 @@
|
||||
export { ContributionLink } from './0038-add_contribution_links_table/ContributionLink'
|
||||
@ -1 +1 @@
|
||||
export { User } from './0037-drop_server_user_table/User'
|
||||
export { User } from './0040-add_contribution_link_id_to_user/User'
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { ContributionLink } from './ContributionLink'
|
||||
import { LoginElopageBuys } from './LoginElopageBuys'
|
||||
import { LoginEmailOptIn } from './LoginEmailOptIn'
|
||||
import { Migration } from './Migration'
|
||||
import { Transaction } from './Transaction'
|
||||
import { TransactionLink } from './TransactionLink'
|
||||
import { User } from './User'
|
||||
import { AdminPendingCreation } from './AdminPendingCreation'
|
||||
import { Contribution } from './Contribution'
|
||||
|
||||
export const entities = [
|
||||
AdminPendingCreation,
|
||||
Contribution,
|
||||
ContributionLink,
|
||||
LoginElopageBuys,
|
||||
LoginEmailOptIn,
|
||||
Migration,
|
||||
|
||||
35
database/migrations/0038-add_contribution_links_table.ts
Normal file
35
database/migrations/0038-add_contribution_links_table.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/* MIGRATION TO ADD CONTRIBUTION_LINKS
|
||||
*
|
||||
* This migration adds the table `contribution_links` in order to store all sorts of contribution_links data
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`
|
||||
CREATE TABLE IF NOT EXISTS \`contribution_links\` (
|
||||
\`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
\`name\` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
\`memo\` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
\`valid_from\` datetime NOT NULL,
|
||||
\`valid_to\` datetime NULL,
|
||||
\`amount\` bigint(20) NOT NULL,
|
||||
\`cycle\` varchar(12) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'ONCE',
|
||||
\`max_per_cycle\` int(10) unsigned NOT NULL DEFAULT '1',
|
||||
\`max_amount_per_month\` bigint(20) NULL DEFAULT NULL,
|
||||
\`total_max_count_of_contribution\` int(10) unsigned NULL DEFAULT NULL,
|
||||
\`max_account_balance\` bigint(20) NULL DEFAULT NULL,
|
||||
\`min_gap_hours\` int(10) unsigned NULL DEFAULT NULL,
|
||||
\`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
\`deleted_at\` datetime NULL DEFAULT NULL,
|
||||
\`code\` varchar(24) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
\`link_enabled\` tinyint(4) NOT NULL DEFAULT '1',
|
||||
PRIMARY KEY (\`id\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
// write downgrade logic as parameter of queryFn
|
||||
await queryFn(`DROP TABLE IF EXISTS \`contribution_links\`;`)
|
||||
}
|
||||
59
database/migrations/0039-contributions_table.ts
Normal file
59
database/migrations/0039-contributions_table.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/* MIGRATION to rename ADMIN_PENDING_CREATION table and add columns
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn('RENAME TABLE `admin_pending_creations` TO `contributions`;')
|
||||
|
||||
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `userId` `user_id` int(10);')
|
||||
|
||||
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `created` `created_at` datetime;')
|
||||
|
||||
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `date` `contribution_date` datetime;')
|
||||
|
||||
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `moderator` `moderator_id` int(10);')
|
||||
|
||||
await queryFn(
|
||||
'ALTER TABLE `contributions` ADD COLUMN `contribution_link_id` int(10) unsigned DEFAULT NULL AFTER `moderator_id`;',
|
||||
)
|
||||
|
||||
await queryFn(
|
||||
'ALTER TABLE `contributions` ADD COLUMN `confirmed_by` int(10) unsigned DEFAULT NULL AFTER `contribution_link_id`;',
|
||||
)
|
||||
|
||||
await queryFn(
|
||||
'ALTER TABLE `contributions` ADD COLUMN `confirmed_at` datetime DEFAULT NULL AFTER `confirmed_by`;',
|
||||
)
|
||||
|
||||
await queryFn(
|
||||
'ALTER TABLE `contributions` ADD COLUMN `transaction_id` int(10) unsigned DEFAULT NULL AFTER `confirmed_at`;',
|
||||
)
|
||||
|
||||
await queryFn(
|
||||
'ALTER TABLE `contributions` ADD COLUMN `deleted_at` datetime DEFAULT NULL AFTER `confirmed_at`;',
|
||||
)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn('ALTER TABLE `contributions` DROP COLUMN IF EXISTS `deleted_at`;')
|
||||
|
||||
await queryFn('ALTER TABLE `contributions` DROP COLUMN IF EXISTS `transaction_id`;')
|
||||
|
||||
await queryFn('ALTER TABLE `contributions` DROP COLUMN IF EXISTS `confirmed_at`;')
|
||||
|
||||
await queryFn('ALTER TABLE `contributions` DROP COLUMN IF EXISTS `confirmed_by`;')
|
||||
|
||||
await queryFn('ALTER TABLE `contributions` DROP COLUMN IF EXISTS `contribution_link_id`;')
|
||||
|
||||
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `moderator_id` `moderator` int(10);')
|
||||
|
||||
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `created_at` `created` datetime;')
|
||||
|
||||
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `contribution_date` `date` datetime;')
|
||||
|
||||
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `user_id` `userId` int(10);')
|
||||
|
||||
await queryFn('RENAME TABLE `contributions` TO `admin_pending_creations`;')
|
||||
}
|
||||
14
database/migrations/0040-add_contribution_link_id_to_user.ts
Normal file
14
database/migrations/0040-add_contribution_link_id_to_user.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/* MIGRATION TO ADD contribution_link_id FIELD TO users */
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(
|
||||
'ALTER TABLE `users` ADD COLUMN `contribution_link_id` int UNSIGNED DEFAULT NULL AFTER `referrer_id`;',
|
||||
)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn('ALTER TABLE `users` DROP COLUMN `contribution_link_id`;')
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-database",
|
||||
"version": "1.9.0",
|
||||
"version": "1.10.0",
|
||||
"description": "Gradido Database Tool to execute database migrations",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/database",
|
||||
|
||||
@ -22,10 +22,11 @@ COMMUNITY_NAME="Gradido Development Stage1"
|
||||
COMMUNITY_URL=https://stage1.gradido.net/
|
||||
COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register
|
||||
COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code}
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
|
||||
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
|
||||
|
||||
# backend
|
||||
BACKEND_CONFIG_VERSION=v6.2022-04-21
|
||||
BACKEND_CONFIG_VERSION=v8.2022-06-20
|
||||
|
||||
JWT_EXPIRES_IN=30m
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
|
||||
173
docu/Concepts/BusinessRequirements/UC_Send_Contribution.md
Normal file
173
docu/Concepts/BusinessRequirements/UC_Send_Contribution.md
Normal file
@ -0,0 +1,173 @@
|
||||
# GDD-Creation per Link/QR-Code
|
||||
|
||||
Die Idee besteht darin, dass ein Administrator eine Contribution mit all seinen Attributen und Regeln im System erfasst. Dabei kann er unter anderem festlegen, ob für diese ein Link oder ein QR-Code generiert und über andere Medien wie Email oder Messenger versendet werden kann. Der Empfänger kann diesen Link bzw QR-Code dann über die Gradido-Anwendung einlösen und bekommt dann den Betrag der Contribution als Schöpfung auf seinem Konto gutgeschrieben.
|
||||
|
||||
## Logischer Ablauf
|
||||
|
||||
Der logische Ablauf für das Szenario "Activity-Confirmation and booking of Creations " wird in der nachfolgenden Grafik dargestellt. Dabei wird links das Szenario der "interactive Confirmation and booking of Creations" und rechts "automatic Confirmation and booking of Creations" dargestellt. Ziel dieser Grafik ist neben der logischen Ablaufsübersicht auch die Gemeinsamkeiten und Unterschiede der beiden Szenarien herauszuarbeiten.
|
||||
|
||||

|
||||
|
||||
Das Szenario der *interaktiven Aktivitäten-Bestätigung* ist derzeit noch in den zwei Systemen EloPage und Gradido enthalten - markiert als IST-Prozess - und wird zukünftig dann nur noch innerhalb Gradido ablaufen - markiert als SOLL-Prozess. Mit der Ablösung von EloPage und der vollständigen Migration nach Gradido erfolgt gleichzeitig eine Migration der Datenbank-Tabelle "admin_pending-creations" nach "Contributions". Unterhalb der gestrichelten Linie sind die beiden Szenarien dann in der Ablauflogik vollständig gleich.
|
||||
|
||||
## Dialoge
|
||||
|
||||
Für die Erfassung, Suche und Anzeige der Contributions und deren Gliederung in Kategorien wird es dazu im Admin-Bereich zusätzliche Funktionen und Dialoge geben.
|
||||
|
||||
### Übersicht - Dialog
|
||||
|
||||
In der Admin-Übersicht wird es zusätzliche Navigations- bzw. Menüpunkte geben, über die der Admin die gewünschte Funktionalität und die zugehörigen Dialoge öffnen kann.
|
||||
|
||||

|
||||
|
||||
### Contribution erfassen - Dialog
|
||||
|
||||
Bei der Erfassung einer Contribution wird die Kategorie, ein Name, eine Beschreibung der Contribution und der Betrag eingegeben.
|
||||
|
||||
Der Gültigkeitsstart wird als Default mit dem aktuellen Erfassungszeitpunkt vorbelegt, wobei das Gültigkeitsende leer bleibt und damit als endlos gültig definiert wird. Mit Eingabe eines Start- und/oder Endezeitpunktes kann aber ein konkreter Gültigkeitszeitraum erfasst werden.
|
||||
|
||||
Wie häufig ein User für diese Contribution eine Schöpfung gutgeschrieben bekommen kann, wird über die Auswahl eines Zyklus - stündlich, 2-stündlich, 4-stündlich, etc. - und innerhalb dieses Zyklus eine Anzahl an Wiederholungen definiert. Voreinstellung sind 1x täglich.
|
||||
|
||||

|
||||
|
||||
Ob die Contribution über einen versendeten Link bzw. QR-Code geschöpft werden kann, wird mittels der Auswahl "Versenden möglich als" bestimmt.
|
||||
|
||||

|
||||
|
||||
Für die Schöpfung der Contribution können weitere Regeln definiert werden:
|
||||
|
||||
* Gesamt - max. Anzahl Schöpfungen: bestimmt die maximale Anzahl der möglichen Schöpfungen über alle User dieser Community. Sobald diese Anzahl an Schöpfungen erreicht ist, werden alle weiteren eingehenden Schöpfungsanfragen für diese Contribution -egal ob per Links, per QR-Code oder User-Online-Erfassung mit einer entsprechend aussagekräftigen Fehlermeldung abgelehnt.
|
||||
* pro User
|
||||
* max schöpfbarer Betrag pro Monat: mit diesem definierbaren Betrag kann vordefiniert werden, wieviel Gradido ein User innerhalb eines Abrechnungsmonats maximal durch diese Contribution schöpfen kann. Ist diese Summer erreicht werden weiter eingehende Schöpfungsanfragen - egal ob per Link, per QR-Code oder online - mit einer entsprechend aussagekräftigen Fehlermeldung abgelehnt.
|
||||
* max. Kontostand vor Schöpfung: mit diesem definierbaren Betrag kann festgelegt werden, dass bevor für diese Contribution eine Schöpfung für den user erfolgt, eine Prüfung auf den aktuellen Kontostand erfolgt. Sobald der Kontostand höher als der vorgegebene Betrag ist, wird die eingehende Schöpfungsanfrage, ob per Link, per QR-Code oder online, mit einer entsprechend aussagekräftigen Fehlermeldung abgelehnt.
|
||||
* min. Abstand zw. erneuter Schöpfung: es kann ein zeitlicher Abstand in Stunden definiert werden, der angibt wieviel Stunden seit der letzten erfolgten Schöpfung vergehen müssen, bevor eine erneute Schöpfungsanfrage, ob per Link, per QR-Code oder online angenommen und durchgeführt werden darf. Ist bei einer erneuten Schöpfungsanfrage der zeitliche Abstand noch nicht erreicht, dann wird mit einer entsprechend aussagekräftigen Fehlermeldung abgebrochen.
|
||||
|
||||

|
||||
|
||||
### Ausbaustufe-1:
|
||||
|
||||
Die Ausbaustufe-1 wird gezielt auf die Anforderungen der "Dokumenta" im Juni 2022 abgestimmt.
|
||||
|
||||
#### Contribution-Erfassungsdialog (Adminbereich)
|
||||
|
||||
Es werden folgende Anforderungen an den Erfassungsdialog einer Contribution gestellt:
|
||||
|
||||
| Attribut | Beschreibung |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Name | Name der Contribution als Bezeichnung, nach dem später auch eine Suche erfolgen kann |
|
||||
| Beschreibung | Beschreibung der Contribution, die mit der Schöpfung als Memo in die Transaktion übernommen wird |
|
||||
| Betrag | der Betrag, der mit Einlösen der Contribution geschöpft wird |
|
||||
| GültigVon | - das Datum, ab wann die Contribution gültig und damit einlösbar ist<br />- es wird die Uhrzeit 00:00:00 angenommen |
|
||||
| GültigBis | - das Datum, wie lange die Contribution gültig und damit einlösbar ist<br />- es wird die Uhrzeit 23:59:59 angenommen |
|
||||
| Zyklus | - Angabe wie häufig eine Contribution gutgeschrieben werden kann<br />- als Auswahlliste (Combobox) geplant, aber für diese Ausbaustufe nur mit dem Wert "kein Wiederholungszyklus" vorbelegt |
|
||||
| Wiederholungen | - Anzahl an Wiederholungen pro Zyklus<br />- für diese Ausbaustufe wird der Wert "1" vorbelegt -> somit gilt 1 x pro User |
|
||||
| VersendenMöglich | - hier wird "als Link / QR-Code" voreingestellt |
|
||||
| alle weiteren Attribute | - entfallen für diese Ausbaustufe<br />- die GUI-Komponenten können optional schon im Dialog eingebaut und angezeigt werden<br />- diese GUI-Komponenten müssen wenn sichtbar disabled sein und dürfen damit keine Eingaben entgegen nehmen |
|
||||
|
||||
|
||||
#### Ablauflogik
|
||||
|
||||
Für die Ausbaustufe-1 wird gemäß der Beschreibung aus dem Kapitel "Logischer Ablauf" nur die "automatic Confirmation and booking of Creations" umgesetzt. Die interaktive Variante - sprich Ablösung des EloPage Prozesses - mit "interactive Confirmation and booking of Creations" bleibt für eine spätere Ausbaustufe aussen vor.
|
||||
|
||||
Das Regelwerk in der Businesslogik wird gemäß der reduzierten Contribution-Attribute aus dem Erfassungsdialog, den vordefinierten Initialwerten und der daraus resultierenden Variantenvielfalt vereinfacht.
|
||||
|
||||
|
||||
#### Kriterien "Dokumenta"
|
||||
|
||||
* Es soll eine "Dokumenta"-Contribution im Admin-Bereich erfassbar sein und in der Datenbank als ContributionLink gespeichert werden.
|
||||
* Es wird für die Gesamtlaufzeit der "Dokumenta" genau ein Contribution benötigt
|
||||
* Die "Dokumenta"-Contribution kann von einem User maximal 1x aktiviert werden
|
||||
* Ein User kann mit diesem Link nur die Menge an GDDs schöpfen, die in der Contribution als "Betrag" festgelegt ist
|
||||
* Die "Dokumenta"-Contribution kann als Link / QR-Code erzeugt, angezeigt und in die Zwischenablage kopiert werden
|
||||
* Jeder beliebige User kann den Link / QR-Code aktivieren
|
||||
* der Link führt auf eine Gradido-Seite, wo der User sich anmelden oder registrieren kann
|
||||
* mit erfolgreichem Login bzw. Registrierung wird der automatische Bestätigungs- und Schöpfungsprozess getriggert
|
||||
* es erfolgt eine Überprüfung der definierten Contribution-Regeln für den angemeldeten User:
|
||||
* Gültigkeit: liegt die Aktivierung im Gültigkeitszeitraum der Contribution
|
||||
* Zyklus und WIederholungen: bei einem Zyklus-Wert = "kein Zyklus" und einem Wiederholungswert = 1 darf der User den Betrag dieser Contribution nur einmal insgesamt schöpfen
|
||||
* max. schöpfbarer Gradido-Betrag pro Monat: wenn der Betrag der Contribution plus der Betrag, den der User in diesem Monat schon geschöpft hat den maximal schöpfbaren Betrag pro Monat von 1000 GDD übersteigt, dann wird die Schöpfung dieser Contribution abgelehnt
|
||||
* mit erfolgreich durchlaufenen Regelprüfungen wird ein "besätigter" aber "noch nicht gebuchten" Eintrag in der "Contributions"-Tabelle erzeugt
|
||||
* ein "bestätigter" aber "noch nicht gebuchter" "Contributions"-Eintrag stößt eine Schöpfungstransaktion für den User an
|
||||
* es erfolgt eine übliche Schöpfungstransaktion nach der Bestätigung der Contribution
|
||||
* die Schöpfungstransaktion schreibt den Betrag der Contribution dem Kontostand des Users gut
|
||||
|
||||
|
||||
## Datenbank-Modell
|
||||
|
||||
### Ausgangsmodell
|
||||
|
||||
Das nachfolgende Bild zeigt das Datenmodell vor der Einführung und Migration auf Contributions.
|
||||
|
||||

|
||||
|
||||
### Datenbank-Änderungen
|
||||
|
||||
Die Datenbank wird in ihrer vollständigen Ausprägung trotz Ausbaustufe-1 wie folgt beschrieben umgesetzt.
|
||||
|
||||
#### neue Tabellen
|
||||
|
||||
##### contribution_links - Tabelle
|
||||
|
||||
| Name | Typ | Nullable | Default | Kommentar |
|
||||
| ------------------------------- | ------------ | :------: | :------------: | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| id | INT UNSIGNED | NOT NULL | auto increment | PrimaryKey |
|
||||
| name | varchar(100) | NOT NULL | | unique Name |
|
||||
| description | varchar(255) | | | |
|
||||
| valid_from | DATETIME | NOT NULL | NOW | |
|
||||
| valid_to | DATETIME | | NULL | |
|
||||
| amount | DECIMAL | NOT NULL | | |
|
||||
| cycle | ENUM | NOT NULL | ONCE | ONCE, HOUR, 2HOUR, 4HOUR, 8HOUR, HALFDAY, DAY, 2DAYS, 3DAYS, 4DAYS, 5DAYS, 6DAYS, WEEK, 2WEEKS, MONTH, 2MONTH, QUARTER, HALFYEAR, YEAR |
|
||||
| max_per_cycle | INT UNSIGNED | NOT NULL | 1 | |
|
||||
| max_amount_per_month | DECIMAL | | NULL | |
|
||||
| total_max_count_of_contribution | INT UNSIGNED | | NULL | |
|
||||
| max_account_balance | DECIMAL | | NULL | |
|
||||
| min_gap_hours | INT UNSIGNED | | NULL | |
|
||||
| created_at | DATETIME | | NOW | |
|
||||
| deleted_at | DATETIMEBOOL | | NULL | |
|
||||
| code | varchar(24) | | NULL | |
|
||||
| link_enabled | BOOL | | NULL | |
|
||||
|
||||
##### contributions -Tabelle
|
||||
|
||||
| Name | Typ | Nullable | Default | Kommentar |
|
||||
| --------------------- | ------------ | -------- | -------------- | -------------------------------------------------------------------------------- |
|
||||
| id | INT UNSIGNED | NOT NULL | auto increment | PrimaryKey |
|
||||
| name | varchar(100) | NOT NULL | | short Naming of activity |
|
||||
| memo | varchar(255) | NOT NULL | | full and detailed description of activities |
|
||||
| amount | DECIMAL | NOT NULL | | the amount of GDD for this activity |
|
||||
| contribution_date | DATETIME | | NULL | the date/month, when the contribution was realized by the user |
|
||||
| user_id | INT UNSIGNED | NOT NULL | | the user, who wants to get GDD for his activity |
|
||||
| created_at | DATETIME | NOT NULL | NOW | the date, when this entry was captured and stored in database |
|
||||
| contribution_links_id | INT UNSIGNED | | NULL | contribution, on which this activity base on |
|
||||
| moderator_id | INT UNSIGNED | | NULL | userID of Moderator/Admin, who captured the contribution |
|
||||
| confirmed_by | INT UNSIGNED | | NULL | userID of Moderator/Admin, who confirms the contribution |
|
||||
| confirmed_at | DATETIME | | NULL | date, when moderator has confirmed the contribution |
|
||||
| booked_at | DATETIME | | NULL | date, when the system has booked the amount of the activity on the users account |
|
||||
| deleted_at | DATETIME | | NULL | soft delete |
|
||||
|
||||
#### zu migrierende Tabellen
|
||||
|
||||
##### Tabelle admin_pending_creations
|
||||
|
||||
Diese Tabelle wird im Rahmen dieses UseCase migriert in die neue Tabelle contributions...
|
||||
|
||||
| Quell-Spalte | Migration | Ziel-Spalte | Beschreibung |
|
||||
| ------------ | --------- | --------------------- | ---------------------------------------------------------- |
|
||||
| id | keine | id | auto inkrement des PK |
|
||||
| user_id | copy | user_id | |
|
||||
| created | copy | created_at | |
|
||||
| date | copy | activity_date | |
|
||||
| memo | copy | memo | |
|
||||
| amount | copy | amount | |
|
||||
| moderator | copy | moderator_id | |
|
||||
| | | name | neu mit ContributionsLinks |
|
||||
| | | contribution_links_id | neu mit ContributionsLinks |
|
||||
| | | confirmed_at | neu mit Erfassung der Contributions von Elopage in Gradido |
|
||||
| | | confirmed_by | neu mit Erfassung der Contributions von Elopage in Gradido |
|
||||
| | | booked_at | neu mit Erfassung der Contributions von Elopage in Gradido |
|
||||
|
||||
...und kann nach Übernahme der Daten in die neue Tabelle gelöscht werden oder es erfolgen die Änderungen sofort auf der Ursprungstabelle.
|
||||
|
||||
### Zielmodell
|
||||
|
||||

|
||||
@ -0,0 +1,368 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="-Bvenr9G4hMm7q4_ZwMA" name="Seite-1">
|
||||
<mxGraphModel dx="3755" dy="1067" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="2336" pageHeight="1654" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="6" value="EloPage" style="rounded=0;whiteSpace=wrap;html=1;fontSize=24;fillColor=#fff2cc;strokeColor=#d6b656;verticalAlign=top;align=center;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="80" width="1080" height="120" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="2" value="interactive Confirmation and booking of Creations" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=28;fontStyle=1" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="10" width="1080" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="automatic Confirmation and booking of Creations" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=28;fontStyle=1" parent="1" vertex="1">
|
||||
<mxGeometry x="1200" y="10" width="1080" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="" style="endArrow=none;html=1;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1160" y="1650" as="sourcePoint"/>
|
||||
<mxPoint x="1160" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="45" value="" style="edgeStyle=none;html=1;fontSize=14;" parent="1" source="5" target="44" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="User erfasst Activity" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="80" y="140" width="240" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="Gradido" style="rounded=0;whiteSpace=wrap;html=1;fontSize=24;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;align=center;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="210" width="1080" height="150" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="21" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=24;" parent="1" source="8" target="9" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="Moderator überträgt&nbsp; offene User-Activity aus EloPage" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="80" y="236" width="240" height="110" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="9" value="admin_pending_creations" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="261" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="29" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="10" target="9" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="users" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="880" y="261" width="170" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="12" value="" style="endArrow=none;dashed=1;html=1;fontSize=24;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint y="440" as="sourcePoint"/>
|
||||
<mxPoint x="1160" y="440" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="13" value="IST-Prozess" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;" parent="1" vertex="1">
|
||||
<mxGeometry y="400" width="210" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="14" value="SOLL-Prozess" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;" parent="1" vertex="1">
|
||||
<mxGeometry y="450" width="210" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="Gradido" style="rounded=0;whiteSpace=wrap;html=1;fontSize=24;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;align=center;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="520" width="1080" height="1080" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="18" value="contributions" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="690" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="30" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="19" target="18" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="19" value="users" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="880" y="690" width="170" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="20" target="18" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="20" value="User erfasst seine Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="690" width="240" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="36" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="23" target="28" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="23" value="Moderator sucht unbestätigte <br>Contributions" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="790" width="240" height="110" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="27" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="25" target="23" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="contributions<br style="font-size: 24px"><font style="font-size: 20px">confirmed_at == NULL</font>" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="795" width="380" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="32" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="28" target="31" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="43" value="" style="edgeStyle=none;html=1;fontSize=14;" parent="1" source="28" target="34" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="28" value="Moderator bestätigt Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="920" width="240" height="110" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="31" value="<font style="font-size: 23px"><span style="font-size: 24px">contributions</span><br></font><div style="text-align: left ; font-size: 20px"><font style="font-size: 20px">confirmed_at = NOW</font></div><span style="line-height: 0.8 ; font-size: 20px"><div style="text-align: left"><font style="font-size: 20px">confirmed_by = Moderator's userID</font></div></span>" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="930" width="380" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="49" value="" style="edgeStyle=none;html=1;startArrow=none;" parent="1" source="50" target="48" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="53" value="Ja" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;" parent="49" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.3333" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="51" value="" style="edgeStyle=none;html=1;" parent="1" source="34" target="50" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="34" value="&nbsp;lese Transaktionen des Users zu bestätigter<br>Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="1090" width="240" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="37" value="" style="edgeStyle=none;html=1;fontSize=12;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="35" target="34" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="35" value="contributions" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="1080" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="38" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="39" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="780" y="1110" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="41" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=12;" parent="1" source="39" target="40" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="39" value="users" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="880" y="1110" width="170" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="42" style="edgeStyle=none;html=1;fontSize=12;" parent="1" source="40" target="34" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="40" value="transactions" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="1150" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="46" style="edgeStyle=none;html=1;entryX=0.75;entryY=0;entryDx=0;entryDy=0;fontSize=14;dashed=1;startArrow=classic;startFill=1;exitX=0;exitY=1;exitDx=0;exitDy=0;" parent="1" source="44" target="8" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="44" value="Aktivitäten-Liste als Chat" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;fillColor=#f5f5f5;strokeColor=#666666;gradientColor=#b3b3b3;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="130" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="47" value="" style="endArrow=none;dashed=1;html=1;fontSize=24;strokeWidth=3;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint y="1040" as="sourcePoint"/>
|
||||
<mxPoint x="1160" y="1040" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="57" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" parent="1" source="48" target="55" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="60" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="48" target="59" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="48" value="erzeuge Schöpfungstransaktion<br>aus Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="1330" width="240" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="50" value="<br>Schöpfungsregeln<br>&nbsp;erfüllt?" style="rhombus;whiteSpace=wrap;html=1;fontSize=16;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="110" y="1210" width="200" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="52" value="" style="edgeStyle=none;html=1;endArrow=none;" parent="1" source="34" target="50" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="210" y="1230" as="sourcePoint"/>
|
||||
<mxPoint x="210" y="1410" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="55" value="transactions" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="1340" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="58" value="contributions<br style="font-size: 24px"><span style="text-align: left"><font style="font-size: 20px">booked_at = NOW</font></span>" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="1460" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="61" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="59" target="58" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="59" value="aktualisiere <br>&nbsp;gebuchte Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="1450" width="240" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="62" value="Gradido" style="rounded=0;whiteSpace=wrap;html=1;fontSize=24;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;align=center;" parent="1" vertex="1">
|
||||
<mxGeometry x="1200" y="80" width="980" height="1520" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="117" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="63" target="67" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="63" value="contribution_links<br style="font-size: 24px"><span style="font-size: 20px">id = X<br>code = X-link<br></span>" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="670" width="380" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="121" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="65" target="71" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="65" value="users<br style="font-size: 24px"><font style="font-size: 20px">ID=Y</font>" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1990" y="930" width="170" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="128" value="" style="edgeStyle=none;html=1;fontSize=20;strokeWidth=1;" parent="1" source="67" target="127" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="67" value="lese Contribution zu aktiviertem Link" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="690" width="240" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="120" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="69" target="71" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="122" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="69" target="79" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="69" value="erzeuge aus ContributionLink zu angemeldetem User eine bestätigte Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="907.5" width="240" height="110" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="71" value="contributions<br style="font-size: 24px"><font style="font-size: 20px">confirmed_at = NOW, contribution_links_id=X, user_id=Y</font>" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="917.5" width="380" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="72" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1490" y="855" as="sourcePoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="76" value="" style="edgeStyle=none;html=1;startArrow=none;" parent="1" source="91" target="90" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="77" value="Ja" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;" parent="76" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.3333" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="78" value="" style="edgeStyle=none;html=1;" parent="1" source="79" target="91" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="79" value="&nbsp;lese Transaktionen des Users zu bestätigter<br>Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="1090" width="240" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="80" value="" style="edgeStyle=none;html=1;fontSize=12;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="81" target="79" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="81" value="contributions" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="1080" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="82" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="84" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1940" y="1110" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="83" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=12;" parent="1" source="84" target="86" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="84" value="users" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1990" y="1110" width="170" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="85" style="edgeStyle=none;html=1;fontSize=12;" parent="1" source="86" target="79" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="86" value="transactions" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="1150" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="87" value="" style="endArrow=none;dashed=1;html=1;fontSize=24;strokeWidth=3;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1160" y="1040" as="sourcePoint"/>
|
||||
<mxPoint x="2320" y="1040" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="88" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" parent="1" source="90" target="93" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="89" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="90" target="96" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="90" value="erzeuge Schöpfungstransaktion<br>aus Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="1330" width="240" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="91" value="<br>Schöpfungsregeln<br>&nbsp;erfüllt?" style="rhombus;whiteSpace=wrap;html=1;fontSize=16;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="1270" y="1210" width="200" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="92" value="" style="edgeStyle=none;html=1;endArrow=none;" parent="1" source="79" target="91" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1370" y="1230" as="sourcePoint"/>
|
||||
<mxPoint x="1370" y="1410" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="93" value="transactions" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="1340" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="94" value="contributions<br style="font-size: 24px"><span style="text-align: left"><font style="font-size: 20px">booked_at = NOW</font></span>" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="1460" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="95" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="96" target="94" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="96" value="aktualisiere <br>gebuchte Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="1450" width="240" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="99" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="97" target="98" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="97" value="Moderator erfasst Contribution für <br>"automatic Confirmation"" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="117.5" width="240" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="98" value="contribution_links" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="127.5" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="101" value="Moderator erzeugt Link/QR-Code aus Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="213.5" width="240" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="103" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="102" target="101" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="102" value="contribution_links" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="223.5" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="110" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="104" target="109" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="104" value="Moderator <br>verbreitet / versendet<br>&nbsp;erzeugten Link/QR-Code" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="312.5" width="240" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="112" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" parent="1" source="109" target="111" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1912.7716129809019" y="520.971840668889" as="sourcePoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="109" value="veröffentlichter <br>Link / QR-Code für<br>eine Contribution" style="ellipse;whiteSpace=wrap;html=1;fontSize=20;rounded=1;fillColor=#d0cee2;strokeColor=#56517e;" parent="1" vertex="1">
|
||||
<mxGeometry x="2010" y="410" width="310" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="118" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" target="113" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1370" y="570" as="sourcePoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="111" value="User aktiviert <br>Link / QR-Code" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="510" width="240" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="115" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" parent="1" source="113" target="114" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="116" style="edgeStyle=none;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontSize=16;" parent="1" source="113" target="67" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="113" value="User führt <br>Login / Register aus" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="597.5" width="240" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="114" value="users" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1990" y="592.5" width="170" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="123" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" parent="1" source="124" target="125" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="126" value="" style="edgeStyle=none;html=1;fontSize=20;strokeWidth=1;" parent="1" source="124" target="20" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="124" value="User führt <br>Login / Register aus" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="597.5" width="240" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="125" value="users" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="880" y="592.5" width="170" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="129" value="" style="edgeStyle=none;html=1;fontSize=20;strokeWidth=1;" parent="1" source="127" target="69" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="130" value="Ja" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=20;" parent="129" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.3467" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="127" value="Contribution <br>und Regel <br>valide?" style="rhombus;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="770" width="240" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user