mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge remote-tracking branch 'origin/master' into 2274-feature-concept-manuel-user-registration-for-admins
This commit is contained in:
commit
dba98e316d
181
.github/workflows/test.yml
vendored
181
.github/workflows/test.yml
vendored
@ -1,6 +1,6 @@
|
||||
name: gradido test CI
|
||||
|
||||
on: [push]
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
##############################################################################
|
||||
@ -15,7 +15,7 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# FRONTEND ###############################################################
|
||||
##########################################################################
|
||||
@ -24,7 +24,7 @@ jobs:
|
||||
docker build --target test -t "gradido/frontend:test" frontend/
|
||||
docker save "gradido/frontend:test" > /tmp/frontend.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docker-frontend-test
|
||||
path: /tmp/frontend.tar
|
||||
@ -41,7 +41,7 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# ADMIN INTERFACE ########################################################
|
||||
##########################################################################
|
||||
@ -50,7 +50,7 @@ jobs:
|
||||
docker build --target test -t "gradido/admin:test" admin/ --build-arg NODE_ENV="test"
|
||||
docker save "gradido/admin:test" > /tmp/admin.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docker-admin-test
|
||||
path: /tmp/admin.tar
|
||||
@ -67,7 +67,7 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# BACKEND ################################################################
|
||||
##########################################################################
|
||||
@ -76,7 +76,7 @@ jobs:
|
||||
docker build -f ./backend/Dockerfile --target test -t "gradido/backend:test" .
|
||||
docker save "gradido/backend:test" > /tmp/backend.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docker-backend-test
|
||||
path: /tmp/backend.tar
|
||||
@ -93,7 +93,7 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# DATABASE UP ############################################################
|
||||
##########################################################################
|
||||
@ -102,7 +102,7 @@ jobs:
|
||||
docker build --target test_up -t "gradido/database:test_up" database/
|
||||
docker save "gradido/database:test_up" > /tmp/database_up.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docker-database-test_up
|
||||
path: /tmp/database_up.tar
|
||||
@ -119,7 +119,7 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# BUILD MARIADB DOCKER IMAGE #############################################
|
||||
##########################################################################
|
||||
@ -128,7 +128,7 @@ jobs:
|
||||
docker build --target mariadb_server -t "gradido/mariadb:test" -f ./mariadb/Dockerfile ./
|
||||
docker save "gradido/mariadb:test" > /tmp/mariadb.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docker-mariadb-test
|
||||
path: /tmp/mariadb.tar
|
||||
@ -145,7 +145,7 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# BUILD NGINX DOCKER IMAGE ###############################################
|
||||
##########################################################################
|
||||
@ -154,7 +154,7 @@ jobs:
|
||||
docker build -t "gradido/nginx:test" nginx/
|
||||
docker save "gradido/nginx:test" > /tmp/nginx.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docker-nginx-test
|
||||
path: /tmp/nginx.tar
|
||||
@ -171,12 +171,12 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# DOWNLOAD DOCKER IMAGE ##################################################
|
||||
##########################################################################
|
||||
- name: Download Docker Image (Frontend)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-frontend-test
|
||||
path: /tmp
|
||||
@ -200,12 +200,12 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# DOWNLOAD DOCKER IMAGE ##################################################
|
||||
##########################################################################
|
||||
- name: Download Docker Image (Frontend)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-frontend-test
|
||||
path: /tmp
|
||||
@ -229,12 +229,12 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# DOWNLOAD DOCKER IMAGE ##################################################
|
||||
##########################################################################
|
||||
- name: Download Docker Image (Frontend)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-frontend-test
|
||||
path: /tmp
|
||||
@ -258,12 +258,12 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# DOWNLOAD DOCKER IMAGE ##################################################
|
||||
##########################################################################
|
||||
- name: Download Docker Image (Admin Interface)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-admin-test
|
||||
path: /tmp
|
||||
@ -287,12 +287,12 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# DOWNLOAD DOCKER IMAGE ##################################################
|
||||
##########################################################################
|
||||
- name: Download Docker Image (Admin Interface)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-admin-test
|
||||
path: /tmp
|
||||
@ -308,7 +308,7 @@ jobs:
|
||||
# JOB: LOCALES ADMIN #########################################################
|
||||
##############################################################################
|
||||
locales_admin:
|
||||
name: Locales - Admin
|
||||
name: Locales - Admin Interface
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build_test_admin]
|
||||
steps:
|
||||
@ -316,12 +316,12 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# DOWNLOAD DOCKER IMAGE ##################################################
|
||||
##########################################################################
|
||||
- name: Download Docker Image (Admin Interface)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-admin-test
|
||||
path: /tmp
|
||||
@ -345,12 +345,12 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# DOWNLOAD DOCKER IMAGE ##################################################
|
||||
##########################################################################
|
||||
- name: Download Docker Image (Backend)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-backend-test
|
||||
path: /tmp
|
||||
@ -374,12 +374,12 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# DOWNLOAD DOCKER IMAGE ##################################################
|
||||
##########################################################################
|
||||
- name: Download Docker Image (Backend)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-database-test_up
|
||||
path: /tmp
|
||||
@ -403,12 +403,12 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# DOWNLOAD DOCKER IMAGES #################################################
|
||||
##########################################################################
|
||||
- name: Download Docker Image (Frontend)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-frontend-test
|
||||
path: /tmp
|
||||
@ -453,12 +453,12 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# DOWNLOAD DOCKER IMAGES #################################################
|
||||
##########################################################################
|
||||
- name: Download Docker Image (Admin Interface)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-admin-test
|
||||
path: /tmp
|
||||
@ -495,12 +495,12 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# DOWNLOAD DOCKER IMAGES #################################################
|
||||
##########################################################################
|
||||
- name: Download Docker Image (Mariadb)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-mariadb-test
|
||||
path: /tmp
|
||||
@ -543,7 +543,7 @@ jobs:
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# DOCKER COMPOSE DATABASE UP + RESET #####################################
|
||||
##########################################################################
|
||||
@ -553,3 +553,110 @@ jobs:
|
||||
run: docker-compose -f docker-compose.yml run -T database yarn up
|
||||
- name: database | reset
|
||||
run: docker-compose -f docker-compose.yml run -T database yarn reset
|
||||
|
||||
##############################################################################
|
||||
# JOB: END-TO-END TESTS #####################################################
|
||||
##############################################################################
|
||||
end-to-end-tests:
|
||||
name: End-to-End Tests
|
||||
runs-on: ubuntu-latest
|
||||
# needs: [build_test_mariadb, build_test_database_up, build_test_backend, build_test_admin, build_test_frontend, build_test_nginx]
|
||||
steps:
|
||||
##########################################################################
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
##########################################################################
|
||||
# DOWNLOAD DOCKER IMAGES #################################################
|
||||
##########################################################################
|
||||
# - name: Download Docker Image (Mariadb)
|
||||
# uses: actions/download-artifact@v3
|
||||
# with:
|
||||
# name: docker-mariadb-test
|
||||
# path: /tmp
|
||||
# - name: Load Docker Image (Mariadb)
|
||||
# run: docker load < /tmp/mariadb.tar
|
||||
# - name: Download Docker Image (Database Up)
|
||||
# uses: actions/download-artifact@v3
|
||||
# with:
|
||||
# name: docker-database-test_up
|
||||
# path: /tmp
|
||||
# - name: Load Docker Image (Database Up)
|
||||
# run: docker load < /tmp/database_up.tar
|
||||
# - name: Download Docker Image (Backend)
|
||||
# uses: actions/download-artifact@v3
|
||||
# with:
|
||||
# name: docker-backend-test
|
||||
# path: /tmp
|
||||
# - name: Load Docker Image (Backend)
|
||||
# run: docker load < /tmp/backend.tar
|
||||
# - name: Download Docker Image (Frontend)
|
||||
# uses: actions/download-artifact@v3
|
||||
# with:
|
||||
# name: docker-frontend-test
|
||||
# path: /tmp
|
||||
# - name: Load Docker Image (Frontend)
|
||||
# run: docker load < /tmp/frontend.tar
|
||||
# - name: Download Docker Image (Admin Interface)
|
||||
# uses: actions/download-artifact@v3
|
||||
# with:
|
||||
# name: docker-admin-test
|
||||
# path: /tmp
|
||||
# - name: Load Docker Image (Admin Interface)
|
||||
# run: docker load < /tmp/admin.tar
|
||||
# - name: Download Docker Image (Nginx)
|
||||
# uses: actions/download-artifact@v3
|
||||
# with:
|
||||
# name: docker-nginx-test
|
||||
# path: /tmp
|
||||
# - name: Load Docker Image (Nginx)
|
||||
# run: docker load < /tmp/nginx.tar
|
||||
##########################################################################
|
||||
# BOOT UP THE TEST SYSTEM ################################################
|
||||
##########################################################################
|
||||
- name: Boot up test system | docker-compose mariadb
|
||||
run: docker-compose up --detach mariadb
|
||||
|
||||
- name: Sleep for 30 seconds
|
||||
run: sleep 30s
|
||||
|
||||
- name: Boot up test system | docker-compose database
|
||||
run: docker-compose up --detach --no-deps database
|
||||
|
||||
- name: Boot up test system | docker-compose backend
|
||||
run: docker-compose up --detach --no-deps backend
|
||||
|
||||
- name: Sleep for 90 seconds
|
||||
run: sleep 90s
|
||||
|
||||
- name: Boot up test system | seed backend
|
||||
run: |
|
||||
sudo chown runner:docker -R *
|
||||
cd database
|
||||
yarn && yarn dev_reset
|
||||
cd ../backend
|
||||
yarn && yarn seed
|
||||
cd ..
|
||||
|
||||
- name: Boot up test system | docker-compose frontends
|
||||
run: docker-compose up --detach --no-deps frontend admin nginx
|
||||
|
||||
- name: Sleep for 2.5 minutes
|
||||
run: sleep 150s
|
||||
|
||||
##########################################################################
|
||||
# END-TO-END TESTS #######################################################
|
||||
##########################################################################
|
||||
- name: End-to-end tests | run tests
|
||||
id: e2e-tests
|
||||
run: |
|
||||
cd e2e-tests/cypress/tests/
|
||||
yarn
|
||||
yarn run cypress run --spec cypress/e2e/User.Authentication.feature
|
||||
- name: End-to-end tests | if tests failed, upload screenshots
|
||||
if: steps.e2e-tests.outcome == 'failure'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: cypress-screenshots
|
||||
path: /home/runner/work/gradido/gradido/e2e-tests/cypress/tests/cypress/screenshots/
|
||||
|
||||
29
CHANGELOG.md
29
CHANGELOG.md
@ -4,8 +4,37 @@ 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.13.3](https://github.com/gradido/gradido/compare/1.13.2...1.13.3)
|
||||
|
||||
- 2294 contribution links on its own page [`#2312`](https://github.com/gradido/gradido/pull/2312)
|
||||
- fix: Change Orange Color [`#2302`](https://github.com/gradido/gradido/pull/2302)
|
||||
- fix: Release Statistic Query Runner [`#2320`](https://github.com/gradido/gradido/pull/2320)
|
||||
- bug: 2295 remove horizontal scrollbar in admin overview [`#2311`](https://github.com/gradido/gradido/pull/2311)
|
||||
- 2292 community information contact [`#2313`](https://github.com/gradido/gradido/pull/2313)
|
||||
- bug: 2315 Contribution Month and TEST FAIL in MASTER [`#2316`](https://github.com/gradido/gradido/pull/2316)
|
||||
- 2291 add button for close contribution messages box [`#2314`](https://github.com/gradido/gradido/pull/2314)
|
||||
|
||||
#### [1.13.2](https://github.com/gradido/gradido/compare/1.13.1...1.13.2)
|
||||
|
||||
> 28 October 2022
|
||||
|
||||
- release: Version 1.13.2 [`#2307`](https://github.com/gradido/gradido/pull/2307)
|
||||
- fix: 🍰 Links In Contribution Messages Target Blank [`#2306`](https://github.com/gradido/gradido/pull/2306)
|
||||
- fix: Link in Contribution Messages [`#2305`](https://github.com/gradido/gradido/pull/2305)
|
||||
- Refactor: 🍰 Change the query so that we only look on the ``contributions`` table. [`#2217`](https://github.com/gradido/gradido/pull/2217)
|
||||
- Refactor: Admin Resolver Events and Logging [`#2244`](https://github.com/gradido/gradido/pull/2244)
|
||||
- contibution messages, links are recognised [`#2248`](https://github.com/gradido/gradido/pull/2248)
|
||||
- fix: Include Deleted Email Contacts in User Search [`#2281`](https://github.com/gradido/gradido/pull/2281)
|
||||
- fix: Pagination Contributions jumps to wrong Page [`#2284`](https://github.com/gradido/gradido/pull/2284)
|
||||
- fix: Changed some texts in E-Mails and Frontend [`#2276`](https://github.com/gradido/gradido/pull/2276)
|
||||
- Feat: 🍰 Add `deletedBy` To Contributions And Admin Can Not Delete Own User Contribution [`#2236`](https://github.com/gradido/gradido/pull/2236)
|
||||
- deleted contributions are displayed to the user [`#2277`](https://github.com/gradido/gradido/pull/2277)
|
||||
|
||||
#### [1.13.1](https://github.com/gradido/gradido/compare/1.13.0...1.13.1)
|
||||
|
||||
> 20 October 2022
|
||||
|
||||
- release: Version 1.13.1 [`#2279`](https://github.com/gradido/gradido/pull/2279)
|
||||
- Fix: correctly evaluate to EMAIL_TEST_MODE to false [`#2273`](https://github.com/gradido/gradido/pull/2273)
|
||||
- Refactor: Contribution resolver logs and events [`#2231`](https://github.com/gradido/gradido/pull/2231)
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administraion Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Moriz Wahl",
|
||||
"version": "1.13.1",
|
||||
"version": "1.13.3",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="content-footer">
|
||||
<hr />
|
||||
<b-row align-v="center" class="mt-4 justify-content-lg-between">
|
||||
<div align-v="center" class="mt-4 mb-4 justify-content-lg-between">
|
||||
<b-col>
|
||||
<div class="copyright text-center text-lg-center text-muted">
|
||||
{{ $t('footer.copyright.year', { year }) }}
|
||||
@ -25,7 +25,7 @@
|
||||
</a>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
@ -35,8 +35,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ContributionLinkForm from './ContributionLinkForm.vue'
|
||||
import ContributionLinkList from './ContributionLinkList.vue'
|
||||
import ContributionLinkForm from '../ContributionLink/ContributionLinkForm.vue'
|
||||
import ContributionLinkList from '../ContributionLink/ContributionLinkList.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContributionLink',
|
||||
@ -1,6 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ContributionLinkForm from './ContributionLinkForm.vue'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../../test/testSetup'
|
||||
import { createContributionLink } from '@/graphql/createContributionLink.js'
|
||||
|
||||
const localVue = global.localVue
|
||||
@ -1,6 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ContributionLinkList from './ContributionLinkList.vue'
|
||||
import { toastSuccessSpy, toastErrorSpy } from '../../test/testSetup'
|
||||
import { toastSuccessSpy, toastErrorSpy } from '../../../test/testSetup'
|
||||
// import { deleteContributionLink } from '../graphql/deleteContributionLink'
|
||||
|
||||
const localVue = global.localVue
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="contribution-link-list">
|
||||
<b-table striped hover :items="items" :fields="fields">
|
||||
<b-table :items="items" :fields="fields" striped hover stacked="lg">
|
||||
<template #cell(delete)="data">
|
||||
<b-button
|
||||
variant="danger"
|
||||
@ -46,7 +46,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import { deleteContributionLink } from '@/graphql/deleteContributionLink.js'
|
||||
import FigureQrCode from './FigureQrCode.vue'
|
||||
import FigureQrCode from '../FigureQrCode.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContributionLinkList',
|
||||
38
admin/src/components/ContributionMessages/LinkifyMessage.vue
Normal file
38
admin/src/components/ContributionMessages/LinkifyMessage.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="mt-2">
|
||||
<span v-for="({ type, text }, index) in linkifiedMessage" :key="index">
|
||||
<b-link v-if="type === 'link'" :href="text" target="_blank">{{ text }}</b-link>
|
||||
<span v-else>{{ text }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const LINK_REGEX_PATTERN =
|
||||
/(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*))/i
|
||||
|
||||
export default {
|
||||
name: 'LinkifyMessage',
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
linkifiedMessage() {
|
||||
const linkified = []
|
||||
let string = this.message
|
||||
let match
|
||||
while ((match = string.match(LINK_REGEX_PATTERN))) {
|
||||
if (match.index > 0)
|
||||
linkified.push({ type: 'text', text: string.substring(0, match.index) })
|
||||
linkified.push({ type: 'link', text: match[0] })
|
||||
string = string.substring(match.index + match[0].length)
|
||||
}
|
||||
if (string.length > 0) linkified.push({ type: 'text', text: string })
|
||||
return linkified
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -125,4 +125,68 @@ describe('ContributionMessagesListItem', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('links in contribtion message', () => {
|
||||
const propsData = {
|
||||
message: {
|
||||
id: 111,
|
||||
message: 'Lorem ipsum?',
|
||||
createdAt: '2022-08-29T12:23:27.000Z',
|
||||
updatedAt: null,
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Peter',
|
||||
userLastName: 'Lustig',
|
||||
userId: 107,
|
||||
__typename: 'ContributionMessage',
|
||||
},
|
||||
}
|
||||
|
||||
const ModeratorItemWrapper = () => {
|
||||
return mount(ContributionMessagesListItem, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
let messageField
|
||||
|
||||
describe('message of only one link', () => {
|
||||
beforeEach(() => {
|
||||
propsData.message.message = 'https://gradido.net/de/'
|
||||
wrapper = ModeratorItemWrapper()
|
||||
messageField = wrapper.find('div.is-not-moderator.text-left > div:nth-child(4)')
|
||||
})
|
||||
|
||||
it('contains the link as text', () => {
|
||||
expect(messageField.text()).toBe('https://gradido.net/de/')
|
||||
})
|
||||
|
||||
it('contains a link to the given address', () => {
|
||||
expect(messageField.find('a').attributes('href')).toBe('https://gradido.net/de/')
|
||||
})
|
||||
})
|
||||
|
||||
describe('message with text and two links', () => {
|
||||
beforeEach(() => {
|
||||
propsData.message.message = `Here you find all you need to know about Gradido: https://gradido.net/de/
|
||||
and here is the link to the repository: https://github.com/gradido/gradido`
|
||||
wrapper = ModeratorItemWrapper()
|
||||
messageField = wrapper.find('div.is-not-moderator.text-left > div:nth-child(4)')
|
||||
})
|
||||
|
||||
it('contains the whole text', () => {
|
||||
expect(messageField.text())
|
||||
.toBe(`Here you find all you need to know about Gradido: https://gradido.net/de/
|
||||
and here is the link to the repository: https://github.com/gradido/gradido`)
|
||||
})
|
||||
|
||||
it('contains the two links', () => {
|
||||
expect(messageField.findAll('a').at(0).attributes('href')).toBe('https://gradido.net/de/')
|
||||
expect(messageField.findAll('a').at(1).attributes('href')).toBe(
|
||||
'https://github.com/gradido/gradido',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,23 +1,28 @@
|
||||
<template>
|
||||
<div class="contribution-messages-list-item">
|
||||
<div v-if="message.isModerator" class="text-right is-moderator">
|
||||
<b-avatar square :text="initialLetters" variant="warning"></b-avatar>
|
||||
<b-avatar square variant="warning"></b-avatar>
|
||||
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
|
||||
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
|
||||
<small class="ml-4 text-success">{{ $t('moderator') }}</small>
|
||||
<div class="mt-2">{{ message.message }}</div>
|
||||
<linkify-message :message="message.message"></linkify-message>
|
||||
</div>
|
||||
<div v-else class="text-left is-not-moderator">
|
||||
<b-avatar :text="initialLetters" variant="info"></b-avatar>
|
||||
<b-avatar variant="info"></b-avatar>
|
||||
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
|
||||
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
|
||||
<div class="mt-2">{{ message.message }}</div>
|
||||
<linkify-message :message="message.message"></linkify-message>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import LinkifyMessage from '@/components/ContributionMessages/LinkifyMessage.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContributionMessagesListItem',
|
||||
components: {
|
||||
LinkifyMessage,
|
||||
},
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
|
||||
@ -7,6 +7,10 @@ const apolloMutateMock = jest.fn()
|
||||
const storeDispatchMock = jest.fn()
|
||||
const routerPushMock = jest.fn()
|
||||
|
||||
const stubs = {
|
||||
RouterLink: true,
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
@ -28,7 +32,7 @@ describe('NavBar', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(NavBar, { mocks, localVue })
|
||||
return mount(NavBar, { mocks, localVue, stubs })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
@ -41,13 +45,35 @@ describe('NavBar', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Navbar Menu', () => {
|
||||
it('has a link to overview', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(0).find('a').attributes('href')).toBe('/')
|
||||
})
|
||||
it('has a link to /user', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(1).find('a').attributes('href')).toBe('/user')
|
||||
})
|
||||
it('has a link to /creation', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(2).find('a').attributes('href')).toBe('/creation')
|
||||
})
|
||||
it('has a link to /creation-confirm', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(3).find('a').attributes('href')).toBe(
|
||||
'/creation-confirm',
|
||||
)
|
||||
})
|
||||
it('has a link to /contribution-links', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(4).find('a').attributes('href')).toBe(
|
||||
'/contribution-links',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('wallet', () => {
|
||||
const assignLocationSpy = jest.fn()
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('a').at(5).trigger('click')
|
||||
await wrapper.findAll('.nav-item').at(5).find('a').trigger('click')
|
||||
})
|
||||
|
||||
it.skip('changes widnow location to wallet', () => {
|
||||
it.skip('changes window location to wallet', () => {
|
||||
expect(assignLocationSpy).toBeCalledWith('valid-token')
|
||||
})
|
||||
|
||||
@ -63,7 +89,7 @@ describe('NavBar', () => {
|
||||
window.location = {
|
||||
assign: windowLocationMock,
|
||||
}
|
||||
await wrapper.findAll('a').at(6).trigger('click')
|
||||
await wrapper.findAll('.nav-item').at(6).find('a').trigger('click')
|
||||
})
|
||||
|
||||
it('redirects to /logout', () => {
|
||||
|
||||
@ -19,6 +19,9 @@
|
||||
>
|
||||
{{ $store.state.openCreations }} {{ $t('navbar.open_creation') }}
|
||||
</b-nav-item>
|
||||
<b-nav-item to="/contribution-links">
|
||||
{{ $t('navbar.automaticContributions') }}
|
||||
</b-nav-item>
|
||||
<b-nav-item @click="wallet">{{ $t('navbar.my-account') }}</b-nav-item>
|
||||
<b-nav-item @click="logout">{{ $t('navbar.logout') }}</b-nav-item>
|
||||
</b-navbar-nav>
|
||||
|
||||
@ -95,6 +95,7 @@
|
||||
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
|
||||
"name": "Name",
|
||||
"navbar": {
|
||||
"automaticContributions": "automatische Beiträge",
|
||||
"logout": "Abmelden",
|
||||
"multi_creation": "Mehrfachschöpfung",
|
||||
"my-account": "Mein Konto",
|
||||
|
||||
@ -95,6 +95,7 @@
|
||||
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
|
||||
"name": "Name",
|
||||
"navbar": {
|
||||
"automaticContributions": "Automatic Contributions",
|
||||
"logout": "Logout",
|
||||
"multi_creation": "Multiple creation",
|
||||
"my-account": "My Account",
|
||||
|
||||
58
admin/src/pages/ContributionLinks.spec.js
Normal file
58
admin/src/pages/ContributionLinks.spec.js
Normal file
@ -0,0 +1,58 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ContributionLinks from './ContributionLinks.vue'
|
||||
import { listContributionLinks } from '@/graphql/listContributionLinks.js'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloQueryMock = jest.fn().mockResolvedValueOnce({
|
||||
data: {
|
||||
listContributionLinks: {
|
||||
links: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
}
|
||||
|
||||
describe('ContributionLinks', () => {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(ContributionLinks, { localVue, mocks })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('calls listContributionLinks', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
query: listContributionLinks,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
45
admin/src/pages/ContributionLinks.vue
Normal file
45
admin/src/pages/ContributionLinks.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="contribution-link">
|
||||
<contribution-link
|
||||
:items="items"
|
||||
:count="count"
|
||||
@get-contribution-links="getContributionLinks"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { listContributionLinks } from '@/graphql/listContributionLinks.js'
|
||||
import ContributionLink from '../components/ContributionLink/ContributionLink.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContributionLinks',
|
||||
components: {
|
||||
ContributionLink,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
count: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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.getContributionLinks()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,6 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Overview from './Overview.vue'
|
||||
import { listContributionLinks } from '@/graphql/listContributionLinks.js'
|
||||
import { communityStatistics } from '@/graphql/communityStatistics.js'
|
||||
import { listUnconfirmedContributions } from '@/graphql/listUnconfirmedContributions.js'
|
||||
|
||||
@ -36,27 +35,6 @@ const apolloQueryMock = jest
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
listContributionLinks: {
|
||||
links: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
listUnconfirmedContributions: [
|
||||
@ -118,14 +96,6 @@ describe('Overview', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('calls listContributionLinks', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
query: listContributionLinks,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('commits three pending creations to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 3)
|
||||
})
|
||||
|
||||
@ -28,31 +28,21 @@
|
||||
</b-link>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
<contribution-link
|
||||
:items="items"
|
||||
:count="count"
|
||||
@get-contribution-links="getContributionLinks"
|
||||
/>
|
||||
<community-statistic class="mt-5" v-model="statistics" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { listContributionLinks } from '@/graphql/listContributionLinks.js'
|
||||
import { communityStatistics } from '@/graphql/communityStatistics.js'
|
||||
import ContributionLink from '../components/ContributionLink.vue'
|
||||
import CommunityStatistic from '../components/CommunityStatistic.vue'
|
||||
import { listUnconfirmedContributions } from '@/graphql/listUnconfirmedContributions.js'
|
||||
|
||||
export default {
|
||||
name: 'overview',
|
||||
components: {
|
||||
ContributionLink,
|
||||
CommunityStatistic,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
count: 0,
|
||||
statistics: {
|
||||
totalUsers: null,
|
||||
activeUsers: null,
|
||||
@ -75,20 +65,6 @@ export default {
|
||||
this.$store.commit('setOpenCreations', result.data.listUnconfirmedContributions.length)
|
||||
})
|
||||
},
|
||||
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')
|
||||
})
|
||||
},
|
||||
getCommunityStatistics() {
|
||||
this.$apollo
|
||||
.query({
|
||||
@ -113,7 +89,6 @@ export default {
|
||||
created() {
|
||||
this.getPendingCreations()
|
||||
this.getCommunityStatistics()
|
||||
this.getContributionLinks()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -45,7 +45,7 @@ describe('router', () => {
|
||||
|
||||
describe('routes', () => {
|
||||
it('has seven routes defined', () => {
|
||||
expect(routes).toHaveLength(7)
|
||||
expect(routes).toHaveLength(8)
|
||||
})
|
||||
|
||||
it('has "/overview" as default', async () => {
|
||||
@ -81,6 +81,13 @@ describe('router', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('contribution-links', () => {
|
||||
it('loads the "ContributionLinks" component', async () => {
|
||||
const component = await routes.find((r) => r.path === '/contribution-links').component()
|
||||
expect(component.default.name).toBe('ContributionLinks')
|
||||
})
|
||||
})
|
||||
|
||||
describe('not found page', () => {
|
||||
it('renders the "NotFound" component', async () => {
|
||||
const component = await routes.find((r) => r.path === '*').component()
|
||||
|
||||
@ -23,6 +23,10 @@ const routes = [
|
||||
path: '/creation-confirm',
|
||||
component: () => import('@/pages/CreationConfirm.vue'),
|
||||
},
|
||||
{
|
||||
path: '/contribution-links',
|
||||
component: () => import('@/pages/ContributionLinks.vue'),
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
component: () => import('@/components/NotFoundPage.vue'),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-backend",
|
||||
"version": "1.13.1",
|
||||
"version": "1.13.3",
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
|
||||
@ -66,6 +66,9 @@ export class EventTransactionCreation extends EventBasicTx {}
|
||||
export class EventTransactionReceive extends EventBasicTxX {}
|
||||
export class EventTransactionReceiveRedeem extends EventBasicTxX {}
|
||||
export class EventContributionCreate extends EventBasicCt {}
|
||||
export class EventAdminContributionCreate extends EventBasicCt {}
|
||||
export class EventAdminContributionDelete extends EventBasicCt {}
|
||||
export class EventAdminContributionUpdate extends EventBasicCt {}
|
||||
export class EventUserCreateContributionMessage extends EventBasicCtMsg {}
|
||||
export class EventAdminCreateContributionMessage extends EventBasicCtMsg {}
|
||||
export class EventContributionDelete extends EventBasicCt {}
|
||||
@ -74,6 +77,14 @@ export class EventContributionConfirm extends EventBasicCtX {}
|
||||
export class EventContributionDeny extends EventBasicCtX {}
|
||||
export class EventContributionLinkDefine extends EventBasicCt {}
|
||||
export class EventContributionLinkActivateRedeem extends EventBasicCt {}
|
||||
export class EventDeleteUser extends EventBasicUserId {}
|
||||
export class EventUndeleteUser extends EventBasicUserId {}
|
||||
export class EventChangeUserRole extends EventBasicUserId {}
|
||||
export class EventAdminUpdateContribution extends EventBasicCt {}
|
||||
export class EventAdminDeleteContribution extends EventBasicCt {}
|
||||
export class EventCreateContributionLink extends EventBasicCt {}
|
||||
export class EventDeleteContributionLink extends EventBasicCt {}
|
||||
export class EventUpdateContributionLink extends EventBasicCt {}
|
||||
|
||||
export class Event {
|
||||
constructor()
|
||||
@ -289,6 +300,27 @@ export class Event {
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventAdminContributionCreate(ev: EventAdminContributionCreate): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.ADMIN_CONTRIBUTION_CREATE
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventAdminContributionDelete(ev: EventAdminContributionDelete): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.ADMIN_CONTRIBUTION_DELETE
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventAdminContributionUpdate(ev: EventAdminContributionUpdate): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.ADMIN_CONTRIBUTION_UPDATE
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventUserCreateContributionMessage(ev: EventUserCreateContributionMessage): Event {
|
||||
this.setByBasicCtMsg(ev.userId, ev.contributionId, ev.amount, ev.messageId)
|
||||
this.type = EventProtocolType.USER_CREATE_CONTRIBUTION_MESSAGE
|
||||
@ -345,6 +377,62 @@ export class Event {
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventDeleteUser(ev: EventDeleteUser): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.DELETE_USER
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventUndeleteUser(ev: EventUndeleteUser): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.UNDELETE_USER
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventChangeUserRole(ev: EventChangeUserRole): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.CHANGE_USER_ROLE
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventAdminUpdateContribution(ev: EventAdminUpdateContribution): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.ADMIN_UPDATE_CONTRIBUTION
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventAdminDeleteContribution(ev: EventAdminDeleteContribution): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.ADMIN_DELETE_CONTRIBUTION
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventCreateContributionLink(ev: EventCreateContributionLink): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.CREATE_CONTRIBUTION_LINK
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventDeleteContributionLink(ev: EventDeleteContributionLink): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.DELETE_CONTRIBUTION_LINK
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventUpdateContributionLink(ev: EventUpdateContributionLink): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.UPDATE_CONTRIBUTION_LINK
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setByBasicUser(userId: number): Event {
|
||||
this.setEventBasic()
|
||||
this.userId = userId
|
||||
|
||||
@ -33,6 +33,17 @@ export enum EventProtocolType {
|
||||
CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM',
|
||||
CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE',
|
||||
CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE',
|
||||
ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE',
|
||||
ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE',
|
||||
ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE',
|
||||
USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE',
|
||||
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
|
||||
DELETE_USER = 'DELETE_USER',
|
||||
UNDELETE_USER = 'UNDELETE_USER',
|
||||
CHANGE_USER_ROLE = 'CHANGE_USER_ROLE',
|
||||
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
|
||||
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
|
||||
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
|
||||
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
|
||||
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
|
||||
}
|
||||
|
||||
@ -42,6 +42,9 @@ import { Contribution } from '@entity/Contribution'
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail'
|
||||
import { EventProtocol } from '@entity/EventProtocol'
|
||||
import { EventProtocolType } from '@/event/EventProtocolType'
|
||||
import { logger } from '@test/testSetup'
|
||||
|
||||
// mock account activation email to avoid console spam
|
||||
jest.mock('@/mailer/sendAccountActivationEmail', () => {
|
||||
@ -144,6 +147,10 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(`Could not find user with userId: ${admin.id + 1}`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('change role with success', () => {
|
||||
@ -196,6 +203,9 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Administrator can not change his own role!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('user has already role to be set', () => {
|
||||
@ -213,6 +223,10 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('User is already admin!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('to usual user', () => {
|
||||
@ -229,6 +243,10 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('User is already a usual user!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -297,6 +315,10 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(`Could not find user with userId: ${admin.id + 1}`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete self', () => {
|
||||
@ -309,6 +331,10 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Moderator can not delete his own account!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete with success', () => {
|
||||
@ -338,6 +364,10 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(`Could not find user with userId: ${user.id}`)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -405,6 +435,10 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(`Could not find user with userId: ${admin.id + 1}`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('user to undelete is not deleted', () => {
|
||||
@ -422,6 +456,10 @@ describe('AdminResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('User is not deleted')
|
||||
})
|
||||
|
||||
describe('undelete deleted user', () => {
|
||||
beforeAll(async () => {
|
||||
await mutate({ mutation: deleteUser, variables: { userId: user.id } })
|
||||
@ -909,6 +947,12 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Could not find user with email: bibi@bloxberg.de',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('user to create for is deleted', () => {
|
||||
@ -928,6 +972,12 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'This user was deleted. Cannot create a contribution.',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('user to create for has email not confirmed', () => {
|
||||
@ -947,6 +997,12 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Contribution could not be saved, Email is not activated',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('valid user to create for', () => {
|
||||
@ -967,6 +1023,13 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'No information for available creations with the given creationDate=',
|
||||
'Invalid Date',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('date of creation is four months ago', () => {
|
||||
@ -987,6 +1050,13 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'No information for available creations with the given creationDate=',
|
||||
variables.creationDate,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('date of creation is in the future', () => {
|
||||
@ -1007,6 +1077,13 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'No information for available creations with the given creationDate=',
|
||||
variables.creationDate,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('amount of creation is too high', () => {
|
||||
@ -1024,6 +1101,12 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'The amount (2000 GDD) to be created exceeds the amount (1000 GDD) still available for this month.',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('creation is valid', () => {
|
||||
@ -1039,6 +1122,15 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the admin create contribution event in the database', async () => {
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.ADMIN_CONTRIBUTION_CREATE,
|
||||
userId: admin.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('second creation surpasses the available amount ', () => {
|
||||
@ -1056,6 +1148,12 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'The amount (1000 GDD) to be created exceeds the amount (800 GDD) still available for this month.',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1134,6 +1232,12 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Could not find UserContact with email: bob@baumeister.de',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('user for creation to update is deleted', () => {
|
||||
@ -1155,6 +1259,10 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('User was deleted (stephen@hawking.uk)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('creation does not exist', () => {
|
||||
@ -1176,6 +1284,10 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('No contribution found to given id.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('user email does not match creation user', () => {
|
||||
@ -1188,7 +1300,9 @@ describe('AdminResolver', () => {
|
||||
email: 'bibi@bloxberg.de',
|
||||
amount: new Decimal(300),
|
||||
memo: 'Danke Bibi!',
|
||||
creationDate: new Date().toString(),
|
||||
creationDate: creation
|
||||
? creation.contributionDate.toString()
|
||||
: new Date().toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
@ -1201,11 +1315,17 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'user of the pending contribution and send user does not correspond',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('creation update is not valid', () => {
|
||||
// as this test has not clearly defined that date, it is a false positive
|
||||
it.skip('throws an error', async () => {
|
||||
it('throws an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: adminUpdateContribution,
|
||||
@ -1214,24 +1334,32 @@ describe('AdminResolver', () => {
|
||||
email: 'peter@lustig.de',
|
||||
amount: new Decimal(1900),
|
||||
memo: 'Danke Peter!',
|
||||
creationDate: new Date().toString(),
|
||||
creationDate: creation
|
||||
? creation.contributionDate.toString()
|
||||
: new Date().toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'The amount (1900 GDD) to be created exceeds the amount (500 GDD) still available for this month.',
|
||||
'The amount (1900 GDD) to be created exceeds the amount (1000 GDD) still available for this month.',
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'The amount (1900 GDD) to be created exceeds the amount (1000 GDD) still available for this month.',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('creation update is successful changing month', () => {
|
||||
describe.skip('creation update is successful changing month', () => {
|
||||
// skipped as changing the month is currently disable
|
||||
it.skip('returns update creation object', async () => {
|
||||
it('returns update creation object', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: adminUpdateContribution,
|
||||
@ -1240,7 +1368,9 @@ describe('AdminResolver', () => {
|
||||
email: 'peter@lustig.de',
|
||||
amount: new Decimal(300),
|
||||
memo: 'Danke Peter!',
|
||||
creationDate: new Date().toString(),
|
||||
creationDate: creation
|
||||
? creation.contributionDate.toString()
|
||||
: new Date().toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
@ -1250,17 +1380,26 @@ describe('AdminResolver', () => {
|
||||
date: expect.any(String),
|
||||
memo: 'Danke Peter!',
|
||||
amount: '300',
|
||||
creation: ['1000', '1000', '200'],
|
||||
creation: ['1000', '700', '500'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the admin update contribution event in the database', async () => {
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
|
||||
userId: admin.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('creation update is successful without changing month', () => {
|
||||
// actually this mutation IS changing the month
|
||||
it.skip('returns update creation object', async () => {
|
||||
it('returns update creation object', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: adminUpdateContribution,
|
||||
@ -1269,7 +1408,9 @@ describe('AdminResolver', () => {
|
||||
email: 'peter@lustig.de',
|
||||
amount: new Decimal(200),
|
||||
memo: 'Das war leider zu Viel!',
|
||||
creationDate: new Date().toString(),
|
||||
creationDate: creation
|
||||
? creation.contributionDate.toString()
|
||||
: new Date().toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
@ -1279,12 +1420,21 @@ describe('AdminResolver', () => {
|
||||
date: expect.any(String),
|
||||
memo: 'Das war leider zu Viel!',
|
||||
amount: '200',
|
||||
creation: ['1000', '1000', '300'],
|
||||
creation: ['1000', '800', '500'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the admin update contribution event in the database', async () => {
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
|
||||
userId: admin.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1304,10 +1454,10 @@ describe('AdminResolver', () => {
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
date: expect.any(String),
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
amount: '400',
|
||||
memo: 'Das war leider zu Viel!',
|
||||
amount: '200',
|
||||
moderator: admin.id,
|
||||
creation: ['1000', '600', '500'],
|
||||
creation: ['1000', '800', '500'],
|
||||
},
|
||||
{
|
||||
id: expect.any(Number),
|
||||
@ -1318,7 +1468,7 @@ describe('AdminResolver', () => {
|
||||
memo: 'Grundeinkommen',
|
||||
amount: '500',
|
||||
moderator: admin.id,
|
||||
creation: ['1000', '600', '500'],
|
||||
creation: ['1000', '800', '500'],
|
||||
},
|
||||
{
|
||||
id: expect.any(Number),
|
||||
@ -1365,6 +1515,10 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution not found for given id: -1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('admin deletes own user contribution', () => {
|
||||
@ -1414,6 +1568,15 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the admin delete contribution event in the database', async () => {
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.ADMIN_CONTRIBUTION_DELETE,
|
||||
userId: admin.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1433,6 +1596,10 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution not found for given id: -1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm own creation', () => {
|
||||
@ -1460,6 +1627,10 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Moderator can not confirm own contribution')
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm creation for other user', () => {
|
||||
@ -1488,6 +1659,14 @@ describe('AdminResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the contribution confirm event in the database', async () => {
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.CONTRIBUTION_CONFIRM,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('creates a transaction', async () => {
|
||||
const transaction = await DbTransaction.find()
|
||||
expect(transaction[0].amount.toString()).toBe('450')
|
||||
@ -1512,6 +1691,14 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the send confirmation email event in the database', async () => {
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.SEND_CONFIRMATION_EMAIL,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm two creations one after the other quickly', () => {
|
||||
@ -2052,6 +2239,12 @@ describe('AdminResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Start-Date is not initialized. A Start-Date must be set!',
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if missing endDate', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
@ -2068,6 +2261,12 @@ describe('AdminResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'End-Date is not initialized. An End-Date must be set!',
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if endDate is before startDate', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
@ -2087,6 +2286,12 @@ describe('AdminResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The value of validFrom must before or equals the validTo!`,
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if name is an empty string', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
@ -2103,6 +2308,10 @@ describe('AdminResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('The name must be initialized!')
|
||||
})
|
||||
|
||||
it('returns an error if name is shorter than 5 characters', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
@ -2123,6 +2332,12 @@ describe('AdminResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The value of 'name' with a length of 3 did not fulfill the requested bounderies min=5 and max=100`,
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if name is longer than 100 characters', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
@ -2143,6 +2358,12 @@ describe('AdminResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The value of 'name' with a length of 101 did not fulfill the requested bounderies min=5 and max=100`,
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if memo is an empty string', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
@ -2159,6 +2380,10 @@ describe('AdminResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('The memo must be initialized!')
|
||||
})
|
||||
|
||||
it('returns an error if memo is shorter than 5 characters', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
@ -2179,6 +2404,12 @@ describe('AdminResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The value of 'memo' with a length of 3 did not fulfill the requested bounderies min=5 and max=255`,
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if memo is longer than 255 characters', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
@ -2199,6 +2430,12 @@ describe('AdminResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The value of 'memo' with a length of 256 did not fulfill the requested bounderies min=5 and max=255`,
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if amount is not positive', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
@ -2216,6 +2453,12 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'The amount=0 must be initialized with a positiv value!',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('listContributionLinks', () => {
|
||||
@ -2271,6 +2514,10 @@ describe('AdminResolver', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution Link not found to given id: -1')
|
||||
})
|
||||
|
||||
describe('valid id', () => {
|
||||
let linkId: number
|
||||
beforeAll(async () => {
|
||||
@ -2336,6 +2583,10 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution Link not found to given id: -1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('valid id', () => {
|
||||
|
||||
@ -64,6 +64,15 @@ import { ContributionMessageType } from '@enum/MessageType'
|
||||
import { ContributionMessage } from '@model/ContributionMessage'
|
||||
import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail'
|
||||
import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributionMessageEmail'
|
||||
import { eventProtocol } from '@/event/EventProtocolEmitter'
|
||||
import {
|
||||
Event,
|
||||
EventAdminContributionCreate,
|
||||
EventAdminContributionDelete,
|
||||
EventAdminContributionUpdate,
|
||||
EventContributionConfirm,
|
||||
EventSendConfirmationEmail,
|
||||
} from '@/event/Event'
|
||||
import { ContributionListResult } from '../model/Contribution'
|
||||
|
||||
// const EMAIL_OPT_IN_REGISTER = 1
|
||||
@ -145,11 +154,13 @@ export class AdminResolver {
|
||||
const user = await dbUser.findOne({ id: userId })
|
||||
// user exists ?
|
||||
if (!user) {
|
||||
logger.error(`Could not find user with userId: ${userId}`)
|
||||
throw new Error(`Could not find user with userId: ${userId}`)
|
||||
}
|
||||
// administrator user changes own role?
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === userId) {
|
||||
logger.error('Administrator can not change his own role!')
|
||||
throw new Error('Administrator can not change his own role!')
|
||||
}
|
||||
// change isAdmin
|
||||
@ -158,6 +169,7 @@ export class AdminResolver {
|
||||
if (isAdmin === true) {
|
||||
user.isAdmin = new Date()
|
||||
} else {
|
||||
logger.error('User is already a usual user!')
|
||||
throw new Error('User is already a usual user!')
|
||||
}
|
||||
break
|
||||
@ -165,6 +177,7 @@ export class AdminResolver {
|
||||
if (isAdmin === false) {
|
||||
user.isAdmin = null
|
||||
} else {
|
||||
logger.error('User is already admin!')
|
||||
throw new Error('User is already admin!')
|
||||
}
|
||||
break
|
||||
@ -183,11 +196,13 @@ export class AdminResolver {
|
||||
const user = await dbUser.findOne({ id: userId })
|
||||
// user exists ?
|
||||
if (!user) {
|
||||
logger.error(`Could not find user with userId: ${userId}`)
|
||||
throw new Error(`Could not find user with userId: ${userId}`)
|
||||
}
|
||||
// moderator user disabled own account?
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === userId) {
|
||||
logger.error('Moderator can not delete his own account!')
|
||||
throw new Error('Moderator can not delete his own account!')
|
||||
}
|
||||
// soft-delete user
|
||||
@ -201,9 +216,11 @@ export class AdminResolver {
|
||||
async unDeleteUser(@Arg('userId', () => Int) userId: number): Promise<Date | null> {
|
||||
const user = await dbUser.findOne({ id: userId }, { withDeleted: true })
|
||||
if (!user) {
|
||||
logger.error(`Could not find user with userId: ${userId}`)
|
||||
throw new Error(`Could not find user with userId: ${userId}`)
|
||||
}
|
||||
if (!user.deletedAt) {
|
||||
logger.error('User is not deleted')
|
||||
throw new Error('User is not deleted')
|
||||
}
|
||||
await user.recover()
|
||||
@ -240,6 +257,8 @@ export class AdminResolver {
|
||||
logger.error('Contribution could not be saved, Email is not activated')
|
||||
throw new Error('Contribution could not be saved, Email is not activated')
|
||||
}
|
||||
|
||||
const event = new Event()
|
||||
const moderator = getUser(context)
|
||||
logger.trace('moderator: ', moderator.id)
|
||||
const creations = await getUserCreation(emailContact.userId)
|
||||
@ -258,7 +277,17 @@ export class AdminResolver {
|
||||
contribution.contributionStatus = ContributionStatus.PENDING
|
||||
|
||||
logger.trace('contribution to save', contribution)
|
||||
|
||||
await DbContribution.save(contribution)
|
||||
|
||||
const eventAdminCreateContribution = new EventAdminContributionCreate()
|
||||
eventAdminCreateContribution.userId = moderator.id
|
||||
eventAdminCreateContribution.amount = amount
|
||||
eventAdminCreateContribution.contributionId = contribution.id
|
||||
await eventProtocol.writeEvent(
|
||||
event.setEventAdminContributionCreate(eventAdminCreateContribution),
|
||||
)
|
||||
|
||||
return getUserCreation(emailContact.userId)
|
||||
}
|
||||
|
||||
@ -319,7 +348,6 @@ export class AdminResolver {
|
||||
const contributionToUpdate = await DbContribution.findOne({
|
||||
where: { id, confirmedAt: IsNull() },
|
||||
})
|
||||
|
||||
if (!contributionToUpdate) {
|
||||
logger.error('No contribution found to given id.')
|
||||
throw new Error('No contribution found to given id.')
|
||||
@ -337,6 +365,7 @@ export class AdminResolver {
|
||||
|
||||
const creationDateObj = new Date(creationDate)
|
||||
let creations = await getUserCreation(user.id)
|
||||
|
||||
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
|
||||
creations = updateCreations(creations, contributionToUpdate)
|
||||
} else {
|
||||
@ -353,6 +382,7 @@ export class AdminResolver {
|
||||
contributionToUpdate.contributionStatus = ContributionStatus.PENDING
|
||||
|
||||
await DbContribution.save(contributionToUpdate)
|
||||
|
||||
const result = new AdminUpdateContribution()
|
||||
result.amount = amount
|
||||
result.memo = contributionToUpdate.memo
|
||||
@ -360,6 +390,15 @@ export class AdminResolver {
|
||||
|
||||
result.creation = await getUserCreation(user.id)
|
||||
|
||||
const event = new Event()
|
||||
const eventAdminContributionUpdate = new EventAdminContributionUpdate()
|
||||
eventAdminContributionUpdate.userId = user.id
|
||||
eventAdminContributionUpdate.amount = amount
|
||||
eventAdminContributionUpdate.contributionId = contributionToUpdate.id
|
||||
await eventProtocol.writeEvent(
|
||||
event.setEventAdminContributionUpdate(eventAdminContributionUpdate),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@ -420,6 +459,16 @@ export class AdminResolver {
|
||||
contribution.deletedBy = moderator.id
|
||||
await contribution.save()
|
||||
const res = await contribution.softRemove()
|
||||
|
||||
const event = new Event()
|
||||
const eventAdminContributionDelete = new EventAdminContributionDelete()
|
||||
eventAdminContributionDelete.userId = contribution.userId
|
||||
eventAdminContributionDelete.amount = contribution.amount
|
||||
eventAdminContributionDelete.contributionId = contribution.id
|
||||
await eventProtocol.writeEvent(
|
||||
event.setEventAdminContributionDelete(eventAdminContributionDelete),
|
||||
)
|
||||
|
||||
return !!res
|
||||
}
|
||||
|
||||
@ -515,6 +564,13 @@ export class AdminResolver {
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
|
||||
const event = new Event()
|
||||
const eventContributionConfirm = new EventContributionConfirm()
|
||||
eventContributionConfirm.userId = user.id
|
||||
eventContributionConfirm.amount = contribution.amount
|
||||
eventContributionConfirm.contributionId = contribution.id
|
||||
await eventProtocol.writeEvent(event.setEventContributionConfirm(eventContributionConfirm))
|
||||
return true
|
||||
}
|
||||
|
||||
@ -576,6 +632,13 @@ export class AdminResolver {
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
logger.info(`Account confirmation link: ${activationLink}`)
|
||||
} else {
|
||||
const event = new Event()
|
||||
const eventSendConfirmationEmail = new EventSendConfirmationEmail()
|
||||
eventSendConfirmationEmail.userId = user.id
|
||||
await eventProtocol.writeEvent(
|
||||
event.setEventSendConfirmationEmail(eventSendConfirmationEmail),
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
@ -768,9 +831,11 @@ export class AdminResolver {
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!contribution) {
|
||||
logger.error('Contribution not found')
|
||||
throw new Error('Contribution not found')
|
||||
}
|
||||
if (contribution.userId === user.id) {
|
||||
logger.error('Admin can not answer on own contribution')
|
||||
throw new Error('Admin can not answer on own contribution')
|
||||
}
|
||||
if (!contribution.user.emailContact) {
|
||||
|
||||
@ -63,6 +63,8 @@ export class StatisticsResolver {
|
||||
.where('transaction.decay IS NOT NULL')
|
||||
.getRawOne()
|
||||
|
||||
await queryRunner.release()
|
||||
|
||||
return {
|
||||
totalUsers,
|
||||
activeUsers,
|
||||
|
||||
@ -6,8 +6,15 @@ import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { login, createContributionLink, redeemTransactionLink } from '@/seeds/graphql/mutations'
|
||||
import {
|
||||
login,
|
||||
createContributionLink,
|
||||
redeemTransactionLink,
|
||||
createContribution,
|
||||
updateContribution,
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { GraphQLError } from 'graphql'
|
||||
|
||||
@ -32,6 +39,7 @@ describe('TransactionLinkResolver', () => {
|
||||
describe('redeem daily Contribution Link', () => {
|
||||
const now = new Date()
|
||||
let contributionLink: DbContributionLink | undefined
|
||||
let contribution: UnconfirmedContribution | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
await mutate({
|
||||
@ -79,56 +87,59 @@ describe('TransactionLinkResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('allows the user to redeem the contribution link', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
redeemTransactionLink: true,
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
|
||||
),
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
describe('after one day', () => {
|
||||
describe('user has pending contribution of 1000 GDD', () => {
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers()
|
||||
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
|
||||
setTimeout(() => {}, 1000 * 60 * 60 * 24)
|
||||
jest.runAllTimers()
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
const result = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: new Decimal(1000),
|
||||
memo: 'I was brewing potions for the community the whole month',
|
||||
creationDate: now.toISOString(),
|
||||
},
|
||||
})
|
||||
contribution = result.data.createContribution
|
||||
})
|
||||
|
||||
it('does not allow the user to redeem the contribution link', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
|
||||
),
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user has no pending contributions that would not allow to redeem the link', () => {
|
||||
beforeAll(async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
await mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: contribution ? contribution.id : -1,
|
||||
amount: new Decimal(800),
|
||||
memo: 'I was brewing potions for the community the whole month',
|
||||
creationDate: now.toISOString(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('allows the user to redeem the contribution link again', async () => {
|
||||
it('allows the user to redeem the contribution link', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
@ -160,6 +171,56 @@ describe('TransactionLinkResolver', () => {
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
describe('after one day', () => {
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers()
|
||||
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
|
||||
setTimeout(() => {}, 1000 * 60 * 60 * 24)
|
||||
jest.runAllTimers()
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('allows the user to redeem the contribution link again', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
redeemTransactionLink: true,
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
|
||||
),
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -74,10 +74,7 @@ export class TransactionLinkResolver {
|
||||
const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay)
|
||||
|
||||
// validate amount
|
||||
const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate)
|
||||
if (!sendBalance) {
|
||||
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||
}
|
||||
await calculateBalance(user.id, holdAvailableAmount, createdDate)
|
||||
|
||||
const transactionLink = dbTransactionLink.create()
|
||||
transactionLink.userId = user.id
|
||||
@ -261,7 +258,7 @@ export class TransactionLinkResolver {
|
||||
}
|
||||
}
|
||||
|
||||
const creations = await getUserCreation(user.id, false)
|
||||
const creations = await getUserCreation(user.id)
|
||||
logger.info('open creations', creations)
|
||||
validateContribution(creations, contributionLink.amount, now)
|
||||
const contribution = new DbContribution()
|
||||
|
||||
366
backend/src/graphql/resolver/TransactionResolver.test.ts
Normal file
366
backend/src/graphql/resolver/TransactionResolver.test.ts
Normal file
@ -0,0 +1,366 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { EventProtocolType } from '@/event/EventProtocolType'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import {
|
||||
confirmContribution,
|
||||
createContribution,
|
||||
login,
|
||||
sendCoins,
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { stephenHawking } from '@/seeds/users/stephen-hawking'
|
||||
import { EventProtocol } from '@entity/EventProtocol'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { User } from '@entity/User'
|
||||
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
|
||||
import { logger } from '@test/testSetup'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { findUserByEmail } from './UserResolver'
|
||||
|
||||
let mutate: any, query: any, con: any
|
||||
let testEnv: any
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment(logger)
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.close()
|
||||
})
|
||||
|
||||
let bobData: any
|
||||
let peterData: any
|
||||
let user: User[]
|
||||
|
||||
describe('send coins', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, peterLustig)
|
||||
await userFactory(testEnv, bobBaumeister)
|
||||
await userFactory(testEnv, stephenHawking)
|
||||
await userFactory(testEnv, garrickOllivander)
|
||||
|
||||
bobData = {
|
||||
email: 'bob@baumeister.de',
|
||||
password: 'Aa12345_',
|
||||
}
|
||||
|
||||
peterData = {
|
||||
email: 'peter@lustig.de',
|
||||
password: 'Aa12345_',
|
||||
}
|
||||
|
||||
user = await User.find({ relations: ['emailContact'] })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
describe('unknown recipient', () => {
|
||||
it('throws an error', async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: bobData,
|
||||
})
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'wrong@email.com',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('No user with this credentials')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(`UserContact with email=wrong@email.com does not exists`)
|
||||
})
|
||||
|
||||
describe('deleted recipient', () => {
|
||||
it('throws an error', async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: peterData,
|
||||
})
|
||||
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'stephen@hawking.uk',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The recipient account was deleted')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', async () => {
|
||||
// find peter to check the log
|
||||
const user = await findUserByEmail(peterData.email)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The recipient account was deleted: recipientUser=${user}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('recipient account not activated', () => {
|
||||
it('throws an error', async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: peterData,
|
||||
})
|
||||
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'garrick@ollivander.com',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The recipient account is not activated')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', async () => {
|
||||
// find peter to check the log
|
||||
const user = await findUserByEmail(peterData.email)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The recipient account is not activated: recipientUser=${user}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('errors in the transaction itself', () => {
|
||||
beforeAll(async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: bobData,
|
||||
})
|
||||
})
|
||||
|
||||
describe('sender and recipient are the same', () => {
|
||||
it('throws an error', async () => {
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'bob@baumeister.de',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Sender and Recipient are the same.')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Sender and Recipient are the same.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('memo text is too long', () => {
|
||||
it('throws an error', async () => {
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test t',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too long (255 characters maximum)')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('memo text is too long: memo.length=256 > 255')
|
||||
})
|
||||
})
|
||||
|
||||
describe('memo text is too short', () => {
|
||||
it('throws an error', async () => {
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too short (5 characters minimum)')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('memo text is too short: memo.length=4 < 5')
|
||||
})
|
||||
})
|
||||
|
||||
describe('user has not enough GDD', () => {
|
||||
it('throws an error', async () => {
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'testing',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError(`User has not received any GDD yet`)],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`No prior transaction found for user with id: ${user[1].id}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sending negative amount', () => {
|
||||
it('throws an error', async () => {
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: -50,
|
||||
memo: 'testing negative',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Transaction amount must be greater than 0')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Transaction amount must be greater than 0: -50')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user has some GDD', () => {
|
||||
beforeAll(async () => {
|
||||
resetToken()
|
||||
|
||||
// login as bob again
|
||||
await query({ mutation: login, variables: bobData })
|
||||
|
||||
// create contribution as user bob
|
||||
const contribution = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: { amount: 1000, memo: 'testing', creationDate: new Date().toISOString() },
|
||||
})
|
||||
|
||||
// login as admin
|
||||
await query({ mutation: login, variables: peterData })
|
||||
|
||||
// confirm the contribution
|
||||
await mutate({
|
||||
mutation: confirmContribution,
|
||||
variables: { id: contribution.data.createContribution.id },
|
||||
})
|
||||
|
||||
// login as bob again
|
||||
await query({ mutation: login, variables: bobData })
|
||||
})
|
||||
|
||||
describe('good transaction', () => {
|
||||
it('sends the coins', async () => {
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 50,
|
||||
memo: 'unrepeatable memo',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
sendCoins: 'true',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the send transaction event in the database', async () => {
|
||||
// Find the exact transaction (sent one is the one with user[1] as user)
|
||||
const transaction = await Transaction.find({
|
||||
userId: user[1].id,
|
||||
memo: 'unrepeatable memo',
|
||||
})
|
||||
|
||||
expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.TRANSACTION_SEND,
|
||||
userId: user[1].id,
|
||||
transactionId: transaction[0].id,
|
||||
xUserId: user[0].id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the receive event in the database', async () => {
|
||||
// Find the exact transaction (received one is the one with user[0] as user)
|
||||
const transaction = await Transaction.find({
|
||||
userId: user[0].id,
|
||||
memo: 'unrepeatable memo',
|
||||
})
|
||||
|
||||
expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.TRANSACTION_RECEIVE,
|
||||
userId: user[0].id,
|
||||
transactionId: transaction[0].id,
|
||||
xUserId: user[1].id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -37,6 +37,9 @@ import { BalanceResolver } from './BalanceResolver'
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
import { findUserByEmail } from './UserResolver'
|
||||
import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed'
|
||||
import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event'
|
||||
import { eventProtocol } from '@/event/EventProtocolEmitter'
|
||||
import { Decay } from '../model/Decay'
|
||||
|
||||
export const executeTransaction = async (
|
||||
amount: Decimal,
|
||||
@ -55,28 +58,19 @@ export const executeTransaction = async (
|
||||
}
|
||||
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS}`)
|
||||
logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`)
|
||||
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
|
||||
}
|
||||
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
logger.error(`memo text is too short: memo.length=${memo.length} < (${MEMO_MIN_CHARS}`)
|
||||
logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`)
|
||||
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
||||
}
|
||||
|
||||
// validate amount
|
||||
const receivedCallDate = new Date()
|
||||
const sendBalance = await calculateBalance(
|
||||
sender.id,
|
||||
amount.mul(-1),
|
||||
receivedCallDate,
|
||||
transactionLink,
|
||||
)
|
||||
logger.debug(`calculated Balance=${sendBalance}`)
|
||||
if (!sendBalance) {
|
||||
logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`)
|
||||
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||
}
|
||||
|
||||
const sendBalance = await calculateBalance(sender.id, amount, receivedCallDate, transactionLink)
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
@ -106,7 +100,24 @@ export const executeTransaction = async (
|
||||
transactionReceive.userId = recipient.id
|
||||
transactionReceive.linkedUserId = sender.id
|
||||
transactionReceive.amount = amount
|
||||
const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
|
||||
|
||||
// state received balance
|
||||
let receiveBalance: {
|
||||
balance: Decimal
|
||||
decay: Decay
|
||||
lastTransactionId: number
|
||||
} | null
|
||||
|
||||
// try received balance
|
||||
try {
|
||||
receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
|
||||
} catch (e) {
|
||||
logger.info(
|
||||
`User with no transactions sent: ${recipient.id}, has received a transaction of ${amount} GDD from user: ${sender.id}`,
|
||||
)
|
||||
receiveBalance = null
|
||||
}
|
||||
|
||||
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
|
||||
transactionReceive.balanceDate = receivedCallDate
|
||||
transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
|
||||
@ -135,6 +146,20 @@ export const executeTransaction = async (
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info(`commit Transaction successful...`)
|
||||
|
||||
const eventTransactionSend = new EventTransactionSend()
|
||||
eventTransactionSend.userId = transactionSend.userId
|
||||
eventTransactionSend.xUserId = transactionSend.linkedUserId
|
||||
eventTransactionSend.transactionId = transactionSend.id
|
||||
eventTransactionSend.amount = transactionSend.amount.mul(-1)
|
||||
await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend))
|
||||
|
||||
const eventTransactionReceive = new EventTransactionReceive()
|
||||
eventTransactionReceive.userId = transactionReceive.userId
|
||||
eventTransactionReceive.xUserId = transactionReceive.linkedUserId
|
||||
eventTransactionReceive.transactionId = transactionReceive.id
|
||||
eventTransactionReceive.amount = transactionReceive.amount
|
||||
await eventProtocol.writeEvent(new Event().setEventTransactionReceive(eventTransactionReceive))
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error(`Transaction was not successful: ${e}`)
|
||||
@ -316,6 +341,10 @@ export class TransactionResolver {
|
||||
}
|
||||
*/
|
||||
// const recipientUser = await dbUser.findOne({ id: emailContact.userId })
|
||||
|
||||
/* Code inside this if statement is unreachable (useless by so),
|
||||
in findUserByEmail() an error is already thrown if the user is not found
|
||||
*/
|
||||
if (!recipientUser) {
|
||||
logger.error(`unknown recipient to UserContact: email=${email}`)
|
||||
throw new Error('unknown recipient')
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { getConnection } from '@dbTools/typeorm'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
@ -50,27 +49,27 @@ export const getUserCreations = async (
|
||||
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
||||
logger.trace('getUserCreations dateFilter=', dateFilter)
|
||||
|
||||
const unionString = includePending
|
||||
? `
|
||||
UNION
|
||||
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`
|
||||
: ''
|
||||
logger.trace('getUserCreations unionString=', unionString)
|
||||
const sumAmountContributionPerUserAndLast3MonthQuery = queryRunner.manager
|
||||
.createQueryBuilder(Contribution, 'c')
|
||||
.select('month(contribution_date)', 'month')
|
||||
.addSelect('user_id', 'userId')
|
||||
.addSelect('sum(amount)', 'sum')
|
||||
.where(`user_id in (${ids.toString()})`)
|
||||
.andWhere(`contribution_date >= ${dateFilter}`)
|
||||
.andWhere('deleted_at IS NULL')
|
||||
.andWhere('denied_at IS NULL')
|
||||
.groupBy('month')
|
||||
.addGroupBy('userId')
|
||||
.orderBy('month', 'DESC')
|
||||
|
||||
const unionQuery = await queryRunner.manager.query(`
|
||||
SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM
|
||||
(SELECT creation_date AS date, amount AS amount, user_id AS userId FROM transactions
|
||||
WHERE user_id IN (${ids.toString()})
|
||||
AND type_id = ${TransactionTypeId.CREATION}
|
||||
AND creation_date >= ${dateFilter}
|
||||
${unionString}) AS result
|
||||
GROUP BY month, userId
|
||||
ORDER BY date DESC
|
||||
`)
|
||||
logger.trace('getUserCreations unionQuery=', unionQuery)
|
||||
if (!includePending) {
|
||||
sumAmountContributionPerUserAndLast3MonthQuery.andWhere('confirmed_at IS NOT NULL')
|
||||
}
|
||||
|
||||
const sumAmountContributionPerUserAndLast3Month =
|
||||
await sumAmountContributionPerUserAndLast3MonthQuery.getRawMany()
|
||||
|
||||
logger.trace(sumAmountContributionPerUserAndLast3Month)
|
||||
|
||||
await queryRunner.release()
|
||||
|
||||
@ -78,9 +77,9 @@ export const getUserCreations = async (
|
||||
return {
|
||||
id,
|
||||
creations: months.map((month) => {
|
||||
const creation = unionQuery.find(
|
||||
(raw: { month: string; id: string; creation: number[] }) =>
|
||||
parseInt(raw.month) === month && parseInt(raw.id) === id,
|
||||
const creation = sumAmountContributionPerUserAndLast3Month.find(
|
||||
(raw: { month: string; userId: string; creation: number[] }) =>
|
||||
parseInt(raw.month) === month && parseInt(raw.userId) === id,
|
||||
)
|
||||
return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0)
|
||||
}),
|
||||
|
||||
@ -28,8 +28,8 @@ export class UserRepository extends Repository<DbUser> {
|
||||
): Promise<[DbUser[], number]> {
|
||||
const query = this.createQueryBuilder('user')
|
||||
.select(select)
|
||||
.leftJoinAndSelect('user.emailContact', 'emailContact')
|
||||
.withDeleted()
|
||||
.leftJoinAndSelect('user.emailContact', 'emailContact')
|
||||
.where(
|
||||
new Brackets((qb) => {
|
||||
qb.where(
|
||||
|
||||
@ -1,5 +1,17 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export const objectValuesToArray = (obj: { [x: string]: string }): Array<string> => {
|
||||
return Object.keys(obj).map(function (key) {
|
||||
return obj[key]
|
||||
})
|
||||
}
|
||||
|
||||
// to improve code readability, as String is needed, it is handled inside this utility function
|
||||
export const decimalAddition = (a: Decimal, b: Decimal): Decimal => {
|
||||
return a.add(b.toString())
|
||||
}
|
||||
|
||||
// to improve code readability, as String is needed, it is handled inside this utility function
|
||||
export const decimalSubtraction = (a: Decimal, b: Decimal): Decimal => {
|
||||
return a.minus(b.toString())
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import { Decay } from '@model/Decay'
|
||||
import { getCustomRepository } from '@dbTools/typeorm'
|
||||
import { TransactionLinkRepository } from '@repository/TransactionLink'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { decimalSubtraction, decimalAddition } from './utilities'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
function isStringBoolean(value: string): boolean {
|
||||
const lowerValue = value.toLowerCase()
|
||||
@ -23,14 +25,26 @@ async function calculateBalance(
|
||||
amount: Decimal,
|
||||
time: Date,
|
||||
transactionLink?: dbTransactionLink | null,
|
||||
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
|
||||
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number }> {
|
||||
// negative or empty amount should not be allowed
|
||||
if (amount.lessThanOrEqualTo(0)) {
|
||||
logger.error(`Transaction amount must be greater than 0: ${amount}`)
|
||||
throw new Error('Transaction amount must be greater than 0')
|
||||
}
|
||||
|
||||
// check if user has prior transactions
|
||||
const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } })
|
||||
if (!lastTransaction) return null
|
||||
|
||||
if (!lastTransaction) {
|
||||
logger.error(`No prior transaction found for user with id: ${userId}`)
|
||||
throw new Error('User has not received any GDD yet')
|
||||
}
|
||||
|
||||
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
|
||||
|
||||
// TODO why we have to use toString() here?
|
||||
const balance = decay.balance.add(amount.toString())
|
||||
// new balance is the old balance minus the amount used
|
||||
const balance = decimalSubtraction(decay.balance, amount)
|
||||
|
||||
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
|
||||
const { sumHoldAvailableAmount } = await transactionLinkRepository.summary(userId, time)
|
||||
|
||||
@ -38,11 +52,16 @@ async function calculateBalance(
|
||||
// else we cannot redeem links which are more or equal to half of what an account actually owns
|
||||
const releasedLinkAmount = transactionLink ? transactionLink.holdAvailableAmount : new Decimal(0)
|
||||
|
||||
if (
|
||||
balance.minus(sumHoldAvailableAmount.toString()).plus(releasedLinkAmount.toString()).lessThan(0)
|
||||
) {
|
||||
return null
|
||||
const availableBalance = decimalSubtraction(balance, sumHoldAvailableAmount)
|
||||
|
||||
if (decimalAddition(availableBalance, releasedLinkAmount).lessThan(0)) {
|
||||
logger.error(
|
||||
`Not enough funds for a transaction of ${amount} GDD, user with id: ${userId} has only ${balance} GDD available`,
|
||||
)
|
||||
throw new Error('Not enough funds for transaction')
|
||||
}
|
||||
|
||||
logger.debug(`calculated Balance=${balance}`)
|
||||
return { balance, lastTransactionId: lastTransaction.id, decay }
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-database",
|
||||
"version": "1.13.1",
|
||||
"version": "1.13.3",
|
||||
"description": "Gradido Database Tool to execute database migrations",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/database",
|
||||
|
||||
@ -2,6 +2,18 @@
|
||||
|
||||
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.
|
||||
|
||||
## Ausbaustufen
|
||||
|
||||
Die beschriebenen Anforderungen werden in mehrere Ausbaustufen eingeteilt. Damit können nach und nach die Dialoge und Businesslogik schrittweise in verschiedene Releases gegossen und ausgeliefert werden.
|
||||
|
||||
### Ausbaustufe 1
|
||||
|
||||
Diese Ausbaustufe wird gezielt für die "Dokumenta" im Juni 2022 zusammengestellt. Details siehe weiter unten im speziellen Kapitel "Ausbaustufe 1".
|
||||
|
||||
### Ausbaustufe 2
|
||||
|
||||
Diese Ausbaustufe wird gezielt für die Anforderungen für das Medidationsportal von "Abraham" zusammegestellt. Details siehe weiter unten im speziellen Kapitel "Ausbaustufe 2".
|
||||
|
||||
## 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.
|
||||
@ -28,11 +40,11 @@ Der Gültigkeitsstart wird als Default mit dem aktuellen Erfassungszeitpunkt vor
|
||||
|
||||
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:
|
||||
|
||||
@ -44,11 +56,11 @@ Für die Schöpfung der Contribution können weitere Regeln definiert werden:
|
||||
|
||||

|
||||
|
||||
### Ausbaustufe-1:
|
||||
## Ausbaustufe-1:
|
||||
|
||||
Die Ausbaustufe-1 wird gezielt auf die Anforderungen der "Dokumenta" im Juni 2022 abgestimmt.
|
||||
Die Ausbaustufe-1 wird gezielt auf die Anforderungen der "Dokumenta" im Juni 2022 abgestimmt.
|
||||
|
||||
#### Contribution-Erfassungsdialog (Adminbereich)
|
||||
### Contribution-Erfassungsdialog (Adminbereich)
|
||||
|
||||
Es werden folgende Anforderungen an den Erfassungsdialog einer Contribution gestellt:
|
||||
|
||||
@ -64,14 +76,12 @@ Es werden folgende Anforderungen an den Erfassungsdialog einer Contribution gest
|
||||
| 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
|
||||
### 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.
|
||||
@ -91,6 +101,66 @@ Das Regelwerk in der Businesslogik wird gemäß der reduzierten Contribution-Att
|
||||
* 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
|
||||
|
||||
## Ausbaustufe-2
|
||||
|
||||
Die Ausbaustufe-2 wird gezielt auf die Anforderungen zur Anbindung des Meditationsportals von Abraham im Oktober 2022 abgestimmt.
|
||||
|
||||
### Contribution-Erfassungsdialog (Adminbereich)
|
||||
|
||||
Es werden folgende Anforderungen an den Erfassungsdialog einer Contribution gestellt:
|
||||
|
||||
| Attribut | Beschreibung |
|
||||
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| GültigBis | - das Datum, wie lange die Contribution gültig und damit einlösbar ist<br />- für diese Ausbaustufe soll ein offenes Ende möglich sein, daher bleibt dieses Attribut leer |
|
||||
| Zyklus | - Angabe wie häufig eine Contribution gutgeschrieben werden kann<br />- als Auswahlliste (Combobox) geplant, aber für diese Ausbaustufe nur mit dem Wert "täglich" vorbelegt |
|
||||
| Wiederholungen | - Anzahl an Wiederholungen pro Zyklus<br />- für diese Ausbaustufe wird der Wert "1" vorbelegt -> somit gilt: ein User kann diese Contribution nur 1x täglich einlösen |
|
||||
| alle weiteren noch nicht vorhandenen 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-2 und der inzwischen umgesetzten Ablösung des "EloPage Contribution Erfassungsprozesses" wird gemäß der Beschreibung aus dem Kapitel "Logischer Ablauf" die "automatic Confirmation and booking of Creations" sowie die interaktive Variante "interactive Confirmation and booking of Creations" mit berücksichtigt.
|
||||
|
||||
Das Regelwerk in der Businesslogik wird gemäß der noch nicht vollumfänglich geplanten Contribution-Attribute aus dem Erfassungsdialog, den vordefinierten Initialwerten und der daraus resultierenden Variantenvielfalt vereinfacht.
|
||||
|
||||
#### Kriterien "Meditationsportal (Abraham)"
|
||||
|
||||
* Es soll eine "GlobalMeditation"-Contribution nur im Admin-Bereich erfassbar sein und in der Datenbank als ContributionLink gespeichert werden.
|
||||
* Es wird ein offenes Ende als Gesamtlaufzeit dieser Contribution benötigt, was durch ein leeres GültigBis-Datum ausgedrückt bzw. erfasst werden soll.
|
||||
* Die "Meditationsportal"-Contribution kann von einem User maximal 1x täglich aktiviert werden. Dies wird über die Erfassung des Attributes "Zyklus" = täglich und des Attributes "Wiederholungen" = 1 ermöglicht.
|
||||
* Ein User kann mit diesem Link nur die Menge an GDDs schöpfen, die in der Contribution als "Betrag" festgelegt ist
|
||||
* Die "GlobalMeditation"-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 = "täglich" und einem Wiederholungswert = 1 darf der User den Betrag dieser Contribution nur einmal am Tag schöpfen. Es gibt keine Überprüfung eines zeitlichen Mindestabstandes zwischen zwei Schöpfungen an zwei aufeinanderfolgenden Tagen.
|
||||
* 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 mit automatischer Bestätigung der Contribution
|
||||
* die Schöpfungstransaktion schreibt den Betrag der Contribution dem Kontostand des Users gut
|
||||
|
||||
## Ausbaustufe-3
|
||||
|
||||
### Änderungen im Registrierungsprozess
|
||||
|
||||
Aktuell treten Probleme mit der Aktivierung des ContributionLinks während des Registrierungsprozesses auf. Sobald der User bei der Registrierung sein Konto zwar angelegt, aber die erhaltene Email-Confirmation nicht abgeschlossen und damit sein Konto noch nicht aktiviert hat, kann derzeit der Redeem-Link nicht als Transaktion durchgeführt werden. Die Gültigkeitsdauer des Redeemlink reicht meist nicht bis der User sein Konto aktiviert. Daher wird nun die Idee verfolgt die Einlösung des Redeemlinks schon während der Anlage des inaktiven Kontos als "pendingRedeem Contribution" anzulegen. Sobald dann der User sein Konto per Email-Confirmation aktiviert, soll die "pendingRedeem Contribution" automatisch zu einer Tranaktion überführt und der Betrag des Redeemlinks auf das Konto des Users gebucht werden.
|
||||
|
||||
Folgende Schritte und Änderungen sind dabei vorgesehen (siehe in der Grafik rechts im orange markierten Bereich im Vergleich zur Grafik im Kapitel "Logischer Ablauf"):
|
||||
|
||||

|
||||
|
||||
* Der User landet mit Aktivierung eines Redeem-Links wie bisher auf der Login/Registrierungsseite, wobei wie bisher schon der Redeemlink als Parameter in den Registrierungsprozess übergeben wird.
|
||||
* Mit der Anlage des neuen aber noch inaktiven User-Kontos und einer Übergabe eines Redeemlinks wird der Redeemlink zu einer "pendingRedeem Contribution" für den neuen User angelegt, aber noch nicht als Transaktion gebucht
|
||||
* nach Anlage des inaktiven User-Kontos und bevor die Confirmation-Email abgeschickt wird, erfolgt das Schreiben eines neuen Contribution-Eintrages mit den Daten des Redeem-Links.
|
||||
* Die neu angelegte Contribution wird im Status "pendingRedeem" gespeichert. Dieser neue Status ist notwendig, um im AdminInterface die normalen "pending Contributions" von den "pendingRedeem Contributions" zu unterscheiden. Denn der Admin soll zum Einen diese "pendingRedeem Contributions" weder bestätigen noch ablehnen können und zum Anderen sollen die "pendingRedeem Contributions" automatisiert bestätigt und gebucht werden können. Daher wird eine Unterscheidung zwischen den interaktiv angelegten Contributions im Status pending und den per Redeem-Link angelegten Contributions im Status pending benötigt.
|
||||
* Damit endet erst einmal die weitere Verarbeitung der Redeem-Link-Aktivierung
|
||||
* Mit Aktivierung des Links in der Email-Confirmation und damit der Aktivierung des User-Kontos erfolgt automatisch die Buchung der "pendingRedeem Contribution" und führt damit zur eigentlichen Buchung des Redeem-Betrages auf das User Konto.
|
||||
* mit Erhalt der Email-Confirmation Aktivierung wird das User-Konto aktiviert
|
||||
* Nach der Aktivierung des User-Kontos erfolgt eine Prüfung auf schon vorhandene "pendingRedeem Contributions" aus vorherigen Redeem-Link-Aktivierungen
|
||||
* Jede vorhandene "pendingRedeem Contribution" wird jetzt automatisch bestätigt und zu einer Transaktion überführt
|
||||
* Mit der bestätigten Contribution und daraus überführten Transaktion erhält der User eine Bestätigungsemail mit den Contribution spezifischen Daten.
|
||||
|
||||
## Datenbank-Modell
|
||||
|
||||
@ -100,34 +170,36 @@ Das nachfolgende Bild zeigt das Datenmodell vor der Einführung und Migration au
|
||||
|
||||

|
||||
|
||||
### Datenbank-Änderungen
|
||||
### Ausbaustufe-1
|
||||
|
||||
#### Datenbank-Änderungen
|
||||
|
||||
Die Datenbank wird in ihrer vollständigen Ausprägung trotz Ausbaustufe-1 wie folgt beschrieben umgesetzt.
|
||||
|
||||
#### neue Tabellen
|
||||
##### neue Tabellen
|
||||
|
||||
##### contribution_links - Tabelle
|
||||
###### 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) | | | |
|
||||
| name | varchar(100) | NOT NULL | | unique Name |
|
||||
| description | varchar(255) | | | |
|
||||
| valid_from | DATETIME | NOT NULL | NOW | |
|
||||
| valid_to | DATETIME | | NULL | |
|
||||
| amount | DECIMAL | NOT NULL | | |
|
||||
| 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 | |
|
||||
| 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
|
||||
###### contributions -Tabelle
|
||||
|
||||
| Name | Typ | Nullable | Default | Kommentar |
|
||||
| --------------------- | ------------ | -------- | -------------- | -------------------------------------------------------------------------------- |
|
||||
@ -145,9 +217,9 @@ Die Datenbank wird in ihrer vollständigen Ausprägung trotz Ausbaustufe-1 wie f
|
||||
| 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
|
||||
##### zu migrierende Tabellen
|
||||
|
||||
##### Tabelle admin_pending_creations
|
||||
###### Tabelle admin_pending_creations
|
||||
|
||||
Diese Tabelle wird im Rahmen dieses UseCase migriert in die neue Tabelle contributions...
|
||||
|
||||
@ -168,6 +240,18 @@ Diese Tabelle wird im Rahmen dieses UseCase migriert in die neue Tabelle contrib
|
||||
|
||||
...und kann nach Übernahme der Daten in die neue Tabelle gelöscht werden oder es erfolgen die Änderungen sofort auf der Ursprungstabelle.
|
||||
|
||||
### Zielmodell
|
||||
#### Zielmodell
|
||||
|
||||

|
||||
|
||||
### Ausbaustufe-2
|
||||
|
||||
Für die Ausbaustufe-2 sind keine Datenbank-Änderungen notwendig. Gemäß dem Zielmodell sind alle notwendigen Tabellen und Attribute schon vorhanden.
|
||||
|
||||
#### Zielmodell
|
||||
|
||||

|
||||
|
||||
### Ausbaustufe-3
|
||||
|
||||
Für die Ausbaustufe-3 dürften im Grunde ebenfalls keine zusätzlichen Datenbankänderungen notwendig sein. Denn für eine "pending Contribution" und deren Confirmation mit Tranaktionsüberführng sind ebenfalls schon alle Attribute vorhanden.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<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">
|
||||
<mxGraphModel dx="3699" 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"/>
|
||||
@ -183,31 +183,31 @@
|
||||
<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"/>
|
||||
<mxGeometry x="1560" y="592.5" width="380" height="85" 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"/>
|
||||
<mxGeometry x="1990" y="925" 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"/>
|
||||
<mxGeometry x="1250" y="605" 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">
|
||||
<mxCell id="122" value="" style="edgeStyle=none;html=1;fontSize=16;entryX=0.25;entryY=0;entryDx=0;entryDy=0;exitX=0.417;exitY=0.991;exitDx=0;exitDy=0;exitPerimeter=0;" 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"/>
|
||||
<mxGeometry x="1210" y="900" 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"/>
|
||||
<mxGeometry x="1560" y="910" 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">
|
||||
@ -317,15 +317,15 @@
|
||||
</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"/>
|
||||
<mxGeometry x="2020" y="310" width="310" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="118" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" target="113" edge="1">
|
||||
<mxCell id="118" value="" style="edgeStyle=none;html=1;fontSize=16;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" parent="1" target="113" edge="1" source="111">
|
||||
<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"/>
|
||||
<mxGeometry x="1250" y="450" 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"/>
|
||||
@ -334,10 +334,10 @@
|
||||
<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"/>
|
||||
<mxGeometry x="1250" y="530" 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"/>
|
||||
<mxGeometry x="1990" y="525" 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"/>
|
||||
@ -351,7 +351,7 @@
|
||||
<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">
|
||||
<mxCell id="129" value="" style="edgeStyle=none;html=1;fontSize=20;strokeWidth=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="127" target="131" 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">
|
||||
@ -359,8 +359,76 @@
|
||||
<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 id="127" value="Contribution <br>und Regel valide?<br>" style="rhombus;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="685" width="240" height="75" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="132" style="edgeStyle=none;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="131" target="69">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="1660" y="780"/>
|
||||
<mxPoint x="1330" y="780"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="134" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="131" target="133">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="135" value="Nein" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;" vertex="1" connectable="0" parent="134">
|
||||
<mxGeometry x="-0.4968" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="131" value="user Konto<br>active?" style="rhombus;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1540" y="685" width="240" height="75" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="137" value="" style="edgeStyle=none;html=1;fontSize=16;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="133" target="138">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="2210" y="722.5" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="133" value="erzeuge aus ContributionLink zu angemeldetem User eine pending Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" vertex="1" parent="1">
|
||||
<mxGeometry x="1830" y="685" width="300" height="75" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="138" value="Ende Redeem-Aktivierung" style="ellipse;whiteSpace=wrap;html=1;fontSize=20;rounded=1;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" vertex="1" parent="1">
|
||||
<mxGeometry x="2170" y="677.5" width="110" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="141" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" edge="1" parent="1" source="139" target="140">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="139" value="Start Konto-<br>bestätigung" style="ellipse;whiteSpace=wrap;html=1;fontSize=20;rounded=1;fillColor=#f5f5f5;strokeColor=#666666;gradientColor=#b3b3b3;" vertex="1" parent="1">
|
||||
<mxGeometry x="2170" y="805" width="110" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="143" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" edge="1" parent="1" source="140" target="142">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="140" value="User führt <br>Email-Bestätigung aus" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
|
||||
<mxGeometry x="1930" y="825" width="210" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="145" value="" style="edgeStyle=none;html=1;fontSize=16;" edge="1" parent="1" source="142" target="144">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="142" value="Konto wird active" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" vertex="1" parent="1">
|
||||
<mxGeometry x="1720" y="827.5" width="180" height="47.5" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="147" value="" style="edgeStyle=none;html=1;fontSize=16;" edge="1" parent="1" source="144" target="146">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="144" value="für alle pending Contributions" style="whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1540" y="825" width="150" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="149" style="edgeStyle=none;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;fontSize=16;" edge="1" parent="1" source="146" target="71">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="150" style="edgeStyle=none;html=1;entryX=0.896;entryY=0.01;entryDx=0;entryDy=0;entryPerimeter=0;fontSize=16;exitX=0.642;exitY=0.988;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="146" target="79">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="146" value="erzeuge bestätigte Contribution" style="whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1390" y="810" width="120" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="151" value="" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;opacity=30;fillColor=#ffcd28;gradientColor=#ffa500;strokeColor=#d79b00;" vertex="1" parent="1">
|
||||
<mxGeometry x="1180" y="680" width="1140" height="220" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 536 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bootstrap-vue-gradido-wallet",
|
||||
"version": "1.13.1",
|
||||
"version": "1.13.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node run/server.js",
|
||||
|
||||
@ -33,7 +33,9 @@ $indigo: #5603ad !default;
|
||||
$purple: #8965e0 !default;
|
||||
$pink: #f3a4b5 !default;
|
||||
$red: #f5365c !default;
|
||||
$orange: #fb6340 !default;
|
||||
|
||||
// $orange: #fb6340 !default;
|
||||
$orange: #8c0505 !default;
|
||||
$yellow: #ffd600 !default;
|
||||
$green: #2dce89 !default;
|
||||
$teal: #11cdef !default;
|
||||
|
||||
@ -5,15 +5,23 @@
|
||||
<contribution-messages-list-item :message="message" />
|
||||
</div>
|
||||
</b-container>
|
||||
<contribution-messages-formular
|
||||
v-if="['PENDING', 'IN_PROGRESS'].includes(state)"
|
||||
:contributionId="contributionId"
|
||||
@get-list-contribution-messages="getListContributionMessages"
|
||||
@update-state="updateState"
|
||||
/>
|
||||
<div v-b-toggle="'collapse' + String(contributionId)" class="text-center pointer h2">
|
||||
<b-icon icon="arrow-up-short"></b-icon>
|
||||
{{ $t('form.close') }}
|
||||
<b-container>
|
||||
<contribution-messages-formular
|
||||
v-if="['PENDING', 'IN_PROGRESS'].includes(state)"
|
||||
:contributionId="contributionId"
|
||||
@get-list-contribution-messages="getListContributionMessages"
|
||||
@update-state="updateState"
|
||||
/>
|
||||
</b-container>
|
||||
|
||||
<div
|
||||
v-b-toggle="'collapse' + String(contributionId)"
|
||||
class="text-center pointer h2 clearboth pt-1"
|
||||
>
|
||||
<b-button variant="outline-primary" block class="mt-4">
|
||||
<b-icon icon="arrow-up-short"></b-icon>
|
||||
{{ $t('form.close') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -55,4 +63,7 @@ export default {
|
||||
.temp-message {
|
||||
margin-top: 50px;
|
||||
}
|
||||
.clearboth {
|
||||
clear: both;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -175,4 +175,68 @@ describe('ContributionMessagesListItem', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('links in contribtion message', () => {
|
||||
const propsData = {
|
||||
message: {
|
||||
id: 111,
|
||||
message: 'Lorem ipsum?',
|
||||
createdAt: '2022-08-29T12:23:27.000Z',
|
||||
updatedAt: null,
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Peter',
|
||||
userLastName: 'Lustig',
|
||||
userId: 107,
|
||||
__typename: 'ContributionMessage',
|
||||
},
|
||||
}
|
||||
|
||||
const ModeratorItemWrapper = () => {
|
||||
return mount(ContributionMessagesListItem, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
let messageField
|
||||
|
||||
describe('message of only one link', () => {
|
||||
beforeEach(() => {
|
||||
propsData.message.message = 'https://gradido.net/de/'
|
||||
wrapper = ModeratorItemWrapper()
|
||||
messageField = wrapper.find('div.is-not-moderator.text-right > div:nth-child(4)')
|
||||
})
|
||||
|
||||
it('contains the link as text', () => {
|
||||
expect(messageField.text()).toBe('https://gradido.net/de/')
|
||||
})
|
||||
|
||||
it('contains a link to the given address', () => {
|
||||
expect(messageField.find('a').attributes('href')).toBe('https://gradido.net/de/')
|
||||
})
|
||||
})
|
||||
|
||||
describe('message with text and two links', () => {
|
||||
beforeEach(() => {
|
||||
propsData.message.message = `Here you find all you need to know about Gradido: https://gradido.net/de/
|
||||
and here is the link to the repository: https://github.com/gradido/gradido`
|
||||
wrapper = ModeratorItemWrapper()
|
||||
messageField = wrapper.find('div.is-not-moderator.text-right > div:nth-child(4)')
|
||||
})
|
||||
|
||||
it('contains the whole text', () => {
|
||||
expect(messageField.text())
|
||||
.toBe(`Here you find all you need to know about Gradido: https://gradido.net/de/
|
||||
and here is the link to the repository: https://github.com/gradido/gradido`)
|
||||
})
|
||||
|
||||
it('contains the two links', () => {
|
||||
expect(messageField.findAll('a').at(0).attributes('href')).toBe('https://gradido.net/de/')
|
||||
expect(messageField.findAll('a').at(1).attributes('href')).toBe(
|
||||
'https://github.com/gradido/gradido',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,24 +1,29 @@
|
||||
<template>
|
||||
<div class="contribution-messages-list-item">
|
||||
<div v-if="isNotModerator" class="is-not-moderator text-right">
|
||||
<b-avatar :text="initialLetters" variant="info"></b-avatar>
|
||||
<b-avatar variant="info"></b-avatar>
|
||||
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
|
||||
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
|
||||
<div class="mt-2">{{ message.message }}</div>
|
||||
<linkify-message :message="message.message"></linkify-message>
|
||||
</div>
|
||||
<div v-else class="is-moderator text-left">
|
||||
<b-avatar square :text="initialLetters" variant="warning"></b-avatar>
|
||||
<b-avatar square variant="warning"></b-avatar>
|
||||
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
|
||||
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
|
||||
<small class="ml-4 text-success">{{ $t('community.moderator') }}</small>
|
||||
<div class="mt-2">{{ message.message }}</div>
|
||||
<linkify-message :message="message.message"></linkify-message>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LinkifyMessage from '@/components/ContributionMessages/LinkifyMessage.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContributionMessagesListItem',
|
||||
components: {
|
||||
LinkifyMessage,
|
||||
},
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="mt-2">
|
||||
<span v-for="({ type, text }, index) in linkifiedMessage" :key="index">
|
||||
<b-link v-if="type === 'link'" :href="text" target="_blank">{{ text }}</b-link>
|
||||
<span v-else>{{ text }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const LINK_REGEX_PATTERN = /(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*))/i
|
||||
|
||||
export default {
|
||||
name: 'LinkifyMessage',
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
linkifiedMessage() {
|
||||
const linkified = []
|
||||
let string = this.message
|
||||
let match
|
||||
while ((match = string.match(LINK_REGEX_PATTERN))) {
|
||||
if (match.index > 0)
|
||||
linkified.push({ type: 'text', text: string.substring(0, match.index) })
|
||||
linkified.push({ type: 'link', text: match[0] })
|
||||
string = string.substring(match.index + match[0].length)
|
||||
}
|
||||
if (string.length > 0) linkified.push({ type: 'text', text: string })
|
||||
return linkified
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -198,6 +198,75 @@ describe('ContributionForm', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('date with the 31st day of the month', () => {
|
||||
describe('same month', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({
|
||||
maximalDate: new Date('2022-10-31T00:00:00.000Z'),
|
||||
form: { date: new Date('2022-10-31T00:00:00.000Z') },
|
||||
})
|
||||
})
|
||||
|
||||
describe('minimalDate', () => {
|
||||
it('has "2022-09-01T00:00:00.000Z"', () => {
|
||||
expect(wrapper.vm.minimalDate.toISOString()).toBe('2022-09-01T00:00:00.000Z')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isThisMonth', () => {
|
||||
it('has true', () => {
|
||||
expect(wrapper.vm.isThisMonth).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('date with the 28th day of the month', () => {
|
||||
describe('same month', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({
|
||||
maximalDate: new Date('2023-02-28T00:00:00.000Z'),
|
||||
form: { date: new Date('2023-02-28T00:00:00.000Z') },
|
||||
})
|
||||
})
|
||||
|
||||
describe('minimalDate', () => {
|
||||
it('has "2023-01-01T00:00:00.000Z"', () => {
|
||||
expect(wrapper.vm.minimalDate.toISOString()).toBe('2023-01-01T00:00:00.000Z')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isThisMonth', () => {
|
||||
it('has true', () => {
|
||||
expect(wrapper.vm.isThisMonth).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('date with 29.02.2024 leap year', () => {
|
||||
describe('same month', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({
|
||||
maximalDate: new Date('2024-02-29T00:00:00.000Z'),
|
||||
form: { date: new Date('2024-02-29T00:00:00.000Z') },
|
||||
})
|
||||
})
|
||||
|
||||
describe('minimalDate', () => {
|
||||
it('has "2024-01-01T00:00:00.000Z"', () => {
|
||||
expect(wrapper.vm.minimalDate.toISOString()).toBe('2024-01-01T00:00:00.000Z')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isThisMonth', () => {
|
||||
it('has true', () => {
|
||||
expect(wrapper.vm.isThisMonth).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('set contrubtion', () => {
|
||||
|
||||
@ -88,6 +88,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
const PATTERN_NON_DIGIT = /\D/g
|
||||
|
||||
export default {
|
||||
name: 'ContributionForm',
|
||||
props: {
|
||||
@ -104,10 +106,10 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
numberFormat(value) {
|
||||
return value.replace(/\D/g, '')
|
||||
return value.replace(PATTERN_NON_DIGIT, '')
|
||||
},
|
||||
submit() {
|
||||
this.form.amount = this.numberFormat(this.form.amount)
|
||||
this.form.amount = this.form.amount.replace(PATTERN_NON_DIGIT, '')
|
||||
// spreading is needed for testing
|
||||
this.$emit(this.form.id ? 'update-contribution' : 'set-contribution', { ...this.form })
|
||||
this.reset()
|
||||
@ -129,10 +131,8 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
minimalDate() {
|
||||
// sets the date to the 1st of the previous month
|
||||
let date = new Date(this.maximalDate) // has to be a new object, because of 'setMonth' changes the objects date
|
||||
date = new Date(date.setMonth(date.getMonth() - 1))
|
||||
return new Date(date.getFullYear(), date.getMonth(), 1)
|
||||
const date = new Date(this.maximalDate)
|
||||
return new Date(date.setMonth(date.getMonth() - 1, 1))
|
||||
},
|
||||
disabled() {
|
||||
return (
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
"submitContribution": "Beitrag einreichen",
|
||||
"switch-to-this-community": "zu dieser Gemeinschaft wechseln"
|
||||
},
|
||||
"contact": "Kontakt",
|
||||
"contribution": {
|
||||
"activity": "Tätigkeit",
|
||||
"alert": {
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
"submitContribution": "Submit contribution",
|
||||
"switch-to-this-community": "Switch to this community"
|
||||
},
|
||||
"contact": "Contact",
|
||||
"contribution": {
|
||||
"activity": "Activity",
|
||||
"alert": {
|
||||
|
||||
@ -231,8 +231,6 @@ export default {
|
||||
this.items = listContributions.contributionList
|
||||
if (this.items.find((item) => item.state === 'IN_PROGRESS')) {
|
||||
this.tabIndex = 1
|
||||
} else {
|
||||
this.tabIndex = 0
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
|
||||
@ -44,6 +44,9 @@
|
||||
{{ item.firstName }} {{ item.lastName }}
|
||||
</li>
|
||||
</ul>
|
||||
</b-container>
|
||||
<b-container>
|
||||
<div class="h3">{{ $t('contact') }}</div>
|
||||
<b-link href="mailto: abc@example.com">{{ supportMail }}</b-link>
|
||||
</b-container>
|
||||
<!--
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido",
|
||||
"version": "1.13.1",
|
||||
"version": "1.13.3",
|
||||
"description": "Gradido",
|
||||
"main": "index.js",
|
||||
"repository": "git@github.com:gradido/gradido.git",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user