mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 1973-List-open-contribution-links-in-the-wallet
This commit is contained in:
commit
baa7f8df4a
@ -1,3 +0,0 @@
|
||||
4ƒk׀ךֻ1°,•<EFBFBD>fעbלAלqִ¬cי<EFBFBD>#¾›צ¾ר#s’8-ְ1‰&»;נצד"¢פשל7€d¥jM?‘bljfB¼ƒqֱ=
|
||||
<EFBFBD>ײmyפ¿
|
||||
vת·´V
|
||||
@ -1,32 +0,0 @@
|
||||
{
|
||||
"folders": {},
|
||||
"connections": {
|
||||
"mariaDB-1813fbbc7bc-107c0b3aeaeb91ab": {
|
||||
"provider": "mysql",
|
||||
"driver": "mariaDB",
|
||||
"name": "gradido",
|
||||
"save-password": true,
|
||||
"read-only": false,
|
||||
"configuration": {
|
||||
"host": "localhost",
|
||||
"port": "3306",
|
||||
"url": "jdbc:mariadb://localhost:3306/",
|
||||
"home": "mysql_client",
|
||||
"type": "dev",
|
||||
"auth-model": "native",
|
||||
"handlers": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"connection-types": {
|
||||
"dev": {
|
||||
"name": "Development",
|
||||
"color": "255,255,255",
|
||||
"description": "Regular development database",
|
||||
"auto-commit": true,
|
||||
"confirm-execute": false,
|
||||
"confirm-data-change": false,
|
||||
"auto-close-transactions": false
|
||||
}
|
||||
}
|
||||
}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
.dbeaver
|
||||
.project
|
||||
*.log
|
||||
*.bak
|
||||
/node_modules/*
|
||||
|
||||
18
.project
18
.project
@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>Gradido</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.wst.validation.validationbuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.jkiss.dbeaver.DBeaverNature</nature>
|
||||
<nature>org.eclipse.wst.jsdt.core.jsNature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
80
CHANGELOG.md
80
CHANGELOG.md
@ -4,8 +4,88 @@ 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.11.0](https://github.com/gradido/gradido/compare/1.10.1...1.11.0)
|
||||
|
||||
- Fix navbar community item [`#2102`](https://github.com/gradido/gradido/pull/2102)
|
||||
- Add validation date info to copied text after transaction link creation [`#2101`](https://github.com/gradido/gradido/pull/2101)
|
||||
- Remove member area from menu (desktop and mobile), when user has no elopage account [`#2099`](https://github.com/gradido/gradido/pull/2099)
|
||||
- fix: Use Inner Join for Contribution and User [`#2100`](https://github.com/gradido/gradido/pull/2100)
|
||||
- Enable copying the link, username, amount, and memo text after transaction link creation [`#2098`](https://github.com/gradido/gradido/pull/2098)
|
||||
- feat: Insert Missing Contributions Migration [`#2053`](https://github.com/gradido/gradido/pull/2053)
|
||||
- [Bug] Wallet improvments for Contributions [`#2090`](https://github.com/gradido/gradido/pull/2090)
|
||||
- [Fix] Add createdAt & contributionDate to ContributionListItems [`#2093`](https://github.com/gradido/gradido/pull/2093)
|
||||
- fix: 🍰 Reset Amount In Contribution Form And Write A Test [`#2086`](https://github.com/gradido/gradido/pull/2086)
|
||||
- [Feat] Replace logic to validation-provider. [`#2088`](https://github.com/gradido/gradido/pull/2088)
|
||||
- fix: Add Confirm Dialog on Delete Contribution [`#2087`](https://github.com/gradido/gradido/pull/2087)
|
||||
- fix: Admin Cannot Edit User Contribution [`#2085`](https://github.com/gradido/gradido/pull/2085)
|
||||
- fix: Update contribution_date when Moved by Seed [`#2083`](https://github.com/gradido/gradido/pull/2083)
|
||||
- chore: 🍰 Provide Volume For Backend Log-Files In Docker [`#2067`](https://github.com/gradido/gradido/pull/2067)
|
||||
- feat: 🍰 Community Contribution Site And Form [`#2042`](https://github.com/gradido/gradido/pull/2042)
|
||||
- [Refactor] Move MEMO_MIN_CHARS and MEMO_MAX_CHARS to const file [`#2082`](https://github.com/gradido/gradido/pull/2082)
|
||||
- Fix: Test memo length on createContribution & updateContribution [`#2080`](https://github.com/gradido/gradido/pull/2080)
|
||||
- chore: 🍰 Change `image` Entries In Docker Compose Files And Get Apple M1 Running [`#2050`](https://github.com/gradido/gradido/pull/2050)
|
||||
- fix: Windows 0D 0A Linebreaks to Unix 0A [`#2064`](https://github.com/gradido/gradido/pull/2064)
|
||||
- Add contributionDate to the Contribution object. [`#2066`](https://github.com/gradido/gradido/pull/2066)
|
||||
- fix: Add Contributions to User [`#2062`](https://github.com/gradido/gradido/pull/2062)
|
||||
- Feat: ContributionResolver - delete mutation [`#2035`](https://github.com/gradido/gradido/pull/2035)
|
||||
- Fix: Add count to list contributions [`#2061`](https://github.com/gradido/gradido/pull/2061)
|
||||
- docu: Explain how `.env` Files are Working [`#2022`](https://github.com/gradido/gradido/pull/2022)
|
||||
- [WIP] 1794 feature event protocol 1 implement the basics of the business event protocol [`#1997`](https://github.com/gradido/gradido/pull/1997)
|
||||
- docs: 🍰 Document The Setup Of The GraphQL Playground [`#2060`](https://github.com/gradido/gradido/pull/2060)
|
||||
- Feat: List all contribution [`#2057`](https://github.com/gradido/gradido/pull/2057)
|
||||
- feat: Do not log IntrospectionQuery from Query Browser [`#2059`](https://github.com/gradido/gradido/pull/2059)
|
||||
- Feat: Add confirmedBy and confirmedAt for the contribution query. [`#2052`](https://github.com/gradido/gradido/pull/2052)
|
||||
- Add open creations to webapp [`#2048`](https://github.com/gradido/gradido/pull/2048)
|
||||
- Prevent session expiration modal from displaying negative seconds, when session is expired for more than 0 seconds [`#2054`](https://github.com/gradido/gradido/pull/2054)
|
||||
- feat: mutation contribution update [`#2032`](https://github.com/gradido/gradido/pull/2032)
|
||||
- feat: Login Returns Open Creations for User [`#2046`](https://github.com/gradido/gradido/pull/2046)
|
||||
- Migrate transaction to valid dataset for gradido node [`#2029`](https://github.com/gradido/gradido/pull/2029)
|
||||
- feat: implement contribution list query [`#2031`](https://github.com/gradido/gradido/pull/2031)
|
||||
- add code for moving user creation date if transaction before exist [`#2034`](https://github.com/gradido/gradido/pull/2034)
|
||||
- change text from page [`#2037`](https://github.com/gradido/gradido/pull/2037)
|
||||
- Transaction link: copy link, text and more [`#2030`](https://github.com/gradido/gradido/pull/2030)
|
||||
- change welcome in community text [`#2025`](https://github.com/gradido/gradido/pull/2025)
|
||||
- changed link color in navbar and language switch [`#2024`](https://github.com/gradido/gradido/pull/2024)
|
||||
- feat: ContributionResolver - createContribution [`#2009`](https://github.com/gradido/gradido/pull/2009)
|
||||
|
||||
#### [1.10.1](https://github.com/gradido/gradido/compare/1.10.0...1.10.1)
|
||||
|
||||
> 30 June 2022
|
||||
|
||||
- release: 1.10.1 [`#2021`](https://github.com/gradido/gradido/pull/2021)
|
||||
- automatic session logout with info modal [`#2001`](https://github.com/gradido/gradido/pull/2001)
|
||||
- 1910 separate text for the slideshow images. [`#1998`](https://github.com/gradido/gradido/pull/1998)
|
||||
- Origin/1921 additional parameter checks for createContributionLinks [`#1996`](https://github.com/gradido/gradido/pull/1996)
|
||||
- add missing locales [`#1999`](https://github.com/gradido/gradido/pull/1999)
|
||||
- 1906 feature concept for gdd creation per linkqr code [`#1907`](https://github.com/gradido/gradido/pull/1907)
|
||||
- refactor: 🍰 Not Throwing An Error When Register With Existing Email [`#1962`](https://github.com/gradido/gradido/pull/1962)
|
||||
- feat: 🍰 Set Role In Admin Interface [`#1974`](https://github.com/gradido/gradido/pull/1974)
|
||||
- refactor mobile style step 1 [`#1977`](https://github.com/gradido/gradido/pull/1977)
|
||||
- changed mobil stage picture [`#1995`](https://github.com/gradido/gradido/pull/1995)
|
||||
|
||||
#### [1.10.0](https://github.com/gradido/gradido/compare/1.9.0...1.10.0)
|
||||
|
||||
> 17 June 2022
|
||||
|
||||
- release: v1.10.0 [`#1993`](https://github.com/gradido/gradido/pull/1993)
|
||||
- frontend redeem contribution link [`#1988`](https://github.com/gradido/gradido/pull/1988)
|
||||
- change new start picture [`#1990`](https://github.com/gradido/gradido/pull/1990)
|
||||
- feat: Redeem Contribution Link [`#1987`](https://github.com/gradido/gradido/pull/1987)
|
||||
- fix: Max Amount on Slider for Edit Contribution [`#1986`](https://github.com/gradido/gradido/pull/1986)
|
||||
- CRUD contribution link admin interface [`#1981`](https://github.com/gradido/gradido/pull/1981)
|
||||
- fix: `.env` log level for apollo and backend category [`#1967`](https://github.com/gradido/gradido/pull/1967)
|
||||
- refactor: Admin Pending Creations Table to Contributions Table [`#1949`](https://github.com/gradido/gradido/pull/1949)
|
||||
- devops: Update Browser List for Unit Tests as Recomended [`#1984`](https://github.com/gradido/gradido/pull/1984)
|
||||
- feat: CRUD for Contribution Links in Admin Resolver [`#1979`](https://github.com/gradido/gradido/pull/1979)
|
||||
- 1920 feature create contribution link table [`#1957`](https://github.com/gradido/gradido/pull/1957)
|
||||
- refactor: 🍰 Delete `user_setting` Table From DB [`#1960`](https://github.com/gradido/gradido/pull/1960)
|
||||
- locales link german, english navbar [`#1969`](https://github.com/gradido/gradido/pull/1969)
|
||||
|
||||
#### [1.9.0](https://github.com/gradido/gradido/compare/1.8.3...1.9.0)
|
||||
|
||||
> 2 June 2022
|
||||
|
||||
- devops: Release Version 1.9.0 [`#1968`](https://github.com/gradido/gradido/pull/1968)
|
||||
- refactor: 🍰 Refactor To `filters` Object And Rename Filters Properties [`#1914`](https://github.com/gradido/gradido/pull/1914)
|
||||
- refactor register button position [`#1964`](https://github.com/gradido/gradido/pull/1964)
|
||||
- fixed redeem link is mobile start false [`#1958`](https://github.com/gradido/gradido/pull/1958)
|
||||
|
||||
44
DOCKER_MORE_CLOSELY.md
Normal file
44
DOCKER_MORE_CLOSELY.md
Normal file
@ -0,0 +1,44 @@
|
||||
# Docker More Closely
|
||||
|
||||
## Apple M1 Platform
|
||||
|
||||
***Attention:** For using Docker commands in Apple M1 environments!*
|
||||
|
||||
### Enviroment Variable For Apple M1 Platform
|
||||
|
||||
To set the Docker platform environment variable in your terminal tab, run:
|
||||
|
||||
```bash
|
||||
# set env variable for your shell
|
||||
$ export DOCKER_DEFAULT_PLATFORM=linux/amd64
|
||||
```
|
||||
|
||||
### Docker Compose Override File For Apple M1 Platform
|
||||
|
||||
For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform:
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
|
||||
# for development
|
||||
$ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.apple-m1.override.yml up
|
||||
|
||||
# for production
|
||||
$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up
|
||||
```
|
||||
|
||||
## Analysing Docker Builds
|
||||
|
||||
To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it!
|
||||
|
||||
The `dive build` command is exactly the right one to fulfill what we are looking for.
|
||||
We can use it just like the `docker build` command and get an analysis afterwards.
|
||||
|
||||
So, in our main folder, we use it in the following way:
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ dive build --target <layer-name> -t "gradido/<app-name>:local-<layer-name>" <app-folder-name-or-dot>/
|
||||
```
|
||||
|
||||
For the specific applications, see our [publish.yml](.github/workflows/publish.yml).
|
||||
13
README.md
13
README.md
@ -121,6 +121,19 @@ After generating a new version you should commit the changes. This will be the C
|
||||
|
||||
Note: The Changelog will be regenerated with all tags on release on the external builder tool, but will not be checked in there. The Changelog on the github release will therefore always be correct, on the repo it might be incorrect due to missing tags when executing the `yarn release` command.
|
||||
|
||||
## How the different .env work on deploy
|
||||
|
||||
Each component (frontend, admin, backend and database) has its own `.env` file. When running in development with docker and nginx you usually do not have to care about the `.env`. The defaults are set by the respective config file, found in the `src/config/` folder of each component. But if you have a local `.env`, the defaults set in the config are overwritten by the `.env`. If you do not use docker, you need the `.env` in the frontend and admin interface because nginx is not running in order to find the backend.
|
||||
|
||||
Each component has a `.env.dist` file. This file contains all environment variables used by the component and can be used as pattern. If you want to use a local `.env`, copy the `.env.dist` and adjust the variables accordingly.
|
||||
|
||||
Each component has a `.env.template` file. These files are very important on deploy.
|
||||
|
||||
There is one `.env.dist` in the `deployment/bare_metal/` folder. This `.env.dist` contains all variables used by the components, e.g. unites all `.env.dist` from the components. On deploy, we copy this `.env.dist` to `.env` and set all variables in this new file. The deploy script loads this variables and provides them by the `.env.templates` of each component, creating an `.env` for each component (see in `deployment/bare_metal/start.sh` the `envsubst`).
|
||||
|
||||
To avoid forgetting to update an existing `.env` in the `deployment/bare_metal/` folder when deploying, we have an environment version variable inside the codebase of each component. You should update this version, when environment variables must be changed or added on deploy. The code checks, that the environement version provided by the `.env` is the one expected by the codebase.
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Issue | Solution | Description |
|
||||
|
||||
@ -22,7 +22,7 @@ module.exports = {
|
||||
'^.+\\.(js|jsx)?$': 'babel-jest',
|
||||
'<rootDir>/node_modules/vee-validate/dist/rules': 'babel-jest',
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.js'],
|
||||
setupFiles: ['<rootDir>/test/testSetup.js', 'jest-canvas-mock'],
|
||||
testMatch: ['**/?(*.)+(spec|test).js?(x)'],
|
||||
// snapshotSerializers: ['jest-serializer-vue'],
|
||||
transformIgnorePatterns: ['<rootDir>/node_modules/(?!vee-validate/dist/rules)'],
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administraion Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Moriz Wahl",
|
||||
"version": "1.9.0",
|
||||
"version": "1.11.0",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
@ -38,7 +38,9 @@
|
||||
"graphql": "^15.6.1",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "26.6.3",
|
||||
"jest-canvas-mock": "^2.3.1",
|
||||
"portal-vue": "^2.1.7",
|
||||
"qrcanvas-vue": "2.1.1",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"stats-webpack-plugin": "^0.7.0",
|
||||
"vue": "^2.6.11",
|
||||
|
||||
BIN
admin/public/img/gdd-coin.png
Normal file
BIN
admin/public/img/gdd-coin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
254
admin/src/components/ChangeUserRoleFormular.spec.js
Normal file
254
admin/src/components/ChangeUserRoleFormular.spec.js
Normal file
@ -0,0 +1,254 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ChangeUserRoleFormular from './ChangeUserRoleFormular.vue'
|
||||
import { setUserRole } from '../graphql/setUserRole'
|
||||
import { toastSuccessSpy, toastErrorSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
setUserRole: null,
|
||||
},
|
||||
})
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
mutate: apolloMutateMock,
|
||||
},
|
||||
$store: {
|
||||
state: {
|
||||
moderator: {
|
||||
id: 0,
|
||||
name: 'test moderator',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
let propsData
|
||||
let wrapper
|
||||
|
||||
describe('ChangeUserRoleFormular', () => {
|
||||
const Wrapper = () => {
|
||||
return mount(ChangeUserRoleFormular, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('DOM has', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
item: {
|
||||
userId: 1,
|
||||
isAdmin: null,
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.delete-user-formular', () => {
|
||||
expect(wrapper.find('.change-user-role-formular').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('change own role', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
item: {
|
||||
userId: 0,
|
||||
isAdmin: null,
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has the text that you cannot change own role', () => {
|
||||
expect(wrapper.text()).toContain('userRole.notChangeYourSelf')
|
||||
})
|
||||
|
||||
it('has role select disabled', () => {
|
||||
expect(wrapper.find('select[disabled="disabled"]').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('change others role', () => {
|
||||
let rolesToSelect
|
||||
|
||||
describe('general', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
item: {
|
||||
userId: 1,
|
||||
isAdmin: null,
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
rolesToSelect = wrapper.find('select.role-select').findAll('option')
|
||||
})
|
||||
|
||||
it('has no text that you cannot change own role', () => {
|
||||
expect(wrapper.text()).not.toContain('userRole.notChangeYourSelf')
|
||||
})
|
||||
|
||||
it('has the select label', () => {
|
||||
expect(wrapper.text()).toContain('userRole.selectLabel')
|
||||
})
|
||||
|
||||
it('has a select', () => {
|
||||
expect(wrapper.find('select.role-select').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has role select enabled', () => {
|
||||
expect(wrapper.find('select.role-select[disabled="disabled"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
describe('on API error', () => {
|
||||
beforeEach(() => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
rolesToSelect.at(1).setSelected()
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user is usual user', () => {
|
||||
beforeEach(() => {
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
setUserRole: new Date(),
|
||||
},
|
||||
})
|
||||
propsData = {
|
||||
item: {
|
||||
userId: 1,
|
||||
isAdmin: null,
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
rolesToSelect = wrapper.find('select.role-select').findAll('option')
|
||||
})
|
||||
|
||||
it('has selected option set to "usual user"', () => {
|
||||
expect(wrapper.find('select.role-select').element.value).toBe('user')
|
||||
})
|
||||
|
||||
describe('change select to', () => {
|
||||
describe('same role', () => {
|
||||
it('does not call the API', () => {
|
||||
rolesToSelect.at(0).setSelected()
|
||||
expect(apolloMutateMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('new role', () => {
|
||||
beforeEach(() => {
|
||||
rolesToSelect.at(1).setSelected()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: setUserRole,
|
||||
variables: {
|
||||
userId: 1,
|
||||
isAdmin: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits "updateIsAdmin"', () => {
|
||||
expect(wrapper.emitted('updateIsAdmin')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
isAdmin: expect.any(Date),
|
||||
},
|
||||
]),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('toasts success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user is admin', () => {
|
||||
beforeEach(() => {
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
setUserRole: null,
|
||||
},
|
||||
})
|
||||
propsData = {
|
||||
item: {
|
||||
userId: 1,
|
||||
isAdmin: new Date(),
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
rolesToSelect = wrapper.find('select.role-select').findAll('option')
|
||||
})
|
||||
|
||||
it('has selected option set to "admin"', () => {
|
||||
expect(wrapper.find('select.role-select').element.value).toBe('admin')
|
||||
})
|
||||
|
||||
describe('change select to', () => {
|
||||
describe('same role', () => {
|
||||
it('does not call the API', () => {
|
||||
rolesToSelect.at(1).setSelected()
|
||||
expect(apolloMutateMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('new role', () => {
|
||||
beforeEach(() => {
|
||||
rolesToSelect.at(0).setSelected()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: setUserRole,
|
||||
variables: {
|
||||
userId: 1,
|
||||
isAdmin: false,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits "updateIsAdmin"', () => {
|
||||
expect(wrapper.emitted('updateIsAdmin')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
isAdmin: null,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('toasts success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
89
admin/src/components/ChangeUserRoleFormular.vue
Normal file
89
admin/src/components/ChangeUserRoleFormular.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="change-user-role-formular">
|
||||
<div class="shadow p-3 mb-5 bg-white rounded">
|
||||
<div v-if="item.userId === $store.state.moderator.id" class="m-3 mb-4">
|
||||
{{ $t('userRole.notChangeYourSelf') }}
|
||||
</div>
|
||||
<div class="m-3">
|
||||
<label for="role" class="mr-3">{{ $t('userRole.selectLabel') }}</label>
|
||||
<b-form-select
|
||||
class="role-select"
|
||||
v-model="roleSelected"
|
||||
:options="roles"
|
||||
:disabled="item.userId === $store.state.moderator.id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { setUserRole } from '../graphql/setUserRole'
|
||||
|
||||
const rolesValues = {
|
||||
admin: 'admin',
|
||||
user: 'user',
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'ChangeUserRoleFormular',
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
roleSelected: this.item.isAdmin ? rolesValues.admin : rolesValues.user,
|
||||
roles: [
|
||||
{ value: rolesValues.user, text: this.$t('userRole.selectRoles.user') },
|
||||
{ value: rolesValues.admin, text: this.$t('userRole.selectRoles.admin') },
|
||||
],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
roleSelected(newRole, oldRole) {
|
||||
if (newRole !== oldRole) {
|
||||
this.setUserRole(newRole, oldRole)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setUserRole(newRole, oldRole) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: setUserRole,
|
||||
variables: {
|
||||
userId: this.item.userId,
|
||||
isAdmin: newRole === rolesValues.admin,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit('updateIsAdmin', {
|
||||
userId: this.item.userId,
|
||||
isAdmin: result.data.setUserRole,
|
||||
})
|
||||
this.toastSuccess(
|
||||
this.$t('userRole.successfullyChangedTo', {
|
||||
role:
|
||||
result.data.setUserRole !== null
|
||||
? this.$t('userRole.selectRoles.admin')
|
||||
: this.$t('userRole.selectRoles.user'),
|
||||
}),
|
||||
)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.roleSelected = oldRole
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.role-select {
|
||||
width: 300pt;
|
||||
}
|
||||
</style>
|
||||
49
admin/src/components/ContributionLink.spec.js
Normal file
49
admin/src/components/ContributionLink.spec.js
Normal file
@ -0,0 +1,49 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ContributionLink from './ContributionLink.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Meditation',
|
||||
memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l',
|
||||
amount: '200',
|
||||
validFrom: '2022-04-01',
|
||||
validTo: '2022-08-01',
|
||||
cycle: 'täglich',
|
||||
maxPerCycle: '3',
|
||||
maxAmountPerMonth: 0,
|
||||
link: 'https://localhost/redeem/CL-1a2345678',
|
||||
},
|
||||
],
|
||||
count: 1,
|
||||
}
|
||||
|
||||
describe('ContributionLink', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(ContributionLink, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the Div Element ".contribution-link"', () => {
|
||||
expect(wrapper.find('div.contribution-link').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('emits toggle::collapse new Contribution', async () => {
|
||||
wrapper.vm.editContributionLinkData()
|
||||
expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
66
admin/src/components/ContributionLink.vue
Normal file
66
admin/src/components/ContributionLink.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="contribution-link">
|
||||
<b-card
|
||||
border-variant="success"
|
||||
:header="$t('contributionLink.contributionLinks')"
|
||||
header-bg-variant="success"
|
||||
header-text-variant="white"
|
||||
header-class="text-center"
|
||||
class="mt-5"
|
||||
>
|
||||
<b-button v-b-toggle.newContribution class="my-3 d-flex justify-content-left">
|
||||
{{ $t('math.plus') }} {{ $t('contributionLink.newContributionLink') }}
|
||||
</b-button>
|
||||
|
||||
<b-collapse v-model="visible" id="newContribution" class="mt-2">
|
||||
<b-card>
|
||||
<p class="h2 ml-5">{{ $t('contributionLink.contributionLinks') }}</p>
|
||||
<contribution-link-form :contributionLinkData="contributionLinkData" />
|
||||
</b-card>
|
||||
</b-collapse>
|
||||
|
||||
<b-card-text>
|
||||
<contribution-link-list
|
||||
v-if="count > 0"
|
||||
:items="items"
|
||||
@editContributionLinkData="editContributionLinkData"
|
||||
/>
|
||||
<div v-else>{{ $t('contributionLink.noContributionLinks') }}</div>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ContributionLinkForm from './ContributionLinkForm.vue'
|
||||
import ContributionLinkList from './ContributionLinkList.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContributionLink',
|
||||
components: {
|
||||
ContributionLinkForm,
|
||||
ContributionLinkList,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
count: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
visible: false,
|
||||
contributionLinkData: {},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editContributionLinkData(data) {
|
||||
if (!this.visible) this.$root.$emit('bv::toggle::collapse', 'newContribution')
|
||||
this.contributionLinkData = data
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
102
admin/src/components/ContributionLinkForm.spec.js
Normal file
102
admin/src/components/ContributionLinkForm.spec.js
Normal file
@ -0,0 +1,102 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ContributionLinkForm from './ContributionLinkForm.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
global.alert = jest.fn()
|
||||
|
||||
const propsData = {
|
||||
contributionLinkData: {},
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
}
|
||||
|
||||
// const mockAPIcall = jest.fn()
|
||||
|
||||
describe('ContributionLinkForm', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(ContributionLinkForm, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the Div Element ".contribution-link-form"', () => {
|
||||
expect(wrapper.find('div.contribution-link-form').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('call onReset', () => {
|
||||
it('form has the set data', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setData({
|
||||
form: {
|
||||
name: 'name',
|
||||
memo: 'memo',
|
||||
amount: 100,
|
||||
validFrom: 'validFrom',
|
||||
validTo: 'validTo',
|
||||
cycle: 'ONCE',
|
||||
maxPerCycle: 1,
|
||||
maxAmountPerMonth: 100,
|
||||
},
|
||||
})
|
||||
wrapper.vm.onReset()
|
||||
})
|
||||
expect(wrapper.vm.form).toEqual({
|
||||
amount: null,
|
||||
cycle: 'ONCE',
|
||||
validTo: null,
|
||||
maxAmountPerMonth: '0',
|
||||
memo: null,
|
||||
name: null,
|
||||
maxPerCycle: 1,
|
||||
validFrom: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('call onSubmit', () => {
|
||||
it('response with the contribution link url', () => {
|
||||
wrapper.vm.onSubmit()
|
||||
})
|
||||
})
|
||||
|
||||
// describe('successfull submit', () => {
|
||||
// beforeEach(async () => {
|
||||
// mockAPIcall.mockResolvedValue({
|
||||
// data: {
|
||||
// createContributionLink: {
|
||||
// link: 'https://localhost/redeem/CL-1a2345678',
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// await wrapper.find('input.test-validFrom').setValue('2022-6-18')
|
||||
// await wrapper.find('input.test-validTo').setValue('2022-7-18')
|
||||
// await wrapper.find('input.test-name').setValue('test name')
|
||||
// await wrapper.find('input.test-memo').setValue('test memo')
|
||||
// await wrapper.find('input.test-amount').setValue('100')
|
||||
// await wrapper.find('form').trigger('submit')
|
||||
// })
|
||||
|
||||
// it('calls the API', () => {
|
||||
// expect(mockAPIcall).toHaveBeenCalledWith(
|
||||
// expect.objectContaining({
|
||||
// variables: {
|
||||
// link: 'https://localhost/redeem/CL-1a2345678',
|
||||
// },
|
||||
// }),
|
||||
// )
|
||||
// })
|
||||
|
||||
// it('displays the new username', () => {
|
||||
// expect(wrapper.find('div.display-username').text()).toEqual('@username')
|
||||
// })
|
||||
// })
|
||||
})
|
||||
})
|
||||
218
admin/src/components/ContributionLinkForm.vue
Normal file
218
admin/src/components/ContributionLinkForm.vue
Normal file
@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<div class="contribution-link-form">
|
||||
<div v-if="updateData" class="text-light bg-info p-3">
|
||||
{{ updateData }}
|
||||
</div>
|
||||
<b-form class="m-5" @submit.prevent="onSubmit" ref="contributionLinkForm">
|
||||
<!-- Date -->
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-form-group :label="$t('contributionLink.validFrom')">
|
||||
<b-form-datepicker
|
||||
v-model="form.validFrom"
|
||||
size="lg"
|
||||
:min="min"
|
||||
class="mb-4 test-validFrom"
|
||||
reset-value=""
|
||||
:label-no-date-selected="$t('contributionLink.noDateSelected')"
|
||||
required
|
||||
></b-form-datepicker>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-group :label="$t('contributionLink.validTo')">
|
||||
<b-form-datepicker
|
||||
v-model="form.validTo"
|
||||
size="lg"
|
||||
:min="form.validFrom ? form.validFrom : min"
|
||||
class="mb-4 test-validTo"
|
||||
reset-value=""
|
||||
:label-no-date-selected="$t('contributionLink.noDateSelected')"
|
||||
required
|
||||
></b-form-datepicker>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<!-- Name -->
|
||||
<b-form-group :label="$t('contributionLink.name')">
|
||||
<b-form-input
|
||||
v-model="form.name"
|
||||
size="lg"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
required
|
||||
maxlength="100"
|
||||
class="test-name"
|
||||
></b-form-input>
|
||||
</b-form-group>
|
||||
<!-- Desc -->
|
||||
<b-form-group :label="$t('contributionLink.memo')">
|
||||
<b-form-textarea
|
||||
v-model="form.memo"
|
||||
size="lg"
|
||||
:placeholder="$t('contributionLink.memo')"
|
||||
required
|
||||
maxlength="255"
|
||||
class="test-memo"
|
||||
></b-form-textarea>
|
||||
</b-form-group>
|
||||
<!-- Amount -->
|
||||
<b-form-group :label="$t('contributionLink.amount')">
|
||||
<b-form-input
|
||||
v-model="form.amount"
|
||||
size="lg"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
required
|
||||
class="test-amount"
|
||||
></b-form-input>
|
||||
</b-form-group>
|
||||
<b-collapse id="collapse-2">
|
||||
<b-jumbotron>
|
||||
<b-row class="mb-4">
|
||||
<b-col>
|
||||
<!-- Cycle -->
|
||||
<label for="cycle">{{ $t('contributionLink.cycle') }}</label>
|
||||
<b-form-select
|
||||
v-model="form.cycle"
|
||||
:options="cycle"
|
||||
:disabled="disabled"
|
||||
class="mb-3"
|
||||
size="lg"
|
||||
></b-form-select>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<!-- maxPerCycle -->
|
||||
<label for="maxPerCycle">{{ $t('contributionLink.maxPerCycle') }}</label>
|
||||
<b-form-select
|
||||
v-model="form.maxPerCycle"
|
||||
:options="maxPerCycle"
|
||||
:disabled="disabled"
|
||||
class="mb-3"
|
||||
size="lg"
|
||||
></b-form-select>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<!-- Max amount -->
|
||||
<b-form-group :label="$t('contributionLink.maximumAmount')">
|
||||
<b-form-input
|
||||
v-model="form.maxAmountPerMonth"
|
||||
size="lg"
|
||||
:disabled="disabled"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
></b-form-input>
|
||||
</b-form-group>
|
||||
</b-jumbotron>
|
||||
</b-collapse>
|
||||
<div class="mt-6">
|
||||
<b-button type="submit" variant="primary">{{ $t('contributionLink.create') }}</b-button>
|
||||
<b-button type="reset" variant="danger" @click.prevent="onReset">
|
||||
{{ $t('contributionLink.clear') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-form>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { createContributionLink } from '@/graphql/createContributionLink.js'
|
||||
export default {
|
||||
name: 'ContributionLinkForm',
|
||||
props: {
|
||||
contributionLinkData: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
name: null,
|
||||
memo: null,
|
||||
amount: null,
|
||||
validFrom: null,
|
||||
validTo: null,
|
||||
cycle: 'ONCE',
|
||||
maxPerCycle: 1,
|
||||
maxAmountPerMonth: '0',
|
||||
},
|
||||
min: new Date(),
|
||||
cycle: [
|
||||
{ value: 'ONCE', text: this.$t('contributionLink.options.cycle.once') },
|
||||
{ value: 'hourly', text: this.$t('contributionLink.options.cycle.hourly') },
|
||||
{ value: 'daily', text: this.$t('contributionLink.options.cycle.daily') },
|
||||
{ value: 'weekly', text: this.$t('contributionLink.options.cycle.weekly') },
|
||||
{ value: 'monthly', text: this.$t('contributionLink.options.cycle.monthly') },
|
||||
{ value: 'yearly', text: this.$t('contributionLink.options.cycle.yearly') },
|
||||
],
|
||||
maxPerCycle: [
|
||||
{ value: '1', text: '1 x' },
|
||||
{ value: '2', text: '2 x' },
|
||||
{ value: '3', text: '3 x' },
|
||||
{ value: '4', text: '4 x' },
|
||||
{ value: '5', text: '5 x' },
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
if (this.form.validFrom === null)
|
||||
return this.toastError(this.$t('contributionLink.noStartDate'))
|
||||
if (this.form.validTo === null) return this.toastError(this.$t('contributionLink.noEndDate'))
|
||||
// alert(JSON.stringify(this.form))
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
validFrom: this.form.validFrom,
|
||||
validTo: this.form.validTo,
|
||||
name: this.form.name,
|
||||
amount: this.form.amount,
|
||||
memo: this.form.memo,
|
||||
cycle: this.form.cycle,
|
||||
maxPerCycle: this.form.maxPerCycle,
|
||||
maxAmountPerMonth: this.form.maxAmountPerMonth,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.link = result.data.createContributionLink.link
|
||||
this.toastSuccess(this.link)
|
||||
this.onReset()
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
onReset() {
|
||||
this.$refs.contributionLinkForm.reset()
|
||||
this.form.validFrom = null
|
||||
this.form.validTo = null
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
updateData() {
|
||||
return this.contributionLinkData
|
||||
},
|
||||
disabled() {
|
||||
if (this.form.cycle === 'ONCE') return true
|
||||
return false
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
contributionLinkData() {
|
||||
this.form.name = this.contributionLinkData.name
|
||||
this.form.memo = this.contributionLinkData.memo
|
||||
this.form.amount = this.contributionLinkData.amount
|
||||
this.form.validFrom = this.contributionLinkData.validFrom
|
||||
this.form.validTo = this.contributionLinkData.validTo
|
||||
this.form.cycle = this.contributionLinkData.cycle
|
||||
this.form.maxPerCycle = this.contributionLinkData.maxPerCycle
|
||||
this.form.maxAmountPerMonth = this.contributionLinkData.maxAmountPerMonth
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
147
admin/src/components/ContributionLinkList.spec.js
Normal file
147
admin/src/components/ContributionLinkList.spec.js
Normal file
@ -0,0 +1,147 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ContributionLinkList from './ContributionLinkList.vue'
|
||||
import { toastSuccessSpy, toastErrorSpy } from '../../test/testSetup'
|
||||
// import { deleteContributionLink } from '../graphql/deleteContributionLink'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const mockAPIcall = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
mutate: mockAPIcall,
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Meditation',
|
||||
memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l',
|
||||
amount: '200',
|
||||
validFrom: '2022-04-01',
|
||||
validTo: '2022-08-01',
|
||||
cycle: 'täglich',
|
||||
maxPerCycle: '3',
|
||||
maxAmountPerMonth: 0,
|
||||
link: 'https://localhost/redeem/CL-1a2345678',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
describe('ContributionLinkList', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(ContributionLinkList, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the Div Element ".contribution-link-list"', () => {
|
||||
expect(wrapper.find('div.contribution-link-list').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders table with contribution link', () => {
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
|
||||
'Meditation',
|
||||
)
|
||||
})
|
||||
|
||||
describe('edit contribution link', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.vm.editContributionLink()
|
||||
})
|
||||
|
||||
it('emits editContributionLinkData', async () => {
|
||||
expect(wrapper.vm.$emit('editContributionLinkData')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete contribution link', () => {
|
||||
let spy
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
wrapper.vm.deleteContributionLink()
|
||||
})
|
||||
|
||||
describe('with success', () => {
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve('some value'))
|
||||
mockAPIcall.mockResolvedValue()
|
||||
await wrapper.find('.test-delete-link').trigger('click')
|
||||
})
|
||||
|
||||
it('opens the modal ', () => {
|
||||
expect(spy).toBeCalled()
|
||||
})
|
||||
|
||||
it.skip('calls the API', () => {
|
||||
// expect(mockAPIcall).toBeCalledWith(
|
||||
// expect.objectContaining({
|
||||
// mutation: deleteContributionLink,
|
||||
// variables: {
|
||||
// id: 1,
|
||||
// },
|
||||
// }),
|
||||
// )
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('TODO: request message deleted ')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with error', () => {
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve('some value'))
|
||||
mockAPIcall.mockRejectedValue({ message: 'Something went wrong :(' })
|
||||
await wrapper.find('.test-delete-link').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Something went wrong :(')
|
||||
})
|
||||
})
|
||||
|
||||
describe('cancel delete', () => {
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve(false))
|
||||
mockAPIcall.mockResolvedValue()
|
||||
await wrapper.find('.test-delete-link').trigger('click')
|
||||
})
|
||||
|
||||
it('does not call the API', () => {
|
||||
expect(mockAPIcall).not.toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('onClick showButton', () => {
|
||||
it('modelData contains contribution link', () => {
|
||||
wrapper.find('button.test-show').trigger('click')
|
||||
expect(wrapper.vm.modalData).toEqual({
|
||||
amount: '200',
|
||||
cycle: 'täglich',
|
||||
id: 1,
|
||||
link: 'https://localhost/redeem/CL-1a2345678',
|
||||
maxAmountPerMonth: 0,
|
||||
maxPerCycle: '3',
|
||||
memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l',
|
||||
name: 'Meditation',
|
||||
validFrom: '2022-04-01',
|
||||
validTo: '2022-08-01',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
106
admin/src/components/ContributionLinkList.vue
Normal file
106
admin/src/components/ContributionLinkList.vue
Normal file
@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="contribution-link-list">
|
||||
<b-table striped hover :items="items" :fields="fields">
|
||||
<template #cell(delete)>
|
||||
<b-button
|
||||
variant="danger"
|
||||
size="md"
|
||||
class="mr-2 test-delete-link"
|
||||
@click="deleteContributionLink"
|
||||
>
|
||||
<b-icon icon="trash" variant="light"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
<template #cell(edit)="data">
|
||||
<b-button variant="success" size="md" class="mr-2" @click="editContributionLink(data.item)">
|
||||
<b-icon icon="pencil" variant="light"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
<template #cell(show)="data">
|
||||
<b-button
|
||||
variant="info"
|
||||
size="md"
|
||||
class="mr-2 test-show"
|
||||
@click="showContributionLink(data.item)"
|
||||
>
|
||||
<b-icon icon="eye" variant="light"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table>
|
||||
|
||||
<b-modal ref="my-modal" ok-only hide-header-close>
|
||||
<b-card header-tag="header" footer-tag="footer">
|
||||
<template #header>
|
||||
<h6 class="mb-0">{{ modalData ? modalData.name : '' }}</h6>
|
||||
</template>
|
||||
<b-card-text>
|
||||
{{ modalData }}
|
||||
<figure-qr-code :link="modalData ? modalData.link : ''" />
|
||||
</b-card-text>
|
||||
<template #footer>
|
||||
<em>{{ modalData ? modalData.link : '' }}</em>
|
||||
</template>
|
||||
</b-card>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { deleteContributionLink } from '@/graphql/deleteContributionLink.js'
|
||||
import FigureQrCode from './FigureQrCode.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContributionLinkList',
|
||||
components: {
|
||||
FigureQrCode,
|
||||
},
|
||||
props: {
|
||||
items: { type: Array, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fields: [
|
||||
'name',
|
||||
'memo',
|
||||
'amount',
|
||||
{ key: 'cycle', label: this.$t('contributionLink.cycle') },
|
||||
{ key: 'maxPerCycle', label: this.$t('contributionLink.maxPerCycle') },
|
||||
{ key: 'validFrom', label: this.$t('contributionLink.validFrom') },
|
||||
{ key: 'validTo', label: this.$t('contributionLink.validTo') },
|
||||
'delete',
|
||||
'edit',
|
||||
'show',
|
||||
],
|
||||
modalData: null,
|
||||
modalDataLink: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteContributionLink() {
|
||||
this.$bvModal.msgBoxConfirm(this.$t('contributionLink.deleteNow')).then(async (value) => {
|
||||
if (value)
|
||||
await this.$apollo
|
||||
.mutate({
|
||||
mutation: deleteContributionLink,
|
||||
variables: {
|
||||
id: this.id,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.toastSuccess('TODO: request message deleted ')
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
})
|
||||
})
|
||||
},
|
||||
editContributionLink(row) {
|
||||
this.$emit('editContributionLinkData', row)
|
||||
},
|
||||
|
||||
showContributionLink(row) {
|
||||
this.modalData = row
|
||||
this.$refs['my-modal'].show()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,14 +1,14 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CreationFormular from './CreationFormular.vue'
|
||||
import { createPendingCreation } from '../graphql/createPendingCreation'
|
||||
import { createPendingCreations } from '../graphql/createPendingCreations'
|
||||
import { adminCreateContribution } from '../graphql/adminCreateContribution'
|
||||
import { adminCreateContributions } from '../graphql/adminCreateContributions'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
createPendingCreation: [0, 0, 0],
|
||||
adminCreateContribution: [0, 0, 0],
|
||||
},
|
||||
})
|
||||
const stateCommitMock = jest.fn()
|
||||
@ -110,7 +110,7 @@ describe('CreationFormular', () => {
|
||||
it('sends ... to apollo', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: createPendingCreation,
|
||||
mutation: adminCreateContribution,
|
||||
variables: {
|
||||
email: 'benjamin@bluemchen.de',
|
||||
creationDate: getCreationDate(2),
|
||||
@ -334,10 +334,10 @@ describe('CreationFormular', () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
createPendingCreations: {
|
||||
adminCreateContributions: {
|
||||
success: true,
|
||||
successfulCreation: ['bob@baumeister.de', 'bibi@bloxberg.de'],
|
||||
failedCreation: [],
|
||||
successfulContribution: ['bob@baumeister.de', 'bibi@bloxberg.de'],
|
||||
failedContribution: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -355,7 +355,7 @@ describe('CreationFormular', () => {
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: createPendingCreations,
|
||||
mutation: adminCreateContributions,
|
||||
variables: {
|
||||
pendingCreations: [
|
||||
{
|
||||
@ -390,10 +390,10 @@ describe('CreationFormular', () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
createPendingCreations: {
|
||||
adminCreateContributions: {
|
||||
success: true,
|
||||
successfulCreation: [],
|
||||
failedCreation: ['bob@baumeister.de', 'bibi@bloxberg.de'],
|
||||
successfulContribution: [],
|
||||
failedContribution: ['bob@baumeister.de', 'bibi@bloxberg.de'],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -85,8 +85,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { createPendingCreation } from '../graphql/createPendingCreation'
|
||||
import { createPendingCreations } from '../graphql/createPendingCreations'
|
||||
import { adminCreateContribution } from '../graphql/adminCreateContribution'
|
||||
import { adminCreateContributions } from '../graphql/adminCreateContributions'
|
||||
import { creationMonths } from '../mixins/creationMonths'
|
||||
export default {
|
||||
name: 'CreationFormular',
|
||||
@ -158,25 +158,25 @@ export default {
|
||||
})
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: createPendingCreations,
|
||||
mutation: adminCreateContributions,
|
||||
variables: {
|
||||
pendingCreations: submitObj,
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
})
|
||||
.then((result) => {
|
||||
const failedCreations = []
|
||||
const failedContributions = []
|
||||
this.$store.commit(
|
||||
'openCreationsPlus',
|
||||
result.data.createPendingCreations.successfulCreation.length,
|
||||
result.data.adminCreateContributions.successfulContribution.length,
|
||||
)
|
||||
if (result.data.createPendingCreations.failedCreation.length > 0) {
|
||||
result.data.createPendingCreations.failedCreation.forEach((email) => {
|
||||
failedCreations.push(email)
|
||||
if (result.data.adminCreateContributions.failedContribution.length > 0) {
|
||||
result.data.adminCreateContributions.failedContribution.forEach((email) => {
|
||||
failedContributions.push(email)
|
||||
})
|
||||
}
|
||||
this.$emit('remove-all-bookmark')
|
||||
this.$emit('toast-failed-creations', failedCreations)
|
||||
this.$emit('toast-failed-creations', failedContributions)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
@ -190,11 +190,11 @@ export default {
|
||||
}
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: createPendingCreation,
|
||||
mutation: adminCreateContribution,
|
||||
variables: submitObj,
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit('update-user-data', this.item, result.data.createPendingCreation)
|
||||
this.$emit('update-user-data', this.item, result.data.adminCreateContribution)
|
||||
this.$store.commit('openCreationsPlus', 1)
|
||||
this.toastSuccess(
|
||||
this.$t('creation_form.toasted', {
|
||||
|
||||
@ -47,200 +47,200 @@ describe('DeletedUserFormular', () => {
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.delete-user-formular', () => {
|
||||
expect(wrapper.find('.deleted-user-formular').exists()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete self', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 0,
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.deleted-user-formular').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('shows a text that you cannot delete yourself', () => {
|
||||
expect(wrapper.text()).toBe('removeNotSelf')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete other user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 1,
|
||||
deletedAt: null,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('has a checkbox', () => {
|
||||
expect(wrapper.find('input[type="checkbox"]').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows the text "delete_user"', () => {
|
||||
expect(wrapper.text()).toBe('delete_user')
|
||||
})
|
||||
|
||||
describe('click on checkbox', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked()
|
||||
})
|
||||
|
||||
it('has a confirmation button', () => {
|
||||
expect(wrapper.find('button').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has the button text "delete_user"', () => {
|
||||
expect(wrapper.find('button').text()).toBe('delete_user')
|
||||
})
|
||||
|
||||
describe('confirm delete with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: deleteUser,
|
||||
variables: {
|
||||
userId: 1,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('unchecks the checkbox', () => {
|
||||
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm delete with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('click on checkbox again', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked(false)
|
||||
})
|
||||
|
||||
it('has no confirmation button anymore', () => {
|
||||
expect(wrapper.find('button').exists()).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('recover user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('has a checkbox', () => {
|
||||
expect(wrapper.find('input[type="checkbox"]').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows the text "undelete_user"', () => {
|
||||
expect(wrapper.text()).toBe('undelete_user')
|
||||
})
|
||||
|
||||
describe('click on checkbox', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
unDeleteUser: null,
|
||||
describe('delete self', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 0,
|
||||
},
|
||||
})
|
||||
await wrapper.find('input[type="checkbox"]').setChecked()
|
||||
})
|
||||
|
||||
it('has a confirmation button', () => {
|
||||
expect(wrapper.find('button').exists()).toBeTruthy()
|
||||
it('shows a text that you cannot delete yourself', () => {
|
||||
expect(wrapper.text()).toBe('removeNotSelf')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete other user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 1,
|
||||
deletedAt: null,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('has the button text "undelete_user"', () => {
|
||||
expect(wrapper.find('button').text()).toBe('undelete_user')
|
||||
it('has a checkbox', () => {
|
||||
expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('confirm recover with success', () => {
|
||||
it('shows the text "delete_user"', () => {
|
||||
expect(wrapper.text()).toBe('delete_user')
|
||||
})
|
||||
|
||||
describe('click on checkbox', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
await wrapper.find('input[type="checkbox"]').setChecked()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: unDeleteUser,
|
||||
variables: {
|
||||
userId: 1,
|
||||
},
|
||||
}),
|
||||
)
|
||||
it('has a confirmation button', () => {
|
||||
expect(wrapper.find('button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
it('has the button text "delete_user"', () => {
|
||||
expect(wrapper.find('button').text()).toBe('delete_user')
|
||||
})
|
||||
|
||||
describe('confirm delete with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: deleteUser,
|
||||
variables: {
|
||||
userId: 1,
|
||||
deletedAt: null,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('unchecks the checkbox', () => {
|
||||
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
it('unchecks the checkbox', () => {
|
||||
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
|
||||
describe('confirm delete with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('click on checkbox again', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked(false)
|
||||
})
|
||||
|
||||
it('has no confirmation button anymore', () => {
|
||||
expect(wrapper.find('button').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('recover user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm recover with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
it('has a checkbox', () => {
|
||||
expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('click on checkbox again', () => {
|
||||
it('shows the text "undelete_user"', () => {
|
||||
expect(wrapper.text()).toBe('undelete_user')
|
||||
})
|
||||
|
||||
describe('click on checkbox', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked(false)
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
unDeleteUser: null,
|
||||
},
|
||||
})
|
||||
await wrapper.find('input[type="checkbox"]').setChecked()
|
||||
})
|
||||
|
||||
it('has no confirmation button anymore', () => {
|
||||
expect(wrapper.find('button').exists()).toBeFalsy()
|
||||
it('has a confirmation button', () => {
|
||||
expect(wrapper.find('button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has the button text "undelete_user"', () => {
|
||||
expect(wrapper.find('button').text()).toBe('undelete_user')
|
||||
})
|
||||
|
||||
describe('confirm recover with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: unDeleteUser,
|
||||
variables: {
|
||||
userId: 1,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
deletedAt: null,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('unchecks the checkbox', () => {
|
||||
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm recover with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('click on checkbox again', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked(false)
|
||||
})
|
||||
|
||||
it('has no confirmation button anymore', () => {
|
||||
expect(wrapper.find('button').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -28,6 +28,7 @@ export default {
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
||||
@ -6,7 +6,7 @@ const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
updatePendingCreation: {
|
||||
adminUpdateContribution: {
|
||||
creation: [0, 0, 0],
|
||||
amount: 500,
|
||||
date: new Date(),
|
||||
|
||||
@ -73,7 +73,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { updatePendingCreation } from '../graphql/updatePendingCreation'
|
||||
import { adminUpdateContribution } from '../graphql/adminUpdateContribution'
|
||||
import { creationMonths } from '../mixins/creationMonths'
|
||||
|
||||
export default {
|
||||
@ -103,7 +103,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
text: !this.creationUserData.memo ? '' : this.creationUserData.memo,
|
||||
value: !this.creationUserData.amount ? 0 : this.creationUserData.amount,
|
||||
value: !this.creationUserData.amount ? 0 : Number(this.creationUserData.amount),
|
||||
rangeMin: 0,
|
||||
rangeMax: 1000,
|
||||
selected: '',
|
||||
@ -113,7 +113,7 @@ export default {
|
||||
submitCreation() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: updatePendingCreation,
|
||||
mutation: adminUpdateContribution,
|
||||
variables: {
|
||||
id: this.item.id,
|
||||
email: this.item.email,
|
||||
@ -123,11 +123,11 @@ export default {
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit('update-user-data', this.item, result.data.updatePendingCreation.creation)
|
||||
this.$emit('update-user-data', this.item, result.data.adminUpdateContribution.creation)
|
||||
this.$emit('update-creation-data', {
|
||||
amount: Number(result.data.updatePendingCreation.amount),
|
||||
date: result.data.updatePendingCreation.date,
|
||||
memo: result.data.updatePendingCreation.memo,
|
||||
amount: Number(result.data.adminUpdateContribution.amount),
|
||||
date: result.data.adminUpdateContribution.date,
|
||||
memo: result.data.adminUpdateContribution.memo,
|
||||
row: this.row,
|
||||
})
|
||||
this.toastSuccess(
|
||||
@ -155,7 +155,7 @@ export default {
|
||||
const month = this.$d(new Date(this.creationUserData.date), 'month')
|
||||
const index = this.radioOptions.findIndex((obj) => obj.item.short === month)
|
||||
this.selected = this.radioOptions[index].item
|
||||
this.rangeMax = this.creation[index] + this.creationUserData.amount
|
||||
this.rangeMax = Number(this.creation[index]) + Number(this.creationUserData.amount)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
30
admin/src/components/FigureQrCode.spec.js
Normal file
30
admin/src/components/FigureQrCode.spec.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import FigureQrCode from './FigureQrCode.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const propsData = {
|
||||
link: '',
|
||||
}
|
||||
|
||||
describe('FigureQrCode', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(FigureQrCode, { localVue, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the Div Element ".figure-qr-code"', () => {
|
||||
expect(wrapper.find('div.figure-qr-code').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders the QRCanvas Element ".canvas"', () => {
|
||||
expect(wrapper.find('.canvas').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
56
admin/src/components/FigureQrCode.vue
Normal file
56
admin/src/components/FigureQrCode.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="figure-qr-code">
|
||||
<div class="qrbox">
|
||||
<q-r-canvas :options="options" class="canvas" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { QRCanvas } from 'qrcanvas-vue'
|
||||
|
||||
export default {
|
||||
name: 'FigureQrCode',
|
||||
components: {
|
||||
QRCanvas,
|
||||
},
|
||||
props: {
|
||||
link: { type: String, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
options: {
|
||||
cellSize: 8,
|
||||
correctLevel: 'H',
|
||||
data: this.link,
|
||||
logo: {
|
||||
image: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const image = new Image()
|
||||
image.src = 'img/gdd-coin.png'
|
||||
image.onload = () => {
|
||||
this.options = {
|
||||
...this.options,
|
||||
logo: {
|
||||
image,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.qrbox {
|
||||
padding: 20px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
.canvas {
|
||||
width: 90%;
|
||||
max-width: 300px;
|
||||
padding: 5px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
</style>
|
||||
@ -28,7 +28,7 @@ const propsData = {
|
||||
amount: 210,
|
||||
memo: 'Aktives Grundeinkommen für Januar 2022',
|
||||
date: '2022-01-01T00:00:00.000Z',
|
||||
moderator: 1,
|
||||
moderator: null,
|
||||
creation: [790, 1000, 1000],
|
||||
__typename: 'PendingCreation',
|
||||
},
|
||||
@ -66,9 +66,10 @@ const propsData = {
|
||||
},
|
||||
},
|
||||
{ key: 'moderator', label: 'moderator' },
|
||||
{ key: 'edit_creation', label: 'edit' },
|
||||
{ key: 'editCreation', label: 'edit' },
|
||||
{ key: 'confirm', label: 'save' },
|
||||
],
|
||||
toggleDetails: false,
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
@ -101,7 +102,7 @@ describe('OpenCreationsTable', () => {
|
||||
})
|
||||
|
||||
it('has a DIV element with the class .open-creations-table', () => {
|
||||
expect(wrapper.find('div.open-creations-table').exists()).toBeTruthy()
|
||||
expect(wrapper.find('div.open-creations-table').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has a table with three rows', () => {
|
||||
@ -109,7 +110,11 @@ describe('OpenCreationsTable', () => {
|
||||
})
|
||||
|
||||
it('find first button.bi-pencil-square for open EditCreationFormular ', () => {
|
||||
expect(wrapper.findAll('tr').at(1).find('.bi-pencil-square').exists()).toBeTruthy()
|
||||
expect(wrapper.findAll('tr').at(1).find('.bi-pencil-square').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has no button.bi-pencil-square for user contribution ', () => {
|
||||
expect(wrapper.findAll('tr').at(2).find('.bi-pencil-square').exists()).toBe(false)
|
||||
})
|
||||
|
||||
describe('show edit details', () => {
|
||||
@ -122,7 +127,15 @@ describe('OpenCreationsTable', () => {
|
||||
})
|
||||
|
||||
it.skip('renders the component component-edit-creation-formular', () => {
|
||||
expect(wrapper.find('div.component-edit-creation-formular').exists()).toBeTruthy()
|
||||
expect(wrapper.find('div.component-edit-creation-formular').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('call updateUserData', () => {
|
||||
it('user creations has updated data', async () => {
|
||||
wrapper.vm.updateUserData(propsData.items[0], [444, 555, 666])
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.vm.items[0].creation).toEqual([444, 555, 666])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -11,8 +11,14 @@
|
||||
<b-icon icon="x" variant="light"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
<template #cell(edit_creation)="row">
|
||||
<b-button variant="info" size="md" @click="rowToggleDetails(row, 0)" class="mr-2">
|
||||
<template #cell(editCreation)="row">
|
||||
<b-button
|
||||
v-if="row.item.moderator"
|
||||
variant="info"
|
||||
size="md"
|
||||
@click="rowToggleDetails(row, 0)"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon :icon="row.detailsShowing ? 'x' : 'pencil-square'" aria-label="Help"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
@ -70,12 +76,23 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
creationUserData: {
|
||||
amount: null,
|
||||
date: null,
|
||||
memo: null,
|
||||
moderator: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateCreationData(data) {
|
||||
this.creationUserData.amount = data.amount
|
||||
this.creationUserData.date = data.date
|
||||
this.creationUserData.memo = data.memo
|
||||
this.creationUserData.moderator = data.moderator
|
||||
this.creationUserData = data
|
||||
// this.creationUserData.amount = data.amount
|
||||
// this.creationUserData.date = data.date
|
||||
// this.creationUserData.memo = data.memo
|
||||
// this.creationUserData.moderator = data.moderator
|
||||
data.row.toggleDetails()
|
||||
},
|
||||
updateUserData(rowItem, newCreation) {
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import SearchUserTable from './SearchUserTable.vue'
|
||||
|
||||
const date = new Date()
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({})
|
||||
@ -96,16 +94,29 @@ describe('SearchUserTable', () => {
|
||||
await wrapper.findAll('tbody > tr').at(1).trigger('click')
|
||||
})
|
||||
|
||||
describe('isAdmin', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('div.change-user-role-formular').vm.$emit('updateIsAdmin', {
|
||||
userId: 1,
|
||||
isAdmin: new Date(),
|
||||
})
|
||||
})
|
||||
|
||||
it('emits updateIsAdmin', () => {
|
||||
expect(wrapper.emitted('updateIsAdmin')).toEqual([[1, expect.any(Date)]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleted at', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('div.deleted-user-formular').vm.$emit('updateDeletedAt', {
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
deletedAt: new Date(),
|
||||
})
|
||||
})
|
||||
|
||||
it('emits updateDeletedAt', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual([[1, date]])
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual([[1, expect.any(Date)]])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
<template #cell(status)="row">
|
||||
<div class="text-right">
|
||||
<b-avatar v-if="row.item.deletedAt" class="mr-3" variant="light">
|
||||
<b-avatar v-if="row.item.deletedAt" class="mr-3 test-deleted-icon" variant="light">
|
||||
<b-iconstack font-scale="2">
|
||||
<b-icon stacked icon="person" variant="info" scale="0.75"></b-icon>
|
||||
<b-icon stacked icon="slash-circle" variant="danger"></b-icon>
|
||||
@ -79,6 +79,9 @@
|
||||
<b-tab :title="$t('transactionlink.name')" :disabled="row.item.deletedAt !== null">
|
||||
<transaction-link-list v-if="!row.item.deletedAt" :userId="row.item.userId" />
|
||||
</b-tab>
|
||||
<b-tab :title="$t('userRole.tabTitle')">
|
||||
<change-user-role-formular :item="row.item" @updateIsAdmin="updateIsAdmin" />
|
||||
</b-tab>
|
||||
<b-tab :title="$t('delete_user')">
|
||||
<deleted-user-formular :item="row.item" @updateDeletedAt="updateDeletedAt" />
|
||||
</b-tab>
|
||||
@ -93,6 +96,7 @@ import CreationFormular from '../CreationFormular.vue'
|
||||
import ConfirmRegisterMailFormular from '../ConfirmRegisterMailFormular.vue'
|
||||
import CreationTransactionList from '../CreationTransactionList.vue'
|
||||
import TransactionLinkList from '../TransactionLinkList.vue'
|
||||
import ChangeUserRoleFormular from '../ChangeUserRoleFormular.vue'
|
||||
import DeletedUserFormular from '../DeletedUserFormular.vue'
|
||||
|
||||
export default {
|
||||
@ -102,6 +106,7 @@ export default {
|
||||
ConfirmRegisterMailFormular,
|
||||
CreationTransactionList,
|
||||
TransactionLinkList,
|
||||
ChangeUserRoleFormular,
|
||||
DeletedUserFormular,
|
||||
},
|
||||
props: {
|
||||
@ -123,6 +128,9 @@ export default {
|
||||
updateUserData(rowItem, newCreation) {
|
||||
rowItem.creation = newCreation
|
||||
},
|
||||
updateIsAdmin({ userId, isAdmin }) {
|
||||
this.$emit('updateIsAdmin', userId, isAdmin)
|
||||
},
|
||||
updateDeletedAt({ userId, deletedAt }) {
|
||||
this.$emit('updateDeletedAt', userId, deletedAt)
|
||||
},
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
:per-page="perPage"
|
||||
:total-rows="rows"
|
||||
align="center"
|
||||
:hide-ellipsis="true"
|
||||
></b-pagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
12
admin/src/graphql/adminCreateContribution.js
Normal file
12
admin/src/graphql/adminCreateContribution.js
Normal file
@ -0,0 +1,12 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const adminCreateContribution = gql`
|
||||
mutation ($email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||
adminCreateContribution(
|
||||
email: $email
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
creationDate: $creationDate
|
||||
)
|
||||
}
|
||||
`
|
||||
11
admin/src/graphql/adminCreateContributions.js
Normal file
11
admin/src/graphql/adminCreateContributions.js
Normal file
@ -0,0 +1,11 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const adminCreateContributions = gql`
|
||||
mutation ($pendingCreations: [AdminCreateContributionArgs!]!) {
|
||||
adminCreateContributions(pendingCreations: $pendingCreations) {
|
||||
success
|
||||
successfulContribution
|
||||
failedContribution
|
||||
}
|
||||
}
|
||||
`
|
||||
7
admin/src/graphql/adminDeleteContribution.js
Normal file
7
admin/src/graphql/adminDeleteContribution.js
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const adminDeleteContribution = gql`
|
||||
mutation ($id: Int!) {
|
||||
adminDeleteContribution(id: $id)
|
||||
}
|
||||
`
|
||||
@ -1,8 +1,8 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const updatePendingCreation = gql`
|
||||
export const adminUpdateContribution = gql`
|
||||
mutation ($id: Int!, $email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||
updatePendingCreation(
|
||||
adminUpdateContribution(
|
||||
id: $id
|
||||
email: $email
|
||||
amount: $amount
|
||||
7
admin/src/graphql/confirmContribution.js
Normal file
7
admin/src/graphql/confirmContribution.js
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const confirmContribution = gql`
|
||||
mutation ($id: Int!) {
|
||||
confirmContribution(id: $id)
|
||||
}
|
||||
`
|
||||
@ -1,7 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const confirmPendingCreation = gql`
|
||||
mutation ($id: Int!) {
|
||||
confirmPendingCreation(id: $id)
|
||||
}
|
||||
`
|
||||
27
admin/src/graphql/createContributionLink.js
Normal file
27
admin/src/graphql/createContributionLink.js
Normal file
@ -0,0 +1,27 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const createContributionLink = gql`
|
||||
mutation (
|
||||
$validFrom: String!
|
||||
$validTo: String!
|
||||
$name: String!
|
||||
$amount: Decimal!
|
||||
$memo: String!
|
||||
$cycle: String!
|
||||
$maxPerCycle: Int! = 1
|
||||
$maxAmountPerMonth: Decimal
|
||||
) {
|
||||
createContributionLink(
|
||||
validFrom: $validFrom
|
||||
validTo: $validTo
|
||||
name: $name
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
cycle: $cycle
|
||||
maxPerCycle: $maxPerCycle
|
||||
maxAmountPerMonth: $maxAmountPerMonth
|
||||
) {
|
||||
link
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,7 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const createPendingCreation = gql`
|
||||
mutation ($email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||
createPendingCreation(email: $email, amount: $amount, memo: $memo, creationDate: $creationDate)
|
||||
}
|
||||
`
|
||||
@ -1,11 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const createPendingCreations = gql`
|
||||
mutation ($pendingCreations: [CreatePendingCreationArgs!]!) {
|
||||
createPendingCreations(pendingCreations: $pendingCreations) {
|
||||
success
|
||||
successfulCreation
|
||||
failedCreation
|
||||
}
|
||||
}
|
||||
`
|
||||
7
admin/src/graphql/deleteContributionLink.js
Normal file
7
admin/src/graphql/deleteContributionLink.js
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const deleteContributionLink = gql`
|
||||
mutation ($id: Int!) {
|
||||
deleteContributionLink(id: $id)
|
||||
}
|
||||
`
|
||||
@ -1,7 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const deletePendingCreation = gql`
|
||||
mutation ($id: Int!) {
|
||||
deletePendingCreation(id: $id)
|
||||
}
|
||||
`
|
||||
23
admin/src/graphql/listContributionLinks.js
Normal file
23
admin/src/graphql/listContributionLinks.js
Normal file
@ -0,0 +1,23 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const listContributionLinks = gql`
|
||||
query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
|
||||
listContributionLinks(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
|
||||
links {
|
||||
id
|
||||
amount
|
||||
name
|
||||
memo
|
||||
code
|
||||
link
|
||||
createdAt
|
||||
validFrom
|
||||
validTo
|
||||
maxAmountPerMonth
|
||||
cycle
|
||||
maxPerCycle
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,8 +1,8 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const getPendingCreations = gql`
|
||||
export const listUnconfirmedContributions = gql`
|
||||
query {
|
||||
getPendingCreations {
|
||||
listUnconfirmedContributions {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
@ -19,6 +19,7 @@ export const searchUsers = gql`
|
||||
hasElopage
|
||||
emailConfirmationSend
|
||||
deletedAt
|
||||
isAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
admin/src/graphql/setUserRole.js
Normal file
7
admin/src/graphql/setUserRole.js
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const setUserRole = gql`
|
||||
mutation ($userId: Int!, $isAdmin: Boolean!) {
|
||||
setUserRole(userId: $userId, isAdmin: $isAdmin)
|
||||
}
|
||||
`
|
||||
18
admin/src/graphql/showContributionLink.js
Normal file
18
admin/src/graphql/showContributionLink.js
Normal file
@ -0,0 +1,18 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const showContributionLink = gql`
|
||||
query ($id: Int!) {
|
||||
showContributionLink {
|
||||
id
|
||||
validFrom
|
||||
validTo
|
||||
name
|
||||
memo
|
||||
amount
|
||||
cycle
|
||||
maxPerCycle
|
||||
maxAmountPerMonth
|
||||
code
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,11 +1,41 @@
|
||||
{
|
||||
"all_emails": "Alle Nutzer",
|
||||
"back": "zurück",
|
||||
"contributionLink": {
|
||||
"amount": "Betrag",
|
||||
"clear": "Löschen",
|
||||
"contributionLinks": "Beitragslinks",
|
||||
"create": "Anlegen",
|
||||
"cycle": "Zyklus",
|
||||
"deleteNow": "Automatische Creations wirklich löschen?",
|
||||
"maximumAmount": "maximaler Betrag",
|
||||
"maxPerCycle": "Wiederholungen",
|
||||
"memo": "Nachricht",
|
||||
"name": "Name",
|
||||
"newContributionLink": "Neuer Beitragslink",
|
||||
"noContributionLinks": "Es sind keine Beitragslinks angelegt.",
|
||||
"noDateSelected": "Kein Datum ausgewählt",
|
||||
"noEndDate": "Kein Enddatum gewählt.",
|
||||
"noStartDate": "Kein Startdatum gewählt.",
|
||||
"options": {
|
||||
"cycle": {
|
||||
"daily": "täglich",
|
||||
"hourly": "stündlich",
|
||||
"monthly": "monatlich",
|
||||
"once": "einmalig",
|
||||
"weekly": "wöchentlich",
|
||||
"yearly": "jährlich"
|
||||
}
|
||||
},
|
||||
"validFrom": "Startdatum",
|
||||
"validTo": "Enddatum"
|
||||
},
|
||||
"creation": "Schöpfung",
|
||||
"creationList": "Schöpfungsliste",
|
||||
"creation_form": {
|
||||
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
|
||||
"creation_for": "Aktives Grundeinkommen für",
|
||||
"deleteNow": "Möchtest du diesen Beitrag zur Gemeinschaft wirklich löschen?",
|
||||
"enter_text": "Text eintragen",
|
||||
"form": "Schöpfungsformular",
|
||||
"min_characters": "Mindestens 10 Zeichen eingeben",
|
||||
@ -44,7 +74,8 @@
|
||||
"lastname": "Nachname",
|
||||
"math": {
|
||||
"exclaim": "!",
|
||||
"pipe": "|"
|
||||
"pipe": "|",
|
||||
"plus": "+"
|
||||
},
|
||||
"moderator": "Moderator",
|
||||
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
|
||||
@ -71,7 +102,7 @@
|
||||
},
|
||||
"redeemed": "eingelöst",
|
||||
"remove": "Entfernen",
|
||||
"removeNotSelf": "Als Admin / Moderator kannst du dich nicht selber löschen.",
|
||||
"removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.",
|
||||
"remove_all": "alle Nutzer entfernen",
|
||||
"save": "Speichern",
|
||||
"status": "Status",
|
||||
@ -101,6 +132,16 @@
|
||||
"text_false": " Die letzte Email wurde am {date} Uhr an das Mitglied ({email}) gesendet.",
|
||||
"text_true": " Die Email wurde bestätigt."
|
||||
},
|
||||
"userRole": {
|
||||
"notChangeYourSelf": "Als Admin/Moderator kannst du nicht selber deine Rolle ändern.",
|
||||
"selectLabel": "Rolle:",
|
||||
"selectRoles": {
|
||||
"admin": "Administrator",
|
||||
"user": "einfacher Nutzer"
|
||||
},
|
||||
"successfullyChangedTo": "Nutzer ist jetzt „{role}“.",
|
||||
"tabTitle": "Nutzer-Rolle"
|
||||
},
|
||||
"user_deleted": "Nutzer ist gelöscht.",
|
||||
"user_recovered": "Nutzer ist wiederhergestellt.",
|
||||
"user_search": "Nutzer-Suche"
|
||||
|
||||
@ -1,11 +1,41 @@
|
||||
{
|
||||
"all_emails": "All users",
|
||||
"back": "back",
|
||||
"contributionLink": {
|
||||
"amount": "Amount",
|
||||
"clear": "Clear",
|
||||
"contributionLinks": "Contribution Links",
|
||||
"create": "Create",
|
||||
"cycle": "Cycle",
|
||||
"deleteNow": "Do you really delete automatic creations?",
|
||||
"maximumAmount": "Maximum amount",
|
||||
"maxPerCycle": "Repetition",
|
||||
"memo": "Memo",
|
||||
"name": "Name",
|
||||
"newContributionLink": "New contribution link",
|
||||
"noContributionLinks": "No contribution link has been created.",
|
||||
"noDateSelected": "No date selected",
|
||||
"noEndDate": "No end-date",
|
||||
"noStartDate": "No start-date",
|
||||
"options": {
|
||||
"cycle": {
|
||||
"daily": "daily",
|
||||
"hourly": "hourly",
|
||||
"monthly": "monthly",
|
||||
"once": "once",
|
||||
"weekly": "weekly",
|
||||
"yearly": "yearly"
|
||||
}
|
||||
},
|
||||
"validFrom": "Start-date",
|
||||
"validTo": "End-Date"
|
||||
},
|
||||
"creation": "Creation",
|
||||
"creationList": "Creation list",
|
||||
"creation_form": {
|
||||
"creation_failed": "Could not create pending creation for {email}",
|
||||
"creation_for": "Active Basic Income for",
|
||||
"deleteNow": "Do you really want to delete this contribution to the community?",
|
||||
"enter_text": "Enter text",
|
||||
"form": "Creation form",
|
||||
"min_characters": "Enter at least 10 characters",
|
||||
@ -44,7 +74,8 @@
|
||||
"lastname": "Lastname",
|
||||
"math": {
|
||||
"exclaim": "!",
|
||||
"pipe": "|"
|
||||
"pipe": "|",
|
||||
"plus": "+"
|
||||
},
|
||||
"moderator": "Moderator",
|
||||
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
|
||||
@ -71,7 +102,7 @@
|
||||
},
|
||||
"redeemed": "redeemed",
|
||||
"remove": "Remove",
|
||||
"removeNotSelf": "As admin / moderator you cannot delete yourself.",
|
||||
"removeNotSelf": "As an admin/moderator, you cannot delete yourself.",
|
||||
"remove_all": "Remove all users",
|
||||
"save": "Speichern",
|
||||
"status": "Status",
|
||||
@ -101,6 +132,16 @@
|
||||
"text_false": "The last email was sent to the member ({email}) on {date}.",
|
||||
"text_true": "The email was confirmed."
|
||||
},
|
||||
"userRole": {
|
||||
"notChangeYourSelf": "As an admin/moderator, you cannot change your own role.",
|
||||
"selectLabel": "Role:",
|
||||
"selectRoles": {
|
||||
"admin": "administrator",
|
||||
"user": "usual user"
|
||||
},
|
||||
"successfullyChangedTo": "User is now \"{role}\".",
|
||||
"tabTitle": "User Role"
|
||||
},
|
||||
"user_deleted": "User is deleted.",
|
||||
"user_recovered": "User is recovered.",
|
||||
"user_search": "User search"
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
per-page="perPage"
|
||||
:total-rows="rows"
|
||||
align="center"
|
||||
:hide-ellipsis="true"
|
||||
></b-pagination>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="6" class="shadow p-3 mb-5 rounded bg-info">
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CreationConfirm from './CreationConfirm.vue'
|
||||
import { deletePendingCreation } from '../graphql/deletePendingCreation'
|
||||
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
|
||||
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
|
||||
import { confirmContribution } from '../graphql/confirmContribution'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
@ -9,7 +9,7 @@ const localVue = global.localVue
|
||||
const storeCommitMock = jest.fn()
|
||||
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
getPendingCreations: [
|
||||
listUnconfirmedContributions: [
|
||||
{
|
||||
id: 1,
|
||||
firstName: 'Bibi',
|
||||
@ -18,7 +18,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
amount: 500,
|
||||
memo: 'Danke für alles',
|
||||
date: new Date(),
|
||||
moderator: 0,
|
||||
moderator: 2,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@ -28,7 +28,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
amount: 1000000,
|
||||
memo: 'Gut Ergattert',
|
||||
date: new Date(),
|
||||
moderator: 0,
|
||||
moderator: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -80,28 +80,54 @@ describe('CreationConfirm', () => {
|
||||
})
|
||||
|
||||
describe('remove creation with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
|
||||
})
|
||||
let spy
|
||||
|
||||
it('calls the deletePendingCreation mutation', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith({
|
||||
mutation: deletePendingCreation,
|
||||
variables: { id: 1 },
|
||||
describe('admin confirms deletion', () => {
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve('some value'))
|
||||
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('opens a modal', () => {
|
||||
expect(spy).toBeCalled()
|
||||
})
|
||||
|
||||
it('calls the adminDeleteContribution mutation', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith({
|
||||
mutation: adminDeleteContribution,
|
||||
variables: { id: 1 },
|
||||
})
|
||||
})
|
||||
|
||||
it('commits openCreationsMinus to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_delete')
|
||||
})
|
||||
})
|
||||
|
||||
it('commits openCreationsMinus to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
|
||||
})
|
||||
describe('admin cancels deletion', () => {
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve(false))
|
||||
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_delete')
|
||||
it('does not call the adminDeleteContribution mutation', () => {
|
||||
expect(apolloMutateMock).not.toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('remove creation with error', () => {
|
||||
let spy
|
||||
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve('some value'))
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Ouchhh!' })
|
||||
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
|
||||
})
|
||||
@ -141,9 +167,9 @@ describe('CreationConfirm', () => {
|
||||
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
|
||||
})
|
||||
|
||||
it('calls the confirmPendingCreation mutation', () => {
|
||||
it('calls the confirmContribution mutation', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith({
|
||||
mutation: confirmPendingCreation,
|
||||
mutation: confirmContribution,
|
||||
variables: { id: 2 },
|
||||
})
|
||||
})
|
||||
|
||||
@ -15,9 +15,9 @@
|
||||
<script>
|
||||
import Overlay from '../components/Overlay.vue'
|
||||
import OpenCreationsTable from '../components/Tables/OpenCreationsTable.vue'
|
||||
import { getPendingCreations } from '../graphql/getPendingCreations'
|
||||
import { deletePendingCreation } from '../graphql/deletePendingCreation'
|
||||
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
|
||||
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions'
|
||||
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
|
||||
import { confirmContribution } from '../graphql/confirmContribution'
|
||||
|
||||
export default {
|
||||
name: 'CreationConfirm',
|
||||
@ -34,25 +34,28 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
removeCreation(item) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: deletePendingCreation,
|
||||
variables: {
|
||||
id: item.id,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.updatePendingCreations(item.id)
|
||||
this.toastSuccess(this.$t('creation_form.toasted_delete'))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
this.$bvModal.msgBoxConfirm(this.$t('creation_form.deleteNow')).then(async (value) => {
|
||||
if (value)
|
||||
await this.$apollo
|
||||
.mutate({
|
||||
mutation: adminDeleteContribution,
|
||||
variables: {
|
||||
id: item.id,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.updatePendingCreations(item.id)
|
||||
this.toastSuccess(this.$t('creation_form.toasted_delete'))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
})
|
||||
},
|
||||
confirmCreation() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: confirmPendingCreation,
|
||||
mutation: confirmContribution,
|
||||
variables: {
|
||||
id: this.item.id,
|
||||
},
|
||||
@ -70,13 +73,13 @@ export default {
|
||||
getPendingCreations() {
|
||||
this.$apollo
|
||||
.query({
|
||||
query: getPendingCreations,
|
||||
query: listUnconfirmedContributions,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
.then((result) => {
|
||||
this.$store.commit('resetOpenCreations')
|
||||
this.pendingCreations = result.data.getPendingCreations
|
||||
this.$store.commit('setOpenCreations', result.data.getPendingCreations.length)
|
||||
this.pendingCreations = result.data.listUnconfirmedContributions
|
||||
this.$store.commit('setOpenCreations', result.data.listUnconfirmedContributions.length)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
@ -114,7 +117,7 @@ export default {
|
||||
},
|
||||
},
|
||||
{ key: 'moderator', label: this.$t('moderator') },
|
||||
{ key: 'edit_creation', label: this.$t('edit') },
|
||||
{ key: 'editCreation', label: this.$t('edit') },
|
||||
{ key: 'confirm', label: this.$t('save') },
|
||||
]
|
||||
},
|
||||
|
||||
@ -5,7 +5,7 @@ const localVue = global.localVue
|
||||
|
||||
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
getPendingCreations: [
|
||||
listUnconfirmedContributions: [
|
||||
{
|
||||
pending: true,
|
||||
},
|
||||
@ -46,7 +46,7 @@ describe('Overview', () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('calls getPendingCreations', () => {
|
||||
it('calls listUnconfirmedContributions', () => {
|
||||
expect(apolloQueryMock).toBeCalled()
|
||||
})
|
||||
|
||||
|
||||
@ -28,27 +28,54 @@
|
||||
</b-link>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
<contribution-link :items="items" :count="count" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getPendingCreations } from '../graphql/getPendingCreations'
|
||||
import { listContributionLinks } from '@/graphql/listContributionLinks.js'
|
||||
import ContributionLink from '../components/ContributionLink.vue'
|
||||
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions'
|
||||
|
||||
export default {
|
||||
name: 'overview',
|
||||
components: {
|
||||
ContributionLink,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
count: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getPendingCreations() {
|
||||
this.$apollo
|
||||
.query({
|
||||
query: getPendingCreations,
|
||||
query: listUnconfirmedContributions,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
.then((result) => {
|
||||
this.$store.commit('setOpenCreations', result.data.getPendingCreations.length)
|
||||
this.$store.commit('setOpenCreations', result.data.listUnconfirmedContributions.length)
|
||||
})
|
||||
},
|
||||
async getContributionLinks() {
|
||||
this.$apollo
|
||||
.query({
|
||||
query: listContributionLinks,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
.then((result) => {
|
||||
this.count = result.data.listContributionLinks.count
|
||||
this.items = result.data.listContributionLinks.links
|
||||
})
|
||||
.catch(() => {
|
||||
this.toastError('listContributionLinks has no result, use default data')
|
||||
})
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getPendingCreations()
|
||||
this.getContributionLinks()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -199,14 +199,43 @@ describe('UserSearch', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('change user role', () => {
|
||||
const userId = 4
|
||||
|
||||
describe('to admin', () => {
|
||||
it('updates user role to admin', async () => {
|
||||
await wrapper
|
||||
.findComponent({ name: 'SearchUserTable' })
|
||||
.vm.$emit('updateIsAdmin', userId, new Date())
|
||||
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).isAdmin).toEqual(
|
||||
expect.any(Date),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('to usual user', () => {
|
||||
it('updates user role to usual user', async () => {
|
||||
await wrapper
|
||||
.findComponent({ name: 'SearchUserTable' })
|
||||
.vm.$emit('updateIsAdmin', userId, null)
|
||||
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).isAdmin).toEqual(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete user', () => {
|
||||
const now = new Date()
|
||||
beforeEach(async () => {
|
||||
wrapper.findComponent({ name: 'SearchUserTable' }).vm.$emit('updateDeletedAt', 4, now)
|
||||
const userId = 4
|
||||
beforeEach(() => {
|
||||
wrapper
|
||||
.findComponent({ name: 'SearchUserTable' })
|
||||
.vm.$emit('updateDeletedAt', userId, new Date())
|
||||
})
|
||||
|
||||
it('marks the user as deleted', () => {
|
||||
expect(wrapper.vm.searchResult.find((obj) => obj.userId === 4).deletedAt).toEqual(now)
|
||||
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).deletedAt).toEqual(
|
||||
expect.any(Date),
|
||||
)
|
||||
expect(wrapper.find('.test-deleted-icon').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
type="PageUserSearch"
|
||||
:items="searchResult"
|
||||
:fields="fields"
|
||||
@updateIsAdmin="updateIsAdmin"
|
||||
@updateDeletedAt="updateDeletedAt"
|
||||
/>
|
||||
<b-pagination
|
||||
@ -51,6 +52,7 @@
|
||||
per-page="perPage"
|
||||
:total-rows="rows"
|
||||
align="center"
|
||||
:hide-ellipsis="true"
|
||||
></b-pagination>
|
||||
<div></div>
|
||||
</div>
|
||||
@ -111,6 +113,9 @@ export default {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
updateIsAdmin(userId, isAdmin) {
|
||||
this.searchResult.find((obj) => obj.userId === userId).isAdmin = isAdmin
|
||||
},
|
||||
updateDeletedAt(userId, deletedAt) {
|
||||
this.searchResult.find((obj) => obj.userId === userId).deletedAt = deletedAt
|
||||
this.toastSuccess(deletedAt ? this.$t('user_deleted') : this.$t('user_recovered'))
|
||||
|
||||
@ -932,6 +932,13 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.11.2", "@babel/runtime@^7.16.0":
|
||||
version "7.18.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4"
|
||||
integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.14.0":
|
||||
version "7.17.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.7.tgz#a5f3328dc41ff39d803f311cfe17703418cf9825"
|
||||
@ -4082,9 +4089,9 @@ caniuse-api@^3.0.0:
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001271:
|
||||
version "1.0.30001271"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz#0dda0c9bcae2cf5407cd34cac304186616cc83e8"
|
||||
integrity sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA==
|
||||
version "1.0.30001354"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001354.tgz"
|
||||
integrity sha512-mImKeCkyGDAHNywYFA4bqnLAzTUvVkqPvhY4DV47X+Gl2c5Z8c3KNETnXp14GQt11LvxE8AwjzGxJ+rsikiOzg==
|
||||
|
||||
capture-exit@^2.0.0:
|
||||
version "2.0.0"
|
||||
@ -4397,7 +4404,7 @@ color-name@1.1.3:
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||
|
||||
color-name@^1.0.0, color-name@~1.1.4:
|
||||
color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
@ -4845,6 +4852,11 @@ cssesc@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
|
||||
|
||||
cssfontparser@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3"
|
||||
integrity sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==
|
||||
|
||||
cssnano-preset-default@^4.0.0, cssnano-preset-default@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz#920622b1fc1e95a34e8838203f1397a504f2d3ff"
|
||||
@ -7821,6 +7833,14 @@ javascript-stringify@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz#27c76539be14d8bd128219a2d731b09337904e79"
|
||||
integrity sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==
|
||||
|
||||
jest-canvas-mock@^2.3.1:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.4.0.tgz#947b71442d7719f8e055decaecdb334809465341"
|
||||
integrity sha512-mmMpZzpmLzn5vepIaHk5HoH3Ka4WykbSoLuG/EKoJd0x0ID/t+INo1l8ByfcUJuDM+RIsL4QDg/gDnBbrj2/IQ==
|
||||
dependencies:
|
||||
cssfontparser "^1.2.1"
|
||||
moo-color "^1.0.2"
|
||||
|
||||
jest-changed-files@^24.9.0:
|
||||
version "24.9.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039"
|
||||
@ -9478,6 +9498,13 @@ mkdirp@0.x, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
|
||||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
|
||||
moo-color@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.3.tgz#d56435f8359c8284d83ac58016df7427febece74"
|
||||
integrity sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==
|
||||
dependencies:
|
||||
color-name "^1.1.4"
|
||||
|
||||
move-concurrently@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
||||
@ -10959,6 +10986,27 @@ q@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
||||
|
||||
qrcanvas-vue@2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/qrcanvas-vue/-/qrcanvas-vue-2.1.1.tgz#27b449f99eaf46f324b300215469bfdf8ef77d88"
|
||||
integrity sha512-86NMjOJ5XJGrrqrD2t+zmZxZKNuW1Is7o88UOiM8qFxDBjuTyfq9VJE9/2rN5XxThsjBuY4bRrQqL9blVwnI9w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.0"
|
||||
qrcanvas "^3.1.2"
|
||||
|
||||
qrcanvas@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/qrcanvas/-/qrcanvas-3.1.2.tgz#81a25e91b2c27e9ace91da95591cbfb100d68702"
|
||||
integrity sha512-lNcAyCHN0Eno/mJ5eBc7lHV/5ejAJxII0UELthG3bNnlLR+u8hCc7CR+hXBawbYUf96kNIosXfG2cJzx92ZWKg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.2"
|
||||
qrcode-generator "^1.4.4"
|
||||
|
||||
qrcode-generator@^1.4.4:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.4.4.tgz#63f771224854759329a99048806a53ed278740e7"
|
||||
integrity sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==
|
||||
|
||||
qs@6.7.0:
|
||||
version "6.7.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
CONFIG_VERSION=v6.2022-04-21
|
||||
CONFIG_VERSION=v9.2022-07-07
|
||||
|
||||
# Server
|
||||
PORT=4000
|
||||
JWT_SECRET=secret123
|
||||
JWT_EXPIRES_IN=30m
|
||||
JWT_EXPIRES_IN=10m
|
||||
GRAPHIQL=false
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
|
||||
@ -28,6 +28,7 @@ COMMUNITY_NAME=Gradido Entwicklung
|
||||
COMMUNITY_URL=http://localhost/
|
||||
COMMUNITY_REGISTER_URL=http://localhost/register
|
||||
COMMUNITY_REDEEM_URL=http://localhost/redeem/{code}
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code}
|
||||
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
|
||||
|
||||
# Login Server
|
||||
@ -51,6 +52,9 @@ EMAIL_CODE_REQUEST_TIME=10
|
||||
# Webhook
|
||||
WEBHOOK_ELOPAGE_SECRET=secret
|
||||
|
||||
# EventProtocol
|
||||
EVENT_PROTOCOL_DISABLED=false
|
||||
|
||||
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
||||
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||
# LOG_LEVEL=info
|
||||
|
||||
@ -27,6 +27,7 @@ COMMUNITY_NAME=$COMMUNITY_NAME
|
||||
COMMUNITY_URL=$COMMUNITY_URL
|
||||
COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL
|
||||
COMMUNITY_REDEEM_URL=$COMMUNITY_REDEEM_URL
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL=$COMMUNITY_REDEEM_CONTRIBUTION_URL
|
||||
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
|
||||
|
||||
# Login Server
|
||||
@ -42,9 +43,13 @@ EMAIL_SMTP_URL=$EMAIL_SMTP_URL
|
||||
EMAIL_SMTP_PORT=587
|
||||
EMAIL_LINK_VERIFICATION=$EMAIL_LINK_VERIFICATION
|
||||
EMAIL_LINK_SETPASSWORD=$EMAIL_LINK_SETPASSWORD
|
||||
EMAIL_LINK_FORGOTPASSWORD=$EMAIL_LINK_FORGOTPASSWORD
|
||||
EMAIL_LINK_OVERVIEW=$EMAIL_LINK_OVERVIEW
|
||||
EMAIL_CODE_VALID_TIME=$EMAIL_CODE_VALID_TIME
|
||||
EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME
|
||||
|
||||
# Webhook
|
||||
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
|
||||
|
||||
# EventProtocol
|
||||
EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
# backend
|
||||
|
||||
## Project setup
|
||||
```
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Seed DB
|
||||
```
|
||||
|
||||
```bash
|
||||
yarn seed
|
||||
```
|
||||
Deletes all data in database. Then seeds data in database.
|
||||
|
||||
Deletes all data in database. Then seeds data in database.
|
||||
|
||||
## Seeded Users
|
||||
|
||||
@ -22,3 +24,47 @@ Deletes all data in database. Then seeds data in database.
|
||||
| bob@baumeister.de | `Aa12345_` | `false` | `true` | `false` |
|
||||
| garrick@ollivander.com | | `false` | `false` | `false` |
|
||||
| stephen@hawking.uk | `Aa12345_` | `false` | `true` | `true` |
|
||||
|
||||
## Setup GraphQL Playground
|
||||
|
||||
### Setup In The Code
|
||||
|
||||
Setting up the GraphQL Playground in our code requires the following steps:
|
||||
|
||||
- Create an empty `.env` file in the `backend` folder and set "GRAPHIQL=true" there.
|
||||
- Start or restart Docker Compose.
|
||||
- For verification, Docker should display `GraphQL available at http://localhost:4000` in the terminal.
|
||||
- If you open "http://localhost:4000/" in your browser, you should see the GraphQL Playground.
|
||||
|
||||
### Authentication
|
||||
|
||||
You need to authenticate yourself in GraphQL Playground to be able to send queries and mutations, to do so follow the steps below:
|
||||
|
||||
- in Firefox go to "Network Analysis" and delete all entries
|
||||
- enter and send the login query:
|
||||
|
||||
```gql
|
||||
{
|
||||
login(email: "bibi@bloxberg.de", password:"Aa12345_") {
|
||||
id
|
||||
publisherId
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
emailChecked
|
||||
language
|
||||
hasElopage
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- search in Firefox under „Network Analysis" for the smallest size of a header and copy the value of the token
|
||||
- open the header section in GraphQL Playground and set your current token by filling in and replacing `XXX`:
|
||||
|
||||
```qgl
|
||||
{
|
||||
"Authorization": "XXX"
|
||||
}
|
||||
```
|
||||
|
||||
Now you can open a new tap in the Playground and enter your query or mutation there.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-backend",
|
||||
"version": "1.9.0",
|
||||
"version": "1.11.0",
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
|
||||
@ -25,16 +25,27 @@ export enum RIGHTS {
|
||||
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
|
||||
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
|
||||
GDT_BALANCE = 'GDT_BALANCE',
|
||||
CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION',
|
||||
DELETE_CONTRIBUTION = 'DELETE_CONTRIBUTION',
|
||||
LIST_CONTRIBUTIONS = 'LIST_CONTRIBUTIONS',
|
||||
LIST_ALL_CONTRIBUTIONS = 'LIST_ALL_CONTRIBUTIONS',
|
||||
UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION',
|
||||
// Admin
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION',
|
||||
UPDATE_PENDING_CREATION = 'UPDATE_PENDING_CREATION',
|
||||
SEARCH_PENDING_CREATION = 'SEARCH_PENDING_CREATION',
|
||||
DELETE_PENDING_CREATION = 'DELETE_PENDING_CREATION',
|
||||
CONFIRM_PENDING_CREATION = 'CONFIRM_PENDING_CREATION',
|
||||
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
|
||||
SET_USER_ROLE = 'SET_USER_ROLE',
|
||||
DELETE_USER = 'DELETE_USER',
|
||||
UNDELETE_USER = 'UNDELETE_USER',
|
||||
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
|
||||
ADMIN_CREATE_CONTRIBUTIONS = 'ADMIN_CREATE_CONTRIBUTIONS',
|
||||
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
|
||||
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
|
||||
LIST_UNCONFIRMED_CONTRIBUTIONS = 'LIST_UNCONFIRMED_CONTRIBUTIONS',
|
||||
CONFIRM_CONTRIBUTION = 'CONFIRM_CONTRIBUTION',
|
||||
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
|
||||
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
|
||||
LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
|
||||
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
|
||||
LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS',
|
||||
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
|
||||
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
|
||||
}
|
||||
|
||||
@ -23,6 +23,11 @@ export const ROLE_USER = new Role('user', [
|
||||
RIGHTS.REDEEM_TRANSACTION_LINK,
|
||||
RIGHTS.LIST_TRANSACTION_LINKS,
|
||||
RIGHTS.GDT_BALANCE,
|
||||
RIGHTS.CREATE_CONTRIBUTION,
|
||||
RIGHTS.DELETE_CONTRIBUTION,
|
||||
RIGHTS.LIST_CONTRIBUTIONS,
|
||||
RIGHTS.LIST_ALL_CONTRIBUTIONS,
|
||||
RIGHTS.UPDATE_CONTRIBUTION,
|
||||
])
|
||||
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
||||
|
||||
|
||||
@ -10,14 +10,14 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0038-add_contribution_links_table',
|
||||
DB_VERSION: '0044-insert_missing_contributions',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v6.2022-04-21',
|
||||
EXPECTED: 'v9.2022-07-07',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -25,7 +25,7 @@ const constants = {
|
||||
const server = {
|
||||
PORT: process.env.PORT || 4000,
|
||||
JWT_SECRET: process.env.JWT_SECRET || 'secret123',
|
||||
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '30m',
|
||||
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
|
||||
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
|
||||
GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net',
|
||||
PRODUCTION: process.env.NODE_ENV === 'production' || false,
|
||||
@ -54,6 +54,8 @@ const community = {
|
||||
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/',
|
||||
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
|
||||
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}',
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL:
|
||||
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}',
|
||||
COMMUNITY_DESCRIPTION:
|
||||
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||
}
|
||||
@ -92,6 +94,11 @@ const webhook = {
|
||||
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
|
||||
}
|
||||
|
||||
const eventProtocol = {
|
||||
// global switch to enable writing of EventProtocol-Entries
|
||||
EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
|
||||
}
|
||||
|
||||
// This is needed by graphql-directive-auth
|
||||
process.env.APP_SECRET = server.JWT_SECRET
|
||||
|
||||
@ -116,6 +123,7 @@ const CONFIG = {
|
||||
...email,
|
||||
...loginServer,
|
||||
...webhook,
|
||||
...eventProtocol,
|
||||
}
|
||||
|
||||
export default CONFIG
|
||||
|
||||
301
backend/src/event/Event.ts
Normal file
301
backend/src/event/Event.ts
Normal file
@ -0,0 +1,301 @@
|
||||
import { EventProtocol } from '@entity/EventProtocol'
|
||||
import decimal from 'decimal.js-light'
|
||||
import { EventProtocolType } from './EventProtocolType'
|
||||
|
||||
export class EventBasic {
|
||||
type: string
|
||||
createdAt: Date
|
||||
}
|
||||
export class EventBasicUserId extends EventBasic {
|
||||
userId: number
|
||||
}
|
||||
|
||||
export class EventBasicTx extends EventBasicUserId {
|
||||
xUserId: number
|
||||
xCommunityId: number
|
||||
transactionId: number
|
||||
amount: decimal
|
||||
}
|
||||
|
||||
export class EventBasicCt extends EventBasicUserId {
|
||||
contributionId: number
|
||||
amount: decimal
|
||||
}
|
||||
|
||||
export class EventBasicRedeem extends EventBasicUserId {
|
||||
transactionId?: number
|
||||
contributionId?: number
|
||||
}
|
||||
|
||||
export class EventVisitGradido extends EventBasic {}
|
||||
export class EventRegister extends EventBasicUserId {}
|
||||
export class EventRedeemRegister extends EventBasicRedeem {}
|
||||
export class EventInactiveAccount extends EventBasicUserId {}
|
||||
export class EventSendConfirmationEmail extends EventBasicUserId {}
|
||||
export class EventConfirmationEmail extends EventBasicUserId {}
|
||||
export class EventRegisterEmailKlicktipp extends EventBasicUserId {}
|
||||
export class EventLogin extends EventBasicUserId {}
|
||||
export class EventRedeemLogin extends EventBasicRedeem {}
|
||||
export class EventActivateAccount extends EventBasicUserId {}
|
||||
export class EventPasswordChange extends EventBasicUserId {}
|
||||
export class EventTransactionSend extends EventBasicTx {}
|
||||
export class EventTransactionSendRedeem extends EventBasicTx {}
|
||||
export class EventTransactionRepeateRedeem extends EventBasicTx {}
|
||||
export class EventTransactionCreation extends EventBasicUserId {
|
||||
transactionId: number
|
||||
amount: decimal
|
||||
}
|
||||
export class EventTransactionReceive extends EventBasicTx {}
|
||||
export class EventTransactionReceiveRedeem extends EventBasicTx {}
|
||||
export class EventContributionCreate extends EventBasicCt {}
|
||||
export class EventContributionConfirm extends EventBasicCt {
|
||||
xUserId: number
|
||||
xCommunityId: number
|
||||
}
|
||||
export class EventContributionLinkDefine extends EventBasicCt {}
|
||||
export class EventContributionLinkActivateRedeem extends EventBasicCt {}
|
||||
|
||||
export class Event {
|
||||
constructor()
|
||||
constructor(event?: EventProtocol) {
|
||||
if (event) {
|
||||
this.id = event.id
|
||||
this.type = event.type
|
||||
this.createdAt = event.createdAt
|
||||
this.userId = event.userId
|
||||
this.xUserId = event.xUserId
|
||||
this.xCommunityId = event.xCommunityId
|
||||
this.transactionId = event.transactionId
|
||||
this.contributionId = event.contributionId
|
||||
this.amount = event.amount
|
||||
}
|
||||
}
|
||||
|
||||
public setEventBasic(): Event {
|
||||
this.type = EventProtocolType.BASIC
|
||||
this.createdAt = new Date()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventVisitGradido(): Event {
|
||||
this.setEventBasic()
|
||||
this.type = EventProtocolType.VISIT_GRADIDO
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventRegister(ev: EventRegister): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.REGISTER
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventRedeemRegister(ev: EventRedeemRegister): Event {
|
||||
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
|
||||
this.type = EventProtocolType.REDEEM_REGISTER
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventInactiveAccount(ev: EventInactiveAccount): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.INACTIVE_ACCOUNT
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendConfirmationEmail(ev: EventSendConfirmationEmail): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.SEND_CONFIRMATION_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventConfirmationEmail(ev: EventConfirmationEmail): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.CONFIRM_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventRegisterEmailKlicktipp(ev: EventRegisterEmailKlicktipp): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.REGISTER_EMAIL_KLICKTIPP
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventLogin(ev: EventLogin): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.LOGIN
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventRedeemLogin(ev: EventRedeemLogin): Event {
|
||||
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
|
||||
this.type = EventProtocolType.REDEEM_LOGIN
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventActivateAccount(ev: EventActivateAccount): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.ACTIVATE_ACCOUNT
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventPasswordChange(ev: EventPasswordChange): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.PASSWORD_CHANGE
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventTransactionSend(ev: EventTransactionSend): Event {
|
||||
this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount)
|
||||
this.type = EventProtocolType.TRANSACTION_SEND
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventTransactionSendRedeem(ev: EventTransactionSendRedeem): Event {
|
||||
this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount)
|
||||
this.type = EventProtocolType.TRANSACTION_SEND_REDEEM
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventTransactionRepeateRedeem(ev: EventTransactionRepeateRedeem): Event {
|
||||
this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount)
|
||||
this.type = EventProtocolType.TRANSACTION_REPEATE_REDEEM
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventTransactionCreation(ev: EventTransactionCreation): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
if (ev.transactionId) this.transactionId = ev.transactionId
|
||||
if (ev.amount) this.amount = ev.amount
|
||||
this.type = EventProtocolType.TRANSACTION_CREATION
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventTransactionReceive(ev: EventTransactionReceive): Event {
|
||||
this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount)
|
||||
this.type = EventProtocolType.TRANSACTION_RECEIVE
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventTransactionReceiveRedeem(ev: EventTransactionReceiveRedeem): Event {
|
||||
this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount)
|
||||
this.type = EventProtocolType.TRANSACTION_RECEIVE_REDEEM
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventContributionCreate(ev: EventContributionCreate): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.CONTRIBUTION_CREATE
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventContributionConfirm(ev: EventContributionConfirm): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
if (ev.xUserId) this.xUserId = ev.xUserId
|
||||
if (ev.xCommunityId) this.xCommunityId = ev.xCommunityId
|
||||
this.type = EventProtocolType.CONTRIBUTION_CONFIRM
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventContributionLinkDefine(ev: EventContributionLinkDefine): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.CONTRIBUTION_LINK_DEFINE
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventContributionLinkActivateRedeem(ev: EventContributionLinkActivateRedeem): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.CONTRIBUTION_LINK_ACTIVATE_REDEEM
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setByBasicUser(userId: number): Event {
|
||||
this.setEventBasic()
|
||||
this.userId = userId
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setByBasicTx(
|
||||
userId: number,
|
||||
xUserId?: number,
|
||||
xCommunityId?: number,
|
||||
transactionId?: number,
|
||||
amount?: decimal,
|
||||
): Event {
|
||||
this.setByBasicUser(userId)
|
||||
if (xUserId) this.xUserId = xUserId
|
||||
if (xCommunityId) this.xCommunityId = xCommunityId
|
||||
if (transactionId) this.transactionId = transactionId
|
||||
if (amount) this.amount = amount
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setByBasicCt(userId: number, contributionId: number, amount?: decimal): Event {
|
||||
this.setByBasicUser(userId)
|
||||
if (contributionId) this.contributionId = contributionId
|
||||
if (amount) this.amount = amount
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setByBasicRedeem(userId: number, transactionId?: number, contributionId?: number): Event {
|
||||
this.setByBasicUser(userId)
|
||||
if (transactionId) this.transactionId = transactionId
|
||||
if (contributionId) this.contributionId = contributionId
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setByEventTransactionCreation(event: EventTransactionCreation): Event {
|
||||
this.type = event.type
|
||||
this.createdAt = event.createdAt
|
||||
this.userId = event.userId
|
||||
this.transactionId = event.transactionId
|
||||
this.amount = event.amount
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setByEventContributionConfirm(event: EventContributionConfirm): Event {
|
||||
this.type = event.type
|
||||
this.createdAt = event.createdAt
|
||||
this.userId = event.userId
|
||||
this.xUserId = event.xUserId
|
||||
this.xCommunityId = event.xCommunityId
|
||||
this.amount = event.amount
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
id: number
|
||||
type: string
|
||||
createdAt: Date
|
||||
userId: number
|
||||
xUserId?: number
|
||||
xCommunityId?: number
|
||||
transactionId?: number
|
||||
contributionId?: number
|
||||
amount?: decimal
|
||||
}
|
||||
39
backend/src/event/EventProtocolEmitter.ts
Normal file
39
backend/src/event/EventProtocolEmitter.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Event } from '@/event/Event'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { EventProtocol } from '@entity/EventProtocol'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
class EventProtocolEmitter {
|
||||
/* }extends EventEmitter { */
|
||||
private events: Event[]
|
||||
|
||||
public addEvent(event: Event) {
|
||||
this.events.push(event)
|
||||
}
|
||||
|
||||
public getEvents(): Event[] {
|
||||
return this.events
|
||||
}
|
||||
|
||||
public isDisabled() {
|
||||
logger.info(`EventProtocol - isDisabled=${CONFIG.EVENT_PROTOCOL_DISABLED}`)
|
||||
return CONFIG.EVENT_PROTOCOL_DISABLED === true
|
||||
}
|
||||
|
||||
public async writeEvent(event: Event): Promise<void> {
|
||||
if (!eventProtocol.isDisabled()) {
|
||||
logger.info(`writeEvent(${JSON.stringify(event)})`)
|
||||
const dbEvent = new EventProtocol()
|
||||
dbEvent.type = event.type
|
||||
dbEvent.createdAt = event.createdAt
|
||||
dbEvent.userId = event.userId
|
||||
if (event.xUserId) dbEvent.xUserId = event.xUserId
|
||||
if (event.xCommunityId) dbEvent.xCommunityId = event.xCommunityId
|
||||
if (event.contributionId) dbEvent.contributionId = event.contributionId
|
||||
if (event.transactionId) dbEvent.transactionId = event.transactionId
|
||||
if (event.amount) dbEvent.amount = event.amount
|
||||
await dbEvent.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
export const eventProtocol = new EventProtocolEmitter()
|
||||
24
backend/src/event/EventProtocolType.ts
Normal file
24
backend/src/event/EventProtocolType.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export enum EventProtocolType {
|
||||
BASIC = 'BASIC',
|
||||
VISIT_GRADIDO = 'VISIT_GRADIDO',
|
||||
REGISTER = 'REGISTER',
|
||||
REDEEM_REGISTER = 'REDEEM_REGISTER',
|
||||
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
|
||||
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
|
||||
CONFIRM_EMAIL = 'CONFIRM_EMAIL',
|
||||
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
|
||||
LOGIN = 'LOGIN',
|
||||
REDEEM_LOGIN = 'REDEEM_LOGIN',
|
||||
ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT',
|
||||
PASSWORD_CHANGE = 'PASSWORD_CHANGE',
|
||||
TRANSACTION_SEND = 'TRANSACTION_SEND',
|
||||
TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM',
|
||||
TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM',
|
||||
TRANSACTION_CREATION = 'TRANSACTION_CREATION',
|
||||
TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE',
|
||||
TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM',
|
||||
CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE',
|
||||
CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM',
|
||||
CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE',
|
||||
CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM',
|
||||
}
|
||||
@ -3,7 +3,7 @@ import Decimal from 'decimal.js-light'
|
||||
|
||||
@InputType()
|
||||
@ArgsType()
|
||||
export default class CreatePendingCreationArgs {
|
||||
export default class AdminCreateContributionArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@ -2,7 +2,7 @@ import { ArgsType, Field, Int } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ArgsType()
|
||||
export default class UpdatePendingCreationArgs {
|
||||
export default class AdminUpdateContributionArgs {
|
||||
@Field(() => Int)
|
||||
id: number
|
||||
|
||||
15
backend/src/graphql/arg/ContributionArgs.ts
Normal file
15
backend/src/graphql/arg/ContributionArgs.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ArgsType, Field, InputType } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@InputType()
|
||||
@ArgsType()
|
||||
export default class ContributionArgs {
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@Field(() => String)
|
||||
creationDate: string
|
||||
}
|
||||
29
backend/src/graphql/arg/ContributionLinkArgs.ts
Normal file
29
backend/src/graphql/arg/ContributionLinkArgs.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ArgsType()
|
||||
export default class ContributionLinkArgs {
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
name: string
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@Field(() => String)
|
||||
cycle: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
validFrom?: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
validTo?: string | null
|
||||
|
||||
@Field(() => Decimal, { nullable: true })
|
||||
maxAmountPerMonth: Decimal | null
|
||||
|
||||
@Field(() => Int)
|
||||
maxPerCycle: number
|
||||
}
|
||||
28
backend/src/graphql/enum/ContributionCycleType.ts
Normal file
28
backend/src/graphql/enum/ContributionCycleType.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum ContributionCycleType {
|
||||
ONCE = 'once',
|
||||
HOUR = 'hour',
|
||||
TWO_HOURS = 'two_hours',
|
||||
FOUR_HOURS = 'four_hours',
|
||||
EIGHT_HOURS = 'eight_hours',
|
||||
HALF_DAY = 'half_day',
|
||||
DAY = 'day',
|
||||
TWO_DAYS = 'two_days',
|
||||
THREE_DAYS = 'three_days',
|
||||
FOUR_DAYS = 'four_days',
|
||||
FIVE_DAYS = 'five_days',
|
||||
SIX_DAYS = 'six_days',
|
||||
WEEK = 'week',
|
||||
TWO_WEEKS = 'two_weeks',
|
||||
MONTH = 'month',
|
||||
TWO_MONTH = 'two_month',
|
||||
QUARTER = 'quarter',
|
||||
HALF_YEAR = 'half_year',
|
||||
YEAR = 'year',
|
||||
}
|
||||
|
||||
registerEnumType(ContributionCycleType, {
|
||||
name: 'ContributionCycleType', // this one is mandatory
|
||||
description: 'Name of the Type of the ContributionCycle', // this one is optional
|
||||
})
|
||||
@ -1,19 +1,19 @@
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class CreatePendingCreations {
|
||||
export class AdminCreateContributions {
|
||||
constructor() {
|
||||
this.success = false
|
||||
this.successfulCreation = []
|
||||
this.failedCreation = []
|
||||
this.successfulContribution = []
|
||||
this.failedContribution = []
|
||||
}
|
||||
|
||||
@Field(() => Boolean)
|
||||
success: boolean
|
||||
|
||||
@Field(() => [String])
|
||||
successfulCreation: string[]
|
||||
successfulContribution: string[]
|
||||
|
||||
@Field(() => [String])
|
||||
failedCreation: string[]
|
||||
failedContribution: string[]
|
||||
}
|
||||
@ -2,7 +2,7 @@ import { ObjectType, Field } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ObjectType()
|
||||
export class UpdatePendingCreation {
|
||||
export class AdminUpdateContribution {
|
||||
@Field(() => Date)
|
||||
date: Date
|
||||
|
||||
64
backend/src/graphql/model/Contribution.ts
Normal file
64
backend/src/graphql/model/Contribution.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { Contribution as dbContribution } from '@entity/Contribution'
|
||||
import { User } from './User'
|
||||
|
||||
@ObjectType()
|
||||
export class Contribution {
|
||||
constructor(contribution: dbContribution, user: User) {
|
||||
this.id = contribution.id
|
||||
this.firstName = user ? user.firstName : null
|
||||
this.lastName = user ? user.lastName : null
|
||||
this.amount = contribution.amount
|
||||
this.memo = contribution.memo
|
||||
this.createdAt = contribution.createdAt
|
||||
this.deletedAt = contribution.deletedAt
|
||||
this.confirmedAt = contribution.confirmedAt
|
||||
this.confirmedBy = contribution.confirmedBy
|
||||
this.contributionDate = contribution.contributionDate
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
id: number
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
firstName: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
lastName: string | null
|
||||
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt: Date | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
confirmedAt: Date | null
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
confirmedBy: number | null
|
||||
|
||||
@Field(() => Date)
|
||||
contributionDate: Date
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class ContributionListResult {
|
||||
constructor(count: number, list: Contribution[]) {
|
||||
this.contributionCount = count
|
||||
this.contributionList = list
|
||||
}
|
||||
|
||||
@Field(() => Int)
|
||||
contributionCount: number
|
||||
|
||||
@Field(() => [Contribution])
|
||||
contributionList: Contribution[]
|
||||
}
|
||||
62
backend/src/graphql/model/ContributionLink.ts
Normal file
62
backend/src/graphql/model/ContributionLink.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
@ObjectType()
|
||||
export class ContributionLink {
|
||||
constructor(contributionLink: dbContributionLink) {
|
||||
this.id = contributionLink.id
|
||||
this.amount = contributionLink.amount
|
||||
this.name = contributionLink.name
|
||||
this.memo = contributionLink.memo
|
||||
this.createdAt = contributionLink.createdAt
|
||||
this.deletedAt = contributionLink.deletedAt
|
||||
this.validFrom = contributionLink.validFrom
|
||||
this.validTo = contributionLink.validTo
|
||||
this.maxAmountPerMonth = contributionLink.maxAmountPerMonth
|
||||
this.cycle = contributionLink.cycle
|
||||
this.maxPerCycle = contributionLink.maxPerCycle
|
||||
this.code = contributionLink.code
|
||||
this.link = CONFIG.COMMUNITY_REDEEM_CONTRIBUTION_URL.replace(/{code}/g, this.code)
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
id: number
|
||||
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
name: string
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@Field(() => String)
|
||||
code: string
|
||||
|
||||
@Field(() => String)
|
||||
link: string
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt: Date | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
validFrom: Date | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
validTo: Date | null
|
||||
|
||||
@Field(() => Decimal, { nullable: true })
|
||||
maxAmountPerMonth: Decimal | null
|
||||
|
||||
@Field(() => String)
|
||||
cycle: string
|
||||
|
||||
@Field(() => Int)
|
||||
maxPerCycle: number
|
||||
}
|
||||
11
backend/src/graphql/model/ContributionLinkList.ts
Normal file
11
backend/src/graphql/model/ContributionLinkList.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
|
||||
@ObjectType()
|
||||
export class ContributionLinkList {
|
||||
@Field(() => [ContributionLink])
|
||||
links: ContributionLink[]
|
||||
|
||||
@Field(() => Number)
|
||||
count: number
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ObjectType()
|
||||
export class PendingCreation {
|
||||
@Field(() => String)
|
||||
firstName: string
|
||||
|
||||
@Field(() => Int)
|
||||
id?: number
|
||||
|
||||
@Field(() => String)
|
||||
lastName: string
|
||||
|
||||
@Field(() => Number)
|
||||
userId: number
|
||||
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => Date)
|
||||
date: Date
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => Number)
|
||||
moderator: number
|
||||
|
||||
@Field(() => [Decimal])
|
||||
creation: Decimal[]
|
||||
}
|
||||
49
backend/src/graphql/model/UnconfirmedContribution.ts
Normal file
49
backend/src/graphql/model/UnconfirmedContribution.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { User } from '@entity/User'
|
||||
|
||||
@ObjectType()
|
||||
export class UnconfirmedContribution {
|
||||
constructor(contribution: Contribution, user: User, creations: Decimal[]) {
|
||||
this.id = contribution.id
|
||||
this.userId = contribution.userId
|
||||
this.amount = contribution.amount
|
||||
this.memo = contribution.memo
|
||||
this.date = contribution.contributionDate
|
||||
this.firstName = user ? user.firstName : ''
|
||||
this.lastName = user ? user.lastName : ''
|
||||
this.email = user ? user.email : ''
|
||||
this.creation = creations
|
||||
}
|
||||
|
||||
@Field(() => String)
|
||||
firstName: string
|
||||
|
||||
@Field(() => Int)
|
||||
id?: number
|
||||
|
||||
@Field(() => String)
|
||||
lastName: string
|
||||
|
||||
@Field(() => Number)
|
||||
userId: number
|
||||
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => Date)
|
||||
date: Date
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
moderator: number | null
|
||||
|
||||
@Field(() => [Decimal])
|
||||
creation: Decimal[]
|
||||
}
|
||||
@ -1,10 +1,12 @@
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { KlickTipp } from './KlickTipp'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { FULL_CREATION_AVAILABLE } from '../resolver/const/const'
|
||||
|
||||
@ObjectType()
|
||||
export class User {
|
||||
constructor(user: dbUser) {
|
||||
constructor(user: dbUser, creation: Decimal[] = FULL_CREATION_AVAILABLE) {
|
||||
this.id = user.id
|
||||
this.email = user.email
|
||||
this.firstName = user.firstName
|
||||
@ -17,6 +19,7 @@ export class User {
|
||||
this.isAdmin = user.isAdmin
|
||||
this.klickTipp = null
|
||||
this.hasElopage = null
|
||||
this.creation = creation
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
@ -64,4 +67,7 @@ export class User {
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
hasElopage: boolean | null
|
||||
|
||||
@Field(() => [Decimal])
|
||||
creation: Decimal[]
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ export class UserAdmin {
|
||||
this.hasElopage = hasElopage
|
||||
this.deletedAt = user.deletedAt
|
||||
this.emailConfirmationSend = emailConfirmationSend
|
||||
this.isAdmin = user.isAdmin
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
@ -42,6 +43,9 @@ export class UserAdmin {
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
emailConfirmationSend?: string
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
isAdmin: Date | null
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,5 @@
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx, Int } from 'type-graphql'
|
||||
import {
|
||||
getCustomRepository,
|
||||
@ -11,21 +12,25 @@ import {
|
||||
FindOperator,
|
||||
} from '@dbTools/typeorm'
|
||||
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
|
||||
import { PendingCreation } from '@model/PendingCreation'
|
||||
import { CreatePendingCreations } from '@model/CreatePendingCreations'
|
||||
import { UpdatePendingCreation } from '@model/UpdatePendingCreation'
|
||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||
import { AdminCreateContributions } from '@model/AdminCreateContributions'
|
||||
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
import { ContributionLinkList } from '@model/ContributionLinkList'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { UserRepository } from '@repository/User'
|
||||
import CreatePendingCreationArgs from '@arg/CreatePendingCreationArgs'
|
||||
import UpdatePendingCreationArgs from '@arg/UpdatePendingCreationArgs'
|
||||
import AdminCreateContributionArgs from '@arg/AdminCreateContributionArgs'
|
||||
import AdminUpdateContributionArgs from '@arg/AdminUpdateContributionArgs'
|
||||
import SearchUsersArgs from '@arg/SearchUsersArgs'
|
||||
import ContributionLinkArgs from '@arg/ContributionLinkArgs'
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { Transaction } from '@model/Transaction'
|
||||
import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { TransactionRepository } from '@repository/Transaction'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
@ -39,12 +44,25 @@ import { Order } from '@enum/Order'
|
||||
import { communityUser } from '@/util/communityUser'
|
||||
import { checkOptInCode, activationLink, printTimeDuration } from './UserResolver'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
||||
import CONFIG from '@/config'
|
||||
import {
|
||||
getUserCreation,
|
||||
getUserCreations,
|
||||
validateContribution,
|
||||
isStartEndDateValid,
|
||||
updateCreations,
|
||||
} from './util/creations'
|
||||
import {
|
||||
CONTRIBUTIONLINK_NAME_MAX_CHARS,
|
||||
CONTRIBUTIONLINK_NAME_MIN_CHARS,
|
||||
FULL_CREATION_AVAILABLE,
|
||||
MEMO_MAX_CHARS,
|
||||
MEMO_MIN_CHARS,
|
||||
} from './const/const'
|
||||
|
||||
// const EMAIL_OPT_IN_REGISTER = 1
|
||||
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
||||
const MAX_CREATION_AMOUNT = new Decimal(1000)
|
||||
const FULL_CREATION_AVAILABLE = [MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT]
|
||||
|
||||
@Resolver()
|
||||
export class AdminResolver {
|
||||
@ -67,7 +85,15 @@ export class AdminResolver {
|
||||
}
|
||||
}
|
||||
|
||||
const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked', 'deletedAt']
|
||||
const userFields = [
|
||||
'id',
|
||||
'firstName',
|
||||
'lastName',
|
||||
'email',
|
||||
'emailChecked',
|
||||
'deletedAt',
|
||||
'isAdmin',
|
||||
]
|
||||
const [users, count] = await userRepository.findBySearchCriteriaPagedFiltered(
|
||||
userFields.map((fieldName) => {
|
||||
return 'user.' + fieldName
|
||||
@ -127,6 +153,48 @@ export class AdminResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.SET_USER_ROLE])
|
||||
@Mutation(() => Date, { nullable: true })
|
||||
async setUserRole(
|
||||
@Arg('userId', () => Int)
|
||||
userId: number,
|
||||
@Arg('isAdmin', () => Boolean)
|
||||
isAdmin: boolean,
|
||||
@Ctx()
|
||||
context: Context,
|
||||
): Promise<Date | null> {
|
||||
const user = await dbUser.findOne({ id: userId })
|
||||
// user exists ?
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with userId: ${userId}`)
|
||||
}
|
||||
// administrator user changes own role?
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === userId) {
|
||||
throw new Error('Administrator can not change his own role!')
|
||||
}
|
||||
// change isAdmin
|
||||
switch (user.isAdmin) {
|
||||
case null:
|
||||
if (isAdmin === true) {
|
||||
user.isAdmin = new Date()
|
||||
} else {
|
||||
throw new Error('User is already a usual user!')
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (isAdmin === false) {
|
||||
user.isAdmin = null
|
||||
} else {
|
||||
throw new Error('User is already admin!')
|
||||
}
|
||||
break
|
||||
}
|
||||
await user.save()
|
||||
const newUser = await dbUser.findOne({ id: userId })
|
||||
return newUser ? newUser.isAdmin : null
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.DELETE_USER])
|
||||
@Mutation(() => Date, { nullable: true })
|
||||
async deleteUser(
|
||||
@ -163,10 +231,10 @@ export class AdminResolver {
|
||||
return null
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CREATE_PENDING_CREATION])
|
||||
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTION])
|
||||
@Mutation(() => [Number])
|
||||
async createPendingCreation(
|
||||
@Args() { email, amount, memo, creationDate }: CreatePendingCreationArgs,
|
||||
async adminCreateContribution(
|
||||
@Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<Decimal[]> {
|
||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
||||
@ -174,61 +242,63 @@ export class AdminResolver {
|
||||
throw new Error(`Could not find user with email: ${email}`)
|
||||
}
|
||||
if (user.deletedAt) {
|
||||
throw new Error('This user was deleted. Cannot make a creation.')
|
||||
throw new Error('This user was deleted. Cannot create a contribution.')
|
||||
}
|
||||
if (!user.emailChecked) {
|
||||
throw new Error('Creation could not be saved, Email is not activated')
|
||||
throw new Error('Contribution could not be saved, Email is not activated')
|
||||
}
|
||||
const moderator = getUser(context)
|
||||
logger.trace('moderator: ', moderator.id)
|
||||
const creations = await getUserCreation(user.id)
|
||||
logger.trace('creations', creations)
|
||||
const creationDateObj = new Date(creationDate)
|
||||
if (isCreationValid(creations, amount, creationDateObj)) {
|
||||
const adminPendingCreation = AdminPendingCreation.create()
|
||||
adminPendingCreation.userId = user.id
|
||||
adminPendingCreation.amount = amount
|
||||
adminPendingCreation.created = new Date()
|
||||
adminPendingCreation.date = creationDateObj
|
||||
adminPendingCreation.memo = memo
|
||||
adminPendingCreation.moderator = moderator.id
|
||||
validateContribution(creations, amount, creationDateObj)
|
||||
const contribution = Contribution.create()
|
||||
contribution.userId = user.id
|
||||
contribution.amount = amount
|
||||
contribution.createdAt = new Date()
|
||||
contribution.contributionDate = creationDateObj
|
||||
contribution.memo = memo
|
||||
contribution.moderatorId = moderator.id
|
||||
|
||||
await AdminPendingCreation.save(adminPendingCreation)
|
||||
}
|
||||
logger.trace('contribution to save', contribution)
|
||||
await Contribution.save(contribution)
|
||||
return getUserCreation(user.id)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CREATE_PENDING_CREATION])
|
||||
@Mutation(() => CreatePendingCreations)
|
||||
async createPendingCreations(
|
||||
@Arg('pendingCreations', () => [CreatePendingCreationArgs])
|
||||
pendingCreations: CreatePendingCreationArgs[],
|
||||
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTIONS])
|
||||
@Mutation(() => AdminCreateContributions)
|
||||
async adminCreateContributions(
|
||||
@Arg('pendingCreations', () => [AdminCreateContributionArgs])
|
||||
contributions: AdminCreateContributionArgs[],
|
||||
@Ctx() context: Context,
|
||||
): Promise<CreatePendingCreations> {
|
||||
): Promise<AdminCreateContributions> {
|
||||
let success = false
|
||||
const successfulCreation: string[] = []
|
||||
const failedCreation: string[] = []
|
||||
for (const pendingCreation of pendingCreations) {
|
||||
await this.createPendingCreation(pendingCreation, context)
|
||||
const successfulContribution: string[] = []
|
||||
const failedContribution: string[] = []
|
||||
for (const contribution of contributions) {
|
||||
await this.adminCreateContribution(contribution, context)
|
||||
.then(() => {
|
||||
successfulCreation.push(pendingCreation.email)
|
||||
successfulContribution.push(contribution.email)
|
||||
success = true
|
||||
})
|
||||
.catch(() => {
|
||||
failedCreation.push(pendingCreation.email)
|
||||
failedContribution.push(contribution.email)
|
||||
})
|
||||
}
|
||||
return {
|
||||
success,
|
||||
successfulCreation,
|
||||
failedCreation,
|
||||
successfulContribution,
|
||||
failedContribution,
|
||||
}
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.UPDATE_PENDING_CREATION])
|
||||
@Mutation(() => UpdatePendingCreation)
|
||||
async updatePendingCreation(
|
||||
@Args() { id, email, amount, memo, creationDate }: UpdatePendingCreationArgs,
|
||||
@Authorized([RIGHTS.ADMIN_UPDATE_CONTRIBUTION])
|
||||
@Mutation(() => AdminUpdateContribution)
|
||||
async adminUpdateContribution(
|
||||
@Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<UpdatePendingCreation> {
|
||||
): Promise<AdminUpdateContribution> {
|
||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with email: ${email}`)
|
||||
@ -239,59 +309,69 @@ export class AdminResolver {
|
||||
|
||||
const moderator = getUser(context)
|
||||
|
||||
const pendingCreationToUpdate = await AdminPendingCreation.findOne({ id })
|
||||
const contributionToUpdate = await Contribution.findOne({
|
||||
where: { id, confirmedAt: IsNull() },
|
||||
})
|
||||
|
||||
if (!pendingCreationToUpdate) {
|
||||
throw new Error('No creation found to given id.')
|
||||
if (!contributionToUpdate) {
|
||||
throw new Error('No contribution found to given id.')
|
||||
}
|
||||
|
||||
if (pendingCreationToUpdate.userId !== user.id) {
|
||||
throw new Error('user of the pending creation and send user does not correspond')
|
||||
if (contributionToUpdate.userId !== user.id) {
|
||||
throw new Error('user of the pending contribution and send user does not correspond')
|
||||
}
|
||||
|
||||
if (contributionToUpdate.moderatorId === null) {
|
||||
throw new Error('An admin is not allowed to update a user contribution.')
|
||||
}
|
||||
|
||||
const creationDateObj = new Date(creationDate)
|
||||
let creations = await getUserCreation(user.id)
|
||||
if (pendingCreationToUpdate.date.getMonth() === creationDateObj.getMonth()) {
|
||||
creations = updateCreations(creations, pendingCreationToUpdate)
|
||||
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
|
||||
creations = updateCreations(creations, contributionToUpdate)
|
||||
}
|
||||
|
||||
// all possible cases not to be true are thrown in this function
|
||||
isCreationValid(creations, amount, creationDateObj)
|
||||
pendingCreationToUpdate.amount = amount
|
||||
pendingCreationToUpdate.memo = memo
|
||||
pendingCreationToUpdate.date = new Date(creationDate)
|
||||
pendingCreationToUpdate.moderator = moderator.id
|
||||
validateContribution(creations, amount, creationDateObj)
|
||||
contributionToUpdate.amount = amount
|
||||
contributionToUpdate.memo = memo
|
||||
contributionToUpdate.contributionDate = new Date(creationDate)
|
||||
contributionToUpdate.moderatorId = moderator.id
|
||||
|
||||
await AdminPendingCreation.save(pendingCreationToUpdate)
|
||||
const result = new UpdatePendingCreation()
|
||||
await Contribution.save(contributionToUpdate)
|
||||
const result = new AdminUpdateContribution()
|
||||
result.amount = amount
|
||||
result.memo = pendingCreationToUpdate.memo
|
||||
result.date = pendingCreationToUpdate.date
|
||||
result.memo = contributionToUpdate.memo
|
||||
result.date = contributionToUpdate.contributionDate
|
||||
|
||||
result.creation = await getUserCreation(user.id)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.SEARCH_PENDING_CREATION])
|
||||
@Query(() => [PendingCreation])
|
||||
async getPendingCreations(): Promise<PendingCreation[]> {
|
||||
const pendingCreations = await AdminPendingCreation.find()
|
||||
if (pendingCreations.length === 0) {
|
||||
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
|
||||
@Query(() => [UnconfirmedContribution])
|
||||
async listUnconfirmedContributions(): Promise<UnconfirmedContribution[]> {
|
||||
const contributions = await Contribution.find({ where: { confirmedAt: IsNull() } })
|
||||
if (contributions.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const userIds = pendingCreations.map((p) => p.userId)
|
||||
const userIds = contributions.map((p) => p.userId)
|
||||
const userCreations = await getUserCreations(userIds)
|
||||
const users = await dbUser.find({ where: { id: In(userIds) }, withDeleted: true })
|
||||
|
||||
return pendingCreations.map((pendingCreation) => {
|
||||
const user = users.find((u) => u.id === pendingCreation.userId)
|
||||
const creation = userCreations.find((c) => c.id === pendingCreation.userId)
|
||||
return contributions.map((contribution) => {
|
||||
const user = users.find((u) => u.id === contribution.userId)
|
||||
const creation = userCreations.find((c) => c.id === contribution.userId)
|
||||
|
||||
return {
|
||||
...pendingCreation,
|
||||
amount: pendingCreation.amount,
|
||||
id: contribution.id,
|
||||
userId: contribution.userId,
|
||||
date: contribution.contributionDate,
|
||||
memo: contribution.memo,
|
||||
amount: contribution.amount,
|
||||
moderator: contribution.moderatorId,
|
||||
firstName: user ? user.firstName : '',
|
||||
lastName: user ? user.lastName : '',
|
||||
email: user ? user.email : '',
|
||||
@ -300,69 +380,91 @@ export class AdminResolver {
|
||||
})
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.DELETE_PENDING_CREATION])
|
||||
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])
|
||||
@Mutation(() => Boolean)
|
||||
async deletePendingCreation(@Arg('id', () => Int) id: number): Promise<boolean> {
|
||||
const pendingCreation = await AdminPendingCreation.findOne(id)
|
||||
if (!pendingCreation) {
|
||||
throw new Error('Creation not found for given id.')
|
||||
async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise<boolean> {
|
||||
const contribution = await Contribution.findOne(id)
|
||||
if (!contribution) {
|
||||
throw new Error('Contribution not found for given id.')
|
||||
}
|
||||
const res = await AdminPendingCreation.delete(pendingCreation)
|
||||
const res = await contribution.softRemove()
|
||||
return !!res
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CONFIRM_PENDING_CREATION])
|
||||
@Authorized([RIGHTS.CONFIRM_CONTRIBUTION])
|
||||
@Mutation(() => Boolean)
|
||||
async confirmPendingCreation(
|
||||
async confirmContribution(
|
||||
@Arg('id', () => Int) id: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const pendingCreation = await AdminPendingCreation.findOne(id)
|
||||
if (!pendingCreation) {
|
||||
throw new Error('Creation not found to given id.')
|
||||
const contribution = await Contribution.findOne(id)
|
||||
if (!contribution) {
|
||||
throw new Error('Contribution not found to given id.')
|
||||
}
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === pendingCreation.userId)
|
||||
throw new Error('Moderator can not confirm own pending creation')
|
||||
if (moderatorUser.id === contribution.userId)
|
||||
throw new Error('Moderator can not confirm own contribution')
|
||||
|
||||
const user = await dbUser.findOneOrFail({ id: pendingCreation.userId }, { withDeleted: true })
|
||||
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a creation.')
|
||||
const user = await dbUser.findOneOrFail({ id: contribution.userId }, { withDeleted: true })
|
||||
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a contribution.')
|
||||
|
||||
const creations = await getUserCreation(pendingCreation.userId, false)
|
||||
if (!isCreationValid(creations, pendingCreation.amount, pendingCreation.date)) {
|
||||
throw new Error('Creation is not valid!!')
|
||||
}
|
||||
const creations = await getUserCreation(contribution.userId, false)
|
||||
validateContribution(creations, contribution.amount, contribution.contributionDate)
|
||||
|
||||
const receivedCallDate = new Date()
|
||||
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const lastTransaction = await transactionRepository.findLastForUser(pendingCreation.userId)
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
try {
|
||||
const lastTransaction = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('transaction')
|
||||
.from(DbTransaction, 'transaction')
|
||||
.where('transaction.userId = :id', { id: contribution.userId })
|
||||
.orderBy('transaction.balanceDate', 'DESC')
|
||||
.getOne()
|
||||
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
|
||||
|
||||
let newBalance = new Decimal(0)
|
||||
let decay: Decay | null = null
|
||||
if (lastTransaction) {
|
||||
decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, receivedCallDate)
|
||||
newBalance = decay.balance
|
||||
let newBalance = new Decimal(0)
|
||||
let decay: Decay | null = null
|
||||
if (lastTransaction) {
|
||||
decay = calculateDecay(
|
||||
lastTransaction.balance,
|
||||
lastTransaction.balanceDate,
|
||||
receivedCallDate,
|
||||
)
|
||||
newBalance = decay.balance
|
||||
}
|
||||
newBalance = newBalance.add(contribution.amount.toString())
|
||||
|
||||
const transaction = new DbTransaction()
|
||||
transaction.typeId = TransactionTypeId.CREATION
|
||||
transaction.memo = contribution.memo
|
||||
transaction.userId = contribution.userId
|
||||
transaction.previous = lastTransaction ? lastTransaction.id : null
|
||||
transaction.amount = contribution.amount
|
||||
transaction.creationDate = contribution.contributionDate
|
||||
transaction.balance = newBalance
|
||||
transaction.balanceDate = receivedCallDate
|
||||
transaction.decay = decay ? decay.decay : new Decimal(0)
|
||||
transaction.decayStart = decay ? decay.start : null
|
||||
await queryRunner.manager.insert(DbTransaction, transaction)
|
||||
|
||||
contribution.confirmedAt = receivedCallDate
|
||||
contribution.confirmedBy = moderatorUser.id
|
||||
contribution.transactionId = transaction.id
|
||||
await queryRunner.manager.update(Contribution, { id: contribution.id }, contribution)
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info('creation commited successfuly.')
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error(`Creation was not successful: ${e}`)
|
||||
throw new Error(`Creation was not successful.`)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
newBalance = newBalance.add(pendingCreation.amount.toString())
|
||||
|
||||
const transaction = new DbTransaction()
|
||||
transaction.typeId = TransactionTypeId.CREATION
|
||||
transaction.memo = pendingCreation.memo
|
||||
transaction.userId = pendingCreation.userId
|
||||
transaction.previous = lastTransaction ? lastTransaction.id : null
|
||||
transaction.amount = pendingCreation.amount
|
||||
transaction.creationDate = pendingCreation.date
|
||||
transaction.balance = newBalance
|
||||
transaction.balanceDate = receivedCallDate
|
||||
transaction.decay = decay ? decay.decay : new Decimal(0)
|
||||
transaction.decayStart = decay ? decay.start : null
|
||||
await transaction.save().catch(() => {
|
||||
throw new Error('Unable to confirm creation.')
|
||||
})
|
||||
|
||||
await AdminPendingCreation.delete(pendingCreation)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@ -460,96 +562,130 @@ export class AdminResolver {
|
||||
linkList: transactionLinks.map((tl) => new TransactionLink(tl, new User(user))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface CreationMap {
|
||||
id: number
|
||||
creations: Decimal[]
|
||||
}
|
||||
|
||||
async function getUserCreation(id: number, includePending = true): Promise<Decimal[]> {
|
||||
const creations = await getUserCreations([id], includePending)
|
||||
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
||||
}
|
||||
|
||||
async function getUserCreations(ids: number[], includePending = true): Promise<CreationMap[]> {
|
||||
const months = getCreationMonths()
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
|
||||
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
||||
|
||||
const unionString = includePending
|
||||
? `
|
||||
UNION
|
||||
SELECT date AS date, amount AS amount, userId AS userId FROM admin_pending_creations
|
||||
WHERE userId IN (${ids.toString()})
|
||||
AND date >= ${dateFilter}`
|
||||
: ''
|
||||
|
||||
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
|
||||
`)
|
||||
|
||||
await queryRunner.release()
|
||||
|
||||
return ids.map((id) => {
|
||||
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,
|
||||
)
|
||||
return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0)
|
||||
}),
|
||||
@Authorized([RIGHTS.CREATE_CONTRIBUTION_LINK])
|
||||
@Mutation(() => ContributionLink)
|
||||
async createContributionLink(
|
||||
@Args()
|
||||
{
|
||||
amount,
|
||||
name,
|
||||
memo,
|
||||
cycle,
|
||||
validFrom,
|
||||
validTo,
|
||||
maxAmountPerMonth,
|
||||
maxPerCycle,
|
||||
}: ContributionLinkArgs,
|
||||
): Promise<ContributionLink> {
|
||||
isStartEndDateValid(validFrom, validTo)
|
||||
if (!name) {
|
||||
logger.error(`The name must be initialized!`)
|
||||
throw new Error(`The name must be initialized!`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateCreations(creations: Decimal[], pendingCreation: AdminPendingCreation): Decimal[] {
|
||||
const index = getCreationIndex(pendingCreation.date.getMonth())
|
||||
|
||||
if (index < 0) {
|
||||
throw new Error('You cannot create GDD for a month older than the last three months.')
|
||||
}
|
||||
creations[index] = creations[index].plus(pendingCreation.amount.toString())
|
||||
return creations
|
||||
}
|
||||
|
||||
function isCreationValid(creations: Decimal[], amount: Decimal, creationDate: Date) {
|
||||
const index = getCreationIndex(creationDate.getMonth())
|
||||
|
||||
if (index < 0) {
|
||||
throw new Error('No information for available creations for the given date')
|
||||
if (
|
||||
name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS ||
|
||||
name.length > CONTRIBUTIONLINK_NAME_MAX_CHARS
|
||||
) {
|
||||
const msg = `The value of 'name' with a length of ${name.length} did not fulfill the requested bounderies min=${CONTRIBUTIONLINK_NAME_MIN_CHARS} and max=${CONTRIBUTIONLINK_NAME_MAX_CHARS}`
|
||||
logger.error(`${msg}`)
|
||||
throw new Error(`${msg}`)
|
||||
}
|
||||
if (!memo) {
|
||||
logger.error(`The memo must be initialized!`)
|
||||
throw new Error(`The memo must be initialized!`)
|
||||
}
|
||||
if (memo.length < MEMO_MIN_CHARS || memo.length > MEMO_MAX_CHARS) {
|
||||
const msg = `The value of 'memo' with a length of ${memo.length} did not fulfill the requested bounderies min=${MEMO_MIN_CHARS} and max=${MEMO_MAX_CHARS}`
|
||||
logger.error(`${msg}`)
|
||||
throw new Error(`${msg}`)
|
||||
}
|
||||
if (!amount) {
|
||||
logger.error(`The amount must be initialized!`)
|
||||
throw new Error('The amount must be initialized!')
|
||||
}
|
||||
if (!new Decimal(amount).isPositive()) {
|
||||
logger.error(`The amount=${amount} must be initialized with a positiv value!`)
|
||||
throw new Error(`The amount=${amount} must be initialized with a positiv value!`)
|
||||
}
|
||||
const dbContributionLink = new DbContributionLink()
|
||||
dbContributionLink.amount = amount
|
||||
dbContributionLink.name = name
|
||||
dbContributionLink.memo = memo
|
||||
dbContributionLink.createdAt = new Date()
|
||||
dbContributionLink.code = contributionLinkCode(dbContributionLink.createdAt)
|
||||
dbContributionLink.cycle = cycle
|
||||
if (validFrom) dbContributionLink.validFrom = new Date(validFrom)
|
||||
if (validTo) dbContributionLink.validTo = new Date(validTo)
|
||||
dbContributionLink.maxAmountPerMonth = maxAmountPerMonth
|
||||
dbContributionLink.maxPerCycle = maxPerCycle
|
||||
await dbContributionLink.save()
|
||||
logger.debug(`createContributionLink successful!`)
|
||||
return new ContributionLink(dbContributionLink)
|
||||
}
|
||||
|
||||
if (amount.greaterThan(creations[index].toString())) {
|
||||
throw new Error(
|
||||
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
||||
)
|
||||
@Authorized([RIGHTS.LIST_CONTRIBUTION_LINKS])
|
||||
@Query(() => ContributionLinkList)
|
||||
async listContributionLinks(
|
||||
@Args()
|
||||
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||
): Promise<ContributionLinkList> {
|
||||
const [links, count] = await DbContributionLink.findAndCount({
|
||||
order: { createdAt: order },
|
||||
skip: (currentPage - 1) * pageSize,
|
||||
take: pageSize,
|
||||
})
|
||||
return {
|
||||
links: links.map((link: DbContributionLink) => new ContributionLink(link)),
|
||||
count,
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@Authorized([RIGHTS.DELETE_CONTRIBUTION_LINK])
|
||||
@Mutation(() => Date, { nullable: true })
|
||||
async deleteContributionLink(@Arg('id', () => Int) id: number): Promise<Date | null> {
|
||||
const contributionLink = await DbContributionLink.findOne(id)
|
||||
if (!contributionLink) {
|
||||
logger.error(`Contribution Link not found to given id: ${id}`)
|
||||
throw new Error('Contribution Link not found to given id.')
|
||||
}
|
||||
await contributionLink.softRemove()
|
||||
logger.debug(`deleteContributionLink successful!`)
|
||||
const newContributionLink = await DbContributionLink.findOne({ id }, { withDeleted: true })
|
||||
return newContributionLink ? newContributionLink.deletedAt : null
|
||||
}
|
||||
|
||||
const getCreationMonths = (): number[] => {
|
||||
const now = new Date(Date.now())
|
||||
return [
|
||||
now.getMonth() + 1,
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, 1).getMonth() + 1,
|
||||
new Date(now.getFullYear(), now.getMonth() - 2, 1).getMonth() + 1,
|
||||
].reverse()
|
||||
}
|
||||
|
||||
const getCreationIndex = (month: number): number => {
|
||||
return getCreationMonths().findIndex((el) => el === month + 1)
|
||||
@Authorized([RIGHTS.UPDATE_CONTRIBUTION_LINK])
|
||||
@Mutation(() => ContributionLink)
|
||||
async updateContributionLink(
|
||||
@Args()
|
||||
{
|
||||
amount,
|
||||
name,
|
||||
memo,
|
||||
cycle,
|
||||
validFrom,
|
||||
validTo,
|
||||
maxAmountPerMonth,
|
||||
maxPerCycle,
|
||||
}: ContributionLinkArgs,
|
||||
@Arg('id', () => Int) id: number,
|
||||
): Promise<ContributionLink> {
|
||||
const dbContributionLink = await DbContributionLink.findOne(id)
|
||||
if (!dbContributionLink) {
|
||||
logger.error(`Contribution Link not found to given id: ${id}`)
|
||||
throw new Error('Contribution Link not found to given id.')
|
||||
}
|
||||
dbContributionLink.amount = amount
|
||||
dbContributionLink.name = name
|
||||
dbContributionLink.memo = memo
|
||||
dbContributionLink.cycle = cycle
|
||||
if (validFrom) dbContributionLink.validFrom = new Date(validFrom)
|
||||
if (validTo) dbContributionLink.validTo = new Date(validTo)
|
||||
dbContributionLink.maxAmountPerMonth = maxAmountPerMonth
|
||||
dbContributionLink.maxPerCycle = maxPerCycle
|
||||
await dbContributionLink.save()
|
||||
logger.debug(`updateContributionLink successful!`)
|
||||
return new ContributionLink(dbContributionLink)
|
||||
}
|
||||
}
|
||||
|
||||
735
backend/src/graphql/resolver/ContributionResolver.test.ts
Normal file
735
backend/src/graphql/resolver/ContributionResolver.test.ts
Normal file
@ -0,0 +1,735 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import {
|
||||
adminUpdateContribution,
|
||||
confirmContribution,
|
||||
createContribution,
|
||||
deleteContribution,
|
||||
updateContribution,
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import { listAllContributions, listContributions, login } from '@/seeds/graphql/queries'
|
||||
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { creationFactory } from '@/seeds/factory/creation'
|
||||
import { creations } from '@/seeds/creation/index'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
|
||||
let mutate: any, query: any, con: any
|
||||
let testEnv: any
|
||||
let result: any
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.close()
|
||||
})
|
||||
|
||||
describe('ContributionResolver', () => {
|
||||
describe('createContribution', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContribution,
|
||||
variables: { amount: 100.0, memo: 'Test Contribution', creationDate: 'not-valid' },
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated with valid user', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
describe('input not valid', () => {
|
||||
it('throws error when memo length smaller than 5 chars', async () => {
|
||||
const date = new Date()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test',
|
||||
creationDate: date.toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too short (5 characters minimum)')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error when memo length greater than 255 chars', async () => {
|
||||
const date = new Date()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
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 Test',
|
||||
creationDate: date.toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too long (255 characters maximum)')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error when creationDate not-valid', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: 'not-valid',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError('No information for available creations for the given date'),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error when creationDate 3 month behind', async () => {
|
||||
const date = new Date()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: date.setMonth(date.getMonth() - 3).toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError('No information for available creations for the given date'),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('valid input', () => {
|
||||
it('creates contribution', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
createContribution: {
|
||||
id: expect.any(Number),
|
||||
amount: '100',
|
||||
memo: 'Test env contribution',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('listContributions', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
order: 'DESC',
|
||||
filterConfirmed: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, peterLustig)
|
||||
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await creationFactory(testEnv, bibisCreation!)
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
describe('filter confirmed is false', () => {
|
||||
it('returns creations', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
order: 'DESC',
|
||||
filterConfirmed: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listContributions: {
|
||||
contributionCount: 2,
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
amount: '1000',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
memo: 'Test env contribution',
|
||||
amount: '100',
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('filter confirmed is true', () => {
|
||||
it('returns only unconfirmed creations', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
order: 'DESC',
|
||||
filterConfirmed: true,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listContributions: {
|
||||
contributionCount: 1,
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
memo: 'Test env contribution',
|
||||
amount: '100',
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateContribution', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: 1,
|
||||
amount: 100.0,
|
||||
memo: 'Test Contribution',
|
||||
creationDate: 'not-valid',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, peterLustig)
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
result = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
describe('wrong contribution id', () => {
|
||||
it('throws an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: -1,
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('No contribution found to given id.')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Memo length smaller than 5 chars', () => {
|
||||
it('throws error', async () => {
|
||||
const date = new Date()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: result.data.createContribution.id,
|
||||
amount: 100.0,
|
||||
memo: 'Test',
|
||||
creationDate: date.toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too short (5 characters minimum)')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Memo length greater than 255 chars', () => {
|
||||
it('throws error', async () => {
|
||||
const date = new Date()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: result.data.createContribution.id,
|
||||
amount: 100.0,
|
||||
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 Test',
|
||||
creationDate: date.toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too long (255 characters maximum)')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('wrong user tries to update the contribution', () => {
|
||||
beforeAll(async () => {
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
it('throws an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: result.data.createContribution.id,
|
||||
amount: 10.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'user of the pending contribution and send user does not correspond',
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('admin tries to update a user contribution', () => {
|
||||
it('throws an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: adminUpdateContribution,
|
||||
variables: {
|
||||
id: result.data.createContribution.id,
|
||||
email: 'bibi@bloxberg.de',
|
||||
amount: 10.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('An admin is not allowed to update a user contribution.')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('update too much so that the limit is exceeded', () => {
|
||||
beforeAll(async () => {
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
it('throws an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: result.data.createContribution.id,
|
||||
amount: 1019.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'The amount (1019 GDD) to be created exceeds the amount (1000 GDD) still available for this month.',
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('update creation to a date that is older than 3 months', () => {
|
||||
it('throws an error', async () => {
|
||||
const date = new Date()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: result.data.createContribution.id,
|
||||
amount: 10.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: date.setMonth(date.getMonth() - 3).toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError('No information for available creations for the given date'),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('valid input', () => {
|
||||
it('updates contribution', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: result.data.createContribution.id,
|
||||
amount: 10.0,
|
||||
memo: 'Test contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
updateContribution: {
|
||||
id: result.data.createContribution.id,
|
||||
amount: '10',
|
||||
memo: 'Test contribution',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('listAllContribution', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listAllContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
order: 'DESC',
|
||||
filterConfirmed: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, peterLustig)
|
||||
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await creationFactory(testEnv, bibisCreation!)
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
it('returns allCreation', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listAllContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
order: 'DESC',
|
||||
filterConfirmed: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listAllContributions: {
|
||||
contributionCount: 2,
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
amount: '1000',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
memo: 'Test env contribution',
|
||||
amount: '100',
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteContribution', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: deleteContribution,
|
||||
variables: {
|
||||
id: -1,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, peterLustig)
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
result = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
describe('wrong contribution id', () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: deleteContribution,
|
||||
variables: {
|
||||
id: -1,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Contribution not found for given id.')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('other user sends a deleteContribtuion', () => {
|
||||
it('returns an error', async () => {
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: deleteContribution,
|
||||
variables: {
|
||||
id: result.data.createContribution.id,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Can not delete contribution of another user')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('User deletes own contribution', () => {
|
||||
it('deletes successfully', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: deleteContribution,
|
||||
variables: {
|
||||
id: result.data.createContribution.id,
|
||||
},
|
||||
}),
|
||||
).resolves.toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User deletes already confirmed contribution', () => {
|
||||
it('throws an error', async () => {
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
await mutate({
|
||||
mutation: confirmContribution,
|
||||
variables: {
|
||||
id: result.data.createContribution.id,
|
||||
},
|
||||
})
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: deleteContribution,
|
||||
variables: {
|
||||
id: result.data.createContribution.id,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('A confirmed contribution can not be deleted')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
171
backend/src/graphql/resolver/ContributionResolver.ts
Normal file
171
backend/src/graphql/resolver/ContributionResolver.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { Contribution as dbContribution } from '@entity/Contribution'
|
||||
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
|
||||
import { FindOperator, IsNull, getConnection } from '@dbTools/typeorm'
|
||||
import ContributionArgs from '@arg/ContributionArgs'
|
||||
import Paginated from '@arg/Paginated'
|
||||
import { Order } from '@enum/Order'
|
||||
import { Contribution, ContributionListResult } from '@model/Contribution'
|
||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||
import { User } from '@model/User'
|
||||
import { validateContribution, getUserCreation, updateCreations } from './util/creations'
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
|
||||
@Resolver()
|
||||
export class ContributionResolver {
|
||||
@Authorized([RIGHTS.CREATE_CONTRIBUTION])
|
||||
@Mutation(() => UnconfirmedContribution)
|
||||
async createContribution(
|
||||
@Args() { amount, memo, creationDate }: ContributionArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<UnconfirmedContribution> {
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS}`)
|
||||
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
|
||||
}
|
||||
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
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)`)
|
||||
}
|
||||
|
||||
const user = getUser(context)
|
||||
const creations = await getUserCreation(user.id)
|
||||
logger.trace('creations', creations)
|
||||
const creationDateObj = new Date(creationDate)
|
||||
validateContribution(creations, amount, creationDateObj)
|
||||
|
||||
const contribution = dbContribution.create()
|
||||
contribution.userId = user.id
|
||||
contribution.amount = amount
|
||||
contribution.createdAt = new Date()
|
||||
contribution.contributionDate = creationDateObj
|
||||
contribution.memo = memo
|
||||
|
||||
logger.trace('contribution to save', contribution)
|
||||
await dbContribution.save(contribution)
|
||||
return new UnconfirmedContribution(contribution, user, creations)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.DELETE_CONTRIBUTION])
|
||||
@Mutation(() => Boolean)
|
||||
async deleteContribution(
|
||||
@Arg('id', () => Int) id: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const user = getUser(context)
|
||||
const contribution = await dbContribution.findOne(id)
|
||||
if (!contribution) {
|
||||
throw new Error('Contribution not found for given id.')
|
||||
}
|
||||
if (contribution.userId !== user.id) {
|
||||
throw new Error('Can not delete contribution of another user')
|
||||
}
|
||||
if (contribution.confirmedAt) {
|
||||
throw new Error('A confirmed contribution can not be deleted')
|
||||
}
|
||||
const res = await contribution.softRemove()
|
||||
return !!res
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LIST_CONTRIBUTIONS])
|
||||
@Query(() => ContributionListResult)
|
||||
async listContributions(
|
||||
@Args()
|
||||
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||
@Arg('filterConfirmed', () => Boolean)
|
||||
filterConfirmed: boolean | null,
|
||||
@Ctx() context: Context,
|
||||
): Promise<ContributionListResult> {
|
||||
const user = getUser(context)
|
||||
const where: {
|
||||
userId: number
|
||||
confirmedBy?: FindOperator<number> | null
|
||||
} = { userId: user.id }
|
||||
if (filterConfirmed) where.confirmedBy = IsNull()
|
||||
const [contributions, count] = await dbContribution.findAndCount({
|
||||
where,
|
||||
order: {
|
||||
createdAt: order,
|
||||
},
|
||||
withDeleted: true,
|
||||
skip: (currentPage - 1) * pageSize,
|
||||
take: pageSize,
|
||||
})
|
||||
return new ContributionListResult(
|
||||
count,
|
||||
contributions.map((contribution) => new Contribution(contribution, new User(user))),
|
||||
)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LIST_ALL_CONTRIBUTIONS])
|
||||
@Query(() => ContributionListResult)
|
||||
async listAllContributions(
|
||||
@Args()
|
||||
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||
): Promise<ContributionListResult> {
|
||||
const [dbContributions, count] = await getConnection()
|
||||
.createQueryBuilder()
|
||||
.select('c')
|
||||
.from(dbContribution, 'c')
|
||||
.innerJoinAndSelect('c.user', 'u')
|
||||
.orderBy('c.createdAt', order)
|
||||
.limit(pageSize)
|
||||
.offset((currentPage - 1) * pageSize)
|
||||
.getManyAndCount()
|
||||
return new ContributionListResult(
|
||||
count,
|
||||
dbContributions.map(
|
||||
(contribution) => new Contribution(contribution, new User(contribution.user)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.UPDATE_CONTRIBUTION])
|
||||
@Mutation(() => UnconfirmedContribution)
|
||||
async updateContribution(
|
||||
@Arg('contributionId', () => Int)
|
||||
contributionId: number,
|
||||
@Args() { amount, memo, creationDate }: ContributionArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<UnconfirmedContribution> {
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS}`)
|
||||
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
|
||||
}
|
||||
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
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)`)
|
||||
}
|
||||
|
||||
const user = getUser(context)
|
||||
|
||||
const contributionToUpdate = await dbContribution.findOne({
|
||||
where: { id: contributionId, confirmedAt: IsNull() },
|
||||
})
|
||||
if (!contributionToUpdate) {
|
||||
throw new Error('No contribution found to given id.')
|
||||
}
|
||||
if (contributionToUpdate.userId !== user.id) {
|
||||
throw new Error('user of the pending contribution and send user does not correspond')
|
||||
}
|
||||
|
||||
const creationDateObj = new Date(creationDate)
|
||||
let creations = await getUserCreation(user.id)
|
||||
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
|
||||
creations = updateCreations(creations, contributionToUpdate)
|
||||
}
|
||||
|
||||
// all possible cases not to be true are thrown in this function
|
||||
validateContribution(creations, amount, creationDateObj)
|
||||
contributionToUpdate.amount = amount
|
||||
contributionToUpdate.memo = memo
|
||||
contributionToUpdate.contributionDate = new Date(creationDate)
|
||||
dbContribution.save(contributionToUpdate)
|
||||
|
||||
return new UnconfirmedContribution(contributionToUpdate, user, creations)
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,21 @@
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import { Resolver, Args, Arg, Authorized, Ctx, Mutation, Query, Int } from 'type-graphql'
|
||||
import { getConnection } from '@dbTools/typeorm'
|
||||
import {
|
||||
Resolver,
|
||||
Args,
|
||||
Arg,
|
||||
Authorized,
|
||||
Ctx,
|
||||
Mutation,
|
||||
Query,
|
||||
Int,
|
||||
createUnionType,
|
||||
} from 'type-graphql'
|
||||
import { TransactionLink } from '@model/TransactionLink'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import TransactionLinkArgs from '@arg/TransactionLinkArgs'
|
||||
import Paginated from '@arg/Paginated'
|
||||
@ -12,6 +26,17 @@ import { User } from '@model/User'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
import { executeTransaction } from './TransactionResolver'
|
||||
import { Order } from '@enum/Order'
|
||||
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { getUserCreation, validateContribution } from './util/creations'
|
||||
import { Decay } from '@model/Decay'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||
|
||||
const QueryLinkResult = createUnionType({
|
||||
name: 'QueryLinkResult', // the name of the GraphQL union
|
||||
types: () => [TransactionLink, ContributionLink] as const, // function that returns tuple of object types classes
|
||||
})
|
||||
|
||||
// TODO: do not export, test it inside the resolver
|
||||
export const transactionLinkCode = (date: Date): string => {
|
||||
@ -95,15 +120,23 @@ export class TransactionLinkResolver {
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.QUERY_TRANSACTION_LINK])
|
||||
@Query(() => TransactionLink)
|
||||
async queryTransactionLink(@Arg('code') code: string): Promise<TransactionLink> {
|
||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
|
||||
const user = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
||||
let redeemedBy: User | null = null
|
||||
if (transactionLink && transactionLink.redeemedBy) {
|
||||
redeemedBy = new User(await dbUser.findOneOrFail({ id: transactionLink.redeemedBy }))
|
||||
@Query(() => QueryLinkResult)
|
||||
async queryTransactionLink(@Arg('code') code: string): Promise<typeof QueryLinkResult> {
|
||||
if (code.match(/^CL-/)) {
|
||||
const contributionLink = await DbContributionLink.findOneOrFail(
|
||||
{ code: code.replace('CL-', '') },
|
||||
{ withDeleted: true },
|
||||
)
|
||||
return new ContributionLink(contributionLink)
|
||||
} else {
|
||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
|
||||
const user = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
||||
let redeemedBy: User | null = null
|
||||
if (transactionLink && transactionLink.redeemedBy) {
|
||||
redeemedBy = new User(await dbUser.findOneOrFail({ id: transactionLink.redeemedBy }))
|
||||
}
|
||||
return new TransactionLink(transactionLink, new User(user), redeemedBy)
|
||||
}
|
||||
return new TransactionLink(transactionLink, new User(user), redeemedBy)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS])
|
||||
@ -137,31 +170,137 @@ export class TransactionLinkResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const user = getUser(context)
|
||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
||||
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
||||
|
||||
const now = new Date()
|
||||
|
||||
if (user.id === linkedUser.id) {
|
||||
throw new Error('Cannot redeem own transaction link.')
|
||||
if (code.match(/^CL-/)) {
|
||||
logger.info('redeem contribution link...')
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('SERIALIZABLE')
|
||||
try {
|
||||
const contributionLink = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('contributionLink')
|
||||
.from(DbContributionLink, 'contributionLink')
|
||||
.where('contributionLink.code = :code', { code: code.replace('CL-', '') })
|
||||
.getOne()
|
||||
if (!contributionLink) {
|
||||
logger.error('no contribution link found to given code:', code)
|
||||
throw new Error('No contribution link found')
|
||||
}
|
||||
logger.info('...contribution link found with id', contributionLink.id)
|
||||
if (new Date(contributionLink.validFrom).getTime() > now.getTime()) {
|
||||
logger.error(
|
||||
'contribution link is not valid yet. Valid from: ',
|
||||
contributionLink.validFrom,
|
||||
)
|
||||
throw new Error('Contribution link not valid yet')
|
||||
}
|
||||
if (contributionLink.validTo) {
|
||||
if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) {
|
||||
logger.error('contribution link is depricated. Valid to: ', contributionLink.validTo)
|
||||
throw new Error('Contribution link is depricated')
|
||||
}
|
||||
}
|
||||
if (contributionLink.cycle !== 'ONCE') {
|
||||
logger.error('contribution link has unknown cycle', contributionLink.cycle)
|
||||
throw new Error('Contribution link has unknown cycle')
|
||||
}
|
||||
// Test ONCE rule
|
||||
const alreadyRedeemed = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('contribution')
|
||||
.from(DbContribution, 'contribution')
|
||||
.where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', {
|
||||
linkId: contributionLink.id,
|
||||
id: user.id,
|
||||
})
|
||||
.getOne()
|
||||
if (alreadyRedeemed) {
|
||||
logger.error('contribution link with rule ONCE already redeemed by user with id', user.id)
|
||||
throw new Error('Contribution link already redeemed')
|
||||
}
|
||||
|
||||
const creations = await getUserCreation(user.id, false)
|
||||
logger.info('open creations', creations)
|
||||
validateContribution(creations, contributionLink.amount, now)
|
||||
const contribution = new DbContribution()
|
||||
contribution.userId = user.id
|
||||
contribution.createdAt = now
|
||||
contribution.contributionDate = now
|
||||
contribution.memo = contributionLink.memo
|
||||
contribution.amount = contributionLink.amount
|
||||
contribution.contributionLinkId = contributionLink.id
|
||||
await queryRunner.manager.insert(DbContribution, contribution)
|
||||
|
||||
const lastTransaction = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('transaction')
|
||||
.from(DbTransaction, 'transaction')
|
||||
.where('transaction.userId = :id', { id: user.id })
|
||||
.orderBy('transaction.balanceDate', 'DESC')
|
||||
.getOne()
|
||||
let newBalance = new Decimal(0)
|
||||
|
||||
let decay: Decay | null = null
|
||||
if (lastTransaction) {
|
||||
decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now)
|
||||
newBalance = decay.balance
|
||||
}
|
||||
newBalance = newBalance.add(contributionLink.amount.toString())
|
||||
|
||||
const transaction = new DbTransaction()
|
||||
transaction.typeId = TransactionTypeId.CREATION
|
||||
transaction.memo = contribution.memo
|
||||
transaction.userId = contribution.userId
|
||||
transaction.previous = lastTransaction ? lastTransaction.id : null
|
||||
transaction.amount = contribution.amount
|
||||
transaction.creationDate = contribution.contributionDate
|
||||
transaction.balance = newBalance
|
||||
transaction.balanceDate = now
|
||||
transaction.decay = decay ? decay.decay : new Decimal(0)
|
||||
transaction.decayStart = decay ? decay.start : null
|
||||
await queryRunner.manager.insert(DbTransaction, transaction)
|
||||
|
||||
contribution.confirmedAt = now
|
||||
contribution.transactionId = transaction.id
|
||||
await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info('creation from contribution link commited successfuly.')
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error(`Creation from contribution link was not successful: ${e}`)
|
||||
throw new Error(`Creation from contribution link was not successful. ${e}`)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
||||
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
||||
|
||||
if (user.id === linkedUser.id) {
|
||||
throw new Error('Cannot redeem own transaction link.')
|
||||
}
|
||||
|
||||
if (transactionLink.validUntil.getTime() < now.getTime()) {
|
||||
throw new Error('Transaction Link is not valid anymore.')
|
||||
}
|
||||
|
||||
if (transactionLink.redeemedBy) {
|
||||
throw new Error('Transaction Link already redeemed.')
|
||||
}
|
||||
|
||||
await executeTransaction(
|
||||
transactionLink.amount,
|
||||
transactionLink.memo,
|
||||
linkedUser,
|
||||
user,
|
||||
transactionLink,
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if (transactionLink.validUntil.getTime() < now.getTime()) {
|
||||
throw new Error('Transaction Link is not valid anymore.')
|
||||
}
|
||||
|
||||
if (transactionLink.redeemedBy) {
|
||||
throw new Error('Transaction Link already redeemed.')
|
||||
}
|
||||
|
||||
await executeTransaction(
|
||||
transactionLink.amount,
|
||||
transactionLink.memo,
|
||||
linkedUser,
|
||||
user,
|
||||
transactionLink,
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,9 +34,7 @@ import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualT
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
import { BalanceResolver } from './BalanceResolver'
|
||||
|
||||
const MEMO_MAX_CHARS = 255
|
||||
const MEMO_MIN_CHARS = 5
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
|
||||
export const executeTransaction = async (
|
||||
amount: Decimal,
|
||||
|
||||
@ -11,8 +11,13 @@ import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { User } from '@entity/User'
|
||||
import CONFIG from '@/config'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
||||
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
||||
import { printTimeDuration, activationLink } from './UserResolver'
|
||||
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
||||
// import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
// import { TransactionLink } from '@entity/TransactionLink'
|
||||
|
||||
import { logger } from '@test/testSetup'
|
||||
|
||||
@ -25,6 +30,13 @@ jest.mock('@/mailer/sendAccountActivationEmail', () => {
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/mailer/sendAccountMultiRegistrationEmail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendAccountMultiRegistrationEmail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/mailer/sendResetPasswordEmail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
@ -69,6 +81,7 @@ describe('UserResolver', () => {
|
||||
|
||||
let result: any
|
||||
let emailOptIn: string
|
||||
let user: User[]
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
@ -86,7 +99,6 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
describe('valid input data', () => {
|
||||
let user: User[]
|
||||
let loginEmailOptIn: LoginEmailOptIn[]
|
||||
beforeAll(async () => {
|
||||
user = await User.find()
|
||||
@ -114,6 +126,7 @@ describe('UserResolver', () => {
|
||||
deletedAt: null,
|
||||
publisherId: 1234,
|
||||
referrerId: null,
|
||||
contributionLinkId: null,
|
||||
},
|
||||
])
|
||||
})
|
||||
@ -151,14 +164,33 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
describe('email already exists', () => {
|
||||
it('throws and logs an error', async () => {
|
||||
const mutation = await mutate({ mutation: createUser, variables })
|
||||
let mutation: User
|
||||
beforeAll(async () => {
|
||||
mutation = await mutate({ mutation: createUser, variables })
|
||||
})
|
||||
|
||||
it('logs an info', async () => {
|
||||
expect(logger.info).toBeCalledWith('User already exists with this email=peter@lustig.de')
|
||||
})
|
||||
|
||||
it('sends an account multi registration email', () => {
|
||||
expect(sendAccountMultiRegistrationEmail).toBeCalledWith({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
})
|
||||
})
|
||||
|
||||
it('results with partly faked user with random "id"', async () => {
|
||||
expect(mutation).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('User already exists.')],
|
||||
data: {
|
||||
createUser: {
|
||||
id: expect.any(Number),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('User already exists with this email=peter@lustig.de')
|
||||
})
|
||||
})
|
||||
|
||||
@ -195,6 +227,72 @@ describe('UserResolver', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('redeem codes', () => {
|
||||
describe('contribution link', () => {
|
||||
let link: ContributionLink
|
||||
beforeAll(async () => {
|
||||
// activate account of admin Peter Lustig
|
||||
await mutate({
|
||||
mutation: setPassword,
|
||||
variables: { code: emailOptIn, password: 'Aa12345_' },
|
||||
})
|
||||
// make Peter Lustig Admin
|
||||
const peter = await User.findOneOrFail({ id: user[0].id })
|
||||
peter.isAdmin = new Date()
|
||||
await peter.save()
|
||||
// factory logs in as Peter Lustig
|
||||
link = await contributionLinkFactory(testEnv, {
|
||||
name: 'Dokumenta 2022',
|
||||
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022',
|
||||
amount: 200,
|
||||
validFrom: new Date(2022, 5, 18),
|
||||
validTo: new Date(2022, 8, 25),
|
||||
})
|
||||
resetToken()
|
||||
await mutate({
|
||||
mutation: createUser,
|
||||
variables: { ...variables, email: 'ein@besucher.de', redeemCode: 'CL-' + link.code },
|
||||
})
|
||||
})
|
||||
|
||||
it('sets the contribution link id', async () => {
|
||||
await expect(User.findOne({ email: 'ein@besucher.de' })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
contributionLinkId: link.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
/* A transaction link requires GDD on account
|
||||
describe('transaction link', () => {
|
||||
let code: string
|
||||
beforeAll(async () => {
|
||||
// factory logs in as Peter Lustig
|
||||
await transactionLinkFactory(testEnv, {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 19.99,
|
||||
memo: `Kein Trick, keine Zauberrei,
|
||||
bei Gradidio sei dabei!`,
|
||||
})
|
||||
const transactionLink = await TransactionLink.findOneOrFail()
|
||||
resetToken()
|
||||
await mutate({
|
||||
mutation: createUser,
|
||||
variables: { ...variables, email: 'neuer@user.de', redeemCode: transactionLink.code },
|
||||
})
|
||||
})
|
||||
|
||||
it('sets the referrer id to Peter Lustigs id', async () => {
|
||||
await expect(User.findOne({ email: 'neuer@user.de' })).resolves.toEqual(expect.objectContaining({
|
||||
referrerId: user[0].id,
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
*/
|
||||
})
|
||||
})
|
||||
|
||||
describe('setPassword', () => {
|
||||
|
||||
@ -7,7 +7,9 @@ import { getConnection } from '@dbTools/typeorm'
|
||||
import CONFIG from '@/config'
|
||||
import { User } from '@model/User'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { communityDbUser } from '@/util/communityUser'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
||||
import { encode } from '@/auth/JWT'
|
||||
import CreateUserArgs from '@arg/CreateUserArgs'
|
||||
import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
|
||||
@ -17,9 +19,19 @@ import { OptInType } from '@enum/OptInType'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
||||
import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||
import { eventProtocol } from '@/event/EventProtocolEmitter'
|
||||
import {
|
||||
Event,
|
||||
EventLogin,
|
||||
EventRedeemRegister,
|
||||
EventRegister,
|
||||
EventSendConfirmationEmail,
|
||||
} from '@/event/Event'
|
||||
import { getUserCreation } from './util/creations'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const sodium = require('sodium-native')
|
||||
@ -221,7 +233,7 @@ export class UserResolver {
|
||||
logger.info('verifyLogin...')
|
||||
// TODO refactor and do not have duplicate code with login(see below)
|
||||
const userEntity = getUser(context)
|
||||
const user = new User(userEntity)
|
||||
const user = new User(userEntity, await getUserCreation(userEntity.id))
|
||||
// user.pubkey = userEntity.pubKey.toString('hex')
|
||||
// Elopage Status & Stored PublisherId
|
||||
user.hasElopage = await this.hasElopage(context)
|
||||
@ -271,7 +283,7 @@ export class UserResolver {
|
||||
logger.addContext('user', dbUser.id)
|
||||
logger.debug('login credentials valid...')
|
||||
|
||||
const user = new User(dbUser)
|
||||
const user = new User(dbUser, await getUserCreation(dbUser.id))
|
||||
logger.debug('user=' + user)
|
||||
|
||||
// Elopage Status & Stored PublisherId
|
||||
@ -287,6 +299,9 @@ export class UserResolver {
|
||||
key: 'token',
|
||||
value: encode(dbUser.pubKey),
|
||||
})
|
||||
const ev = new EventLogin()
|
||||
ev.userId = user.id
|
||||
eventProtocol.writeEvent(new Event().setEventLogin(ev))
|
||||
logger.info('successful Login:' + user)
|
||||
return user
|
||||
}
|
||||
@ -327,10 +342,35 @@ export class UserResolver {
|
||||
// TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes
|
||||
const userFound = await DbUser.findOne({ email }, { withDeleted: true })
|
||||
logger.info(`DbUser.findOne(email=${email}) = ${userFound}`)
|
||||
|
||||
if (userFound) {
|
||||
logger.error('User already exists with this email=' + email)
|
||||
logger.info('User already exists with this email=' + email)
|
||||
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
||||
throw new Error(`User already exists.`)
|
||||
|
||||
const user = new User(communityDbUser)
|
||||
user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in?
|
||||
user.email = email
|
||||
user.firstName = firstName
|
||||
user.lastName = lastName
|
||||
user.language = language
|
||||
user.publisherId = publisherId
|
||||
logger.debug('partly faked user=' + user)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const emailSent = await sendAccountMultiRegistrationEmail({
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
})
|
||||
logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`)
|
||||
/* uncomment this, when you need the activation link on the console */
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
logger.debug(`Email not send!`)
|
||||
}
|
||||
logger.info('createUser() faked and send multi registration mail...')
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
const passphrase = PassphraseGenerate()
|
||||
@ -339,6 +379,9 @@ export class UserResolver {
|
||||
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
||||
const emailHash = getEmailHash(email)
|
||||
|
||||
const eventRegister = new EventRegister()
|
||||
const eventRedeemRegister = new EventRedeemRegister()
|
||||
const eventSendConfirmEmail = new EventSendConfirmationEmail()
|
||||
const dbUser = new DbUser()
|
||||
dbUser.email = email
|
||||
dbUser.firstName = firstName
|
||||
@ -349,10 +392,22 @@ export class UserResolver {
|
||||
dbUser.passphrase = passphrase.join(' ')
|
||||
logger.debug('new dbUser=' + dbUser)
|
||||
if (redeemCode) {
|
||||
const transactionLink = await dbTransactionLink.findOne({ code: redeemCode })
|
||||
logger.info('redeemCode found transactionLink=' + transactionLink)
|
||||
if (transactionLink) {
|
||||
dbUser.referrerId = transactionLink.userId
|
||||
if (redeemCode.match(/^CL-/)) {
|
||||
const contributionLink = await dbContributionLink.findOne({
|
||||
code: redeemCode.replace('CL-', ''),
|
||||
})
|
||||
logger.info('redeemCode found contributionLink=' + contributionLink)
|
||||
if (contributionLink) {
|
||||
dbUser.contributionLinkId = contributionLink.id
|
||||
eventRedeemRegister.contributionId = contributionLink.id
|
||||
}
|
||||
} else {
|
||||
const transactionLink = await dbTransactionLink.findOne({ code: redeemCode })
|
||||
logger.info('redeemCode found transactionLink=' + transactionLink)
|
||||
if (transactionLink) {
|
||||
dbUser.referrerId = transactionLink.userId
|
||||
eventRedeemRegister.transactionId = transactionLink.id
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO this field has no null allowed unlike the loginServer table
|
||||
@ -362,6 +417,7 @@ export class UserResolver {
|
||||
// loginUser.pubKey = keyPair[0]
|
||||
// loginUser.privKey = encryptedPrivkey
|
||||
|
||||
const event = new Event()
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
@ -391,6 +447,9 @@ export class UserResolver {
|
||||
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||
})
|
||||
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
|
||||
eventSendConfirmEmail.userId = dbUser.id
|
||||
eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail))
|
||||
|
||||
/* uncomment this, when you need the activation link on the console */
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
@ -406,6 +465,15 @@ export class UserResolver {
|
||||
await queryRunner.release()
|
||||
}
|
||||
logger.info('createUser() successful...')
|
||||
|
||||
if (redeemCode) {
|
||||
eventRedeemRegister.userId = dbUser.id
|
||||
eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
|
||||
} else {
|
||||
eventRegister.userId = dbUser.id
|
||||
eventProtocol.writeEvent(event.setEventRegister(eventRegister))
|
||||
}
|
||||
|
||||
return new User(dbUser)
|
||||
}
|
||||
|
||||
|
||||
12
backend/src/graphql/resolver/const/const.ts
Normal file
12
backend/src/graphql/resolver/const/const.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export const MAX_CREATION_AMOUNT = new Decimal(1000)
|
||||
export const FULL_CREATION_AVAILABLE = [
|
||||
MAX_CREATION_AMOUNT,
|
||||
MAX_CREATION_AMOUNT,
|
||||
MAX_CREATION_AMOUNT,
|
||||
]
|
||||
export const CONTRIBUTIONLINK_NAME_MAX_CHARS = 100
|
||||
export const CONTRIBUTIONLINK_NAME_MIN_CHARS = 5
|
||||
export const MEMO_MAX_CHARS = 255
|
||||
export const MEMO_MIN_CHARS = 5
|
||||
130
backend/src/graphql/resolver/util/creations.ts
Normal file
130
backend/src/graphql/resolver/util/creations.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { getConnection } from '@dbTools/typeorm'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { FULL_CREATION_AVAILABLE, MAX_CREATION_AMOUNT } from '../const/const'
|
||||
|
||||
interface CreationMap {
|
||||
id: number
|
||||
creations: Decimal[]
|
||||
}
|
||||
|
||||
export const validateContribution = (
|
||||
creations: Decimal[],
|
||||
amount: Decimal,
|
||||
creationDate: Date,
|
||||
): void => {
|
||||
logger.trace('isContributionValid', creations, amount, creationDate)
|
||||
const index = getCreationIndex(creationDate.getMonth())
|
||||
|
||||
if (index < 0) {
|
||||
throw new Error('No information for available creations for the given date')
|
||||
}
|
||||
|
||||
if (amount.greaterThan(creations[index].toString())) {
|
||||
throw new Error(
|
||||
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const getUserCreations = async (
|
||||
ids: number[],
|
||||
includePending = true,
|
||||
): Promise<CreationMap[]> => {
|
||||
logger.trace('getUserCreations:', ids, includePending)
|
||||
const months = getCreationMonths()
|
||||
logger.trace('getUserCreations months', months)
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
|
||||
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
||||
logger.trace('getUserCreations dateFilter', dateFilter)
|
||||
|
||||
const unionString = includePending
|
||||
? `
|
||||
UNION
|
||||
SELECT contribution_date AS date, amount AS amount, user_id AS userId FROM contributions
|
||||
WHERE user_id IN (${ids.toString()})
|
||||
AND contribution_date >= ${dateFilter}
|
||||
AND confirmed_at IS NULL AND deleted_at IS NULL`
|
||||
: ''
|
||||
|
||||
const unionQuery = await queryRunner.manager.query(`
|
||||
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
|
||||
`)
|
||||
|
||||
await queryRunner.release()
|
||||
|
||||
return ids.map((id) => {
|
||||
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,
|
||||
)
|
||||
return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0)
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
||||
logger.trace('getUserCreation', id, includePending)
|
||||
const creations = await getUserCreations([id], includePending)
|
||||
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
||||
}
|
||||
|
||||
export const getCreationMonths = (): number[] => {
|
||||
const now = new Date(Date.now())
|
||||
return [
|
||||
now.getMonth() + 1,
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, 1).getMonth() + 1,
|
||||
new Date(now.getFullYear(), now.getMonth() - 2, 1).getMonth() + 1,
|
||||
].reverse()
|
||||
}
|
||||
|
||||
export const getCreationIndex = (month: number): number => {
|
||||
return getCreationMonths().findIndex((el) => el === month + 1)
|
||||
}
|
||||
|
||||
export const isStartEndDateValid = (
|
||||
startDate: string | null | undefined,
|
||||
endDate: string | null | undefined,
|
||||
): void => {
|
||||
if (!startDate) {
|
||||
logger.error('Start-Date is not initialized. A Start-Date must be set!')
|
||||
throw new Error('Start-Date is not initialized. A Start-Date must be set!')
|
||||
}
|
||||
|
||||
if (!endDate) {
|
||||
logger.error('End-Date is not initialized. An End-Date must be set!')
|
||||
throw new Error('End-Date is not initialized. An End-Date must be set!')
|
||||
}
|
||||
|
||||
// check if endDate is before startDate
|
||||
if (new Date(endDate).getTime() - new Date(startDate).getTime() < 0) {
|
||||
logger.error(`The value of validFrom must before or equals the validTo!`)
|
||||
throw new Error(`The value of validFrom must before or equals the validTo!`)
|
||||
}
|
||||
}
|
||||
|
||||
export const updateCreations = (creations: Decimal[], contribution: Contribution): Decimal[] => {
|
||||
const index = getCreationIndex(contribution.contributionDate.getMonth())
|
||||
|
||||
if (index < 0) {
|
||||
throw new Error('You cannot create GDD for a month older than the last three months.')
|
||||
}
|
||||
creations[index] = creations[index].plus(contribution.amount.toString())
|
||||
return creations
|
||||
}
|
||||
31
backend/src/mailer/sendAccountMultiRegistrationEmail.test.ts
Normal file
31
backend/src/mailer/sendAccountMultiRegistrationEmail.test.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import CONFIG from '@/config'
|
||||
import { sendAccountMultiRegistrationEmail } from './sendAccountMultiRegistrationEmail'
|
||||
import { sendEMail } from './sendEMail'
|
||||
|
||||
jest.mock('./sendEMail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendEMail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('sendAccountMultiRegistrationEmail', () => {
|
||||
beforeEach(async () => {
|
||||
await sendAccountMultiRegistrationEmail({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls sendEMail', () => {
|
||||
expect(sendEMail).toBeCalledWith({
|
||||
to: `Peter Lustig <peter@lustig.de>`,
|
||||
subject: 'Gradido: Erneuter Registrierungsversuch mit deiner E-Mail',
|
||||
text:
|
||||
expect.stringContaining('Hallo Peter Lustig') &&
|
||||
expect.stringContaining(CONFIG.EMAIL_LINK_FORGOTPASSWORD) &&
|
||||
expect.stringContaining('https://gradido.net/de/contact/'),
|
||||
})
|
||||
})
|
||||
})
|
||||
18
backend/src/mailer/sendAccountMultiRegistrationEmail.ts
Normal file
18
backend/src/mailer/sendAccountMultiRegistrationEmail.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { accountMultiRegistration } from './text/accountMultiRegistration'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
export const sendAccountMultiRegistrationEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
}): Promise<boolean> => {
|
||||
return sendEMail({
|
||||
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
||||
subject: accountMultiRegistration.de.subject,
|
||||
text: accountMultiRegistration.de.text({
|
||||
...data,
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
}),
|
||||
})
|
||||
}
|
||||
@ -31,6 +31,7 @@ describe('sendEMail', () => {
|
||||
beforeEach(async () => {
|
||||
result = await sendEMail({
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
@ -50,6 +51,7 @@ describe('sendEMail', () => {
|
||||
CONFIG.EMAIL = true
|
||||
result = await sendEMail({
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
@ -72,6 +74,7 @@ describe('sendEMail', () => {
|
||||
expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({
|
||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
|
||||
@ -5,10 +5,15 @@ import CONFIG from '@/config'
|
||||
|
||||
export const sendEMail = async (emailDef: {
|
||||
to: string
|
||||
cc?: string
|
||||
subject: string
|
||||
text: string
|
||||
}): Promise<boolean> => {
|
||||
logger.info(`send Email: to=${emailDef.to}, subject=${emailDef.subject}, text=${emailDef.text}`)
|
||||
logger.info(
|
||||
`send Email: to=${emailDef.to}` +
|
||||
(emailDef.cc ? `, cc=${emailDef.cc}` : '') +
|
||||
`, subject=${emailDef.subject}, text=${emailDef.text}`,
|
||||
)
|
||||
|
||||
if (!CONFIG.EMAIL) {
|
||||
logger.info(`Emails are disabled via config...`)
|
||||
|
||||
25
backend/src/mailer/text/accountMultiRegistration.ts
Normal file
25
backend/src/mailer/text/accountMultiRegistration.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export const accountMultiRegistration = {
|
||||
de: {
|
||||
subject: 'Gradido: Erneuter Registrierungsversuch mit deiner E-Mail',
|
||||
text: (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
resendLink: string
|
||||
}): string =>
|
||||
`Hallo ${data.firstName} ${data.lastName},
|
||||
|
||||
Deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.
|
||||
Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto.
|
||||
|
||||
Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest:
|
||||
${data.resendLink}
|
||||
oder kopiere den obigen Link in dein Browserfenster.
|
||||
|
||||
Wenn du nicht derjenige bist, der sich versucht hat erneut zu registrieren, wende dich bitte an unseren support:
|
||||
https://gradido.net/de/contact/
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
dein Gradido-Team`,
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
export interface ContributionLinkInterface {
|
||||
amount: number
|
||||
name: string
|
||||
memo: string
|
||||
validFrom?: Date
|
||||
validTo?: Date
|
||||
}
|
||||
18
backend/src/seeds/contributionLink/index.ts
Normal file
18
backend/src/seeds/contributionLink/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { ContributionLinkInterface } from './ContributionLinkInterface'
|
||||
|
||||
export const contributionLinks: ContributionLinkInterface[] = [
|
||||
{
|
||||
name: 'Dokumenta 2017',
|
||||
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2017',
|
||||
amount: 200,
|
||||
validFrom: new Date(2017, 3, 8),
|
||||
validTo: new Date(2017, 6, 16),
|
||||
},
|
||||
{
|
||||
name: 'Dokumenta 2022',
|
||||
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022',
|
||||
amount: 200,
|
||||
validFrom: new Date(2022, 5, 18),
|
||||
validTo: new Date(2022, 8, 25),
|
||||
},
|
||||
]
|
||||
29
backend/src/seeds/factory/contributionLink.ts
Normal file
29
backend/src/seeds/factory/contributionLink.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { createContributionLink } from '@/seeds/graphql/mutations'
|
||||
import { login } from '@/seeds/graphql/queries'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface'
|
||||
|
||||
export const contributionLinkFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
contributionLink: ContributionLinkInterface,
|
||||
): Promise<ContributionLink> => {
|
||||
const { mutate, query } = client
|
||||
|
||||
// login as admin
|
||||
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||
|
||||
const variables = {
|
||||
amount: contributionLink.amount,
|
||||
memo: contributionLink.memo,
|
||||
name: contributionLink.name,
|
||||
cycle: 'ONCE',
|
||||
maxPerCycle: 1,
|
||||
maxAmountPerMonth: 200,
|
||||
validFrom: contributionLink.validFrom ? contributionLink.validFrom.toISOString() : undefined,
|
||||
validTo: contributionLink.validTo ? contributionLink.validTo.toISOString() : undefined,
|
||||
}
|
||||
|
||||
const result = await mutate({ mutation: createContributionLink, variables })
|
||||
return result.data.createContributionLink
|
||||
}
|
||||
@ -1,13 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { createPendingCreation, confirmPendingCreation } from '@/seeds/graphql/mutations'
|
||||
import { adminCreateContribution, confirmContribution } from '@/seeds/graphql/mutations'
|
||||
import { login } from '@/seeds/graphql/queries'
|
||||
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { User } from '@entity/User'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
// import CONFIG from '@/config/index'
|
||||
|
||||
export const nMonthsBefore = (date: Date, months = 1): string => {
|
||||
@ -17,23 +17,25 @@ export const nMonthsBefore = (date: Date, months = 1): string => {
|
||||
export const creationFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
creation: CreationInterface,
|
||||
): Promise<AdminPendingCreation | void> => {
|
||||
): Promise<Contribution | void> => {
|
||||
const { mutate, query } = client
|
||||
|
||||
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||
|
||||
// TODO it would be nice to have this mutation return the id
|
||||
await mutate({ mutation: createPendingCreation, variables: { ...creation } })
|
||||
await mutate({ mutation: adminCreateContribution, variables: { ...creation } })
|
||||
|
||||
const user = await User.findOneOrFail({ where: { email: creation.email } })
|
||||
|
||||
const pendingCreation = await AdminPendingCreation.findOneOrFail({
|
||||
const pendingCreation = await Contribution.findOneOrFail({
|
||||
where: { userId: user.id, amount: creation.amount },
|
||||
order: { created: 'DESC' },
|
||||
order: { createdAt: 'DESC' },
|
||||
})
|
||||
|
||||
if (creation.confirmed) {
|
||||
await mutate({ mutation: confirmPendingCreation, variables: { id: pendingCreation.id } })
|
||||
await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } })
|
||||
|
||||
const confirmedCreation = await Contribution.findOneOrFail({ id: pendingCreation.id })
|
||||
|
||||
if (creation.moveCreationDate) {
|
||||
const transaction = await Transaction.findOneOrFail({
|
||||
@ -41,6 +43,9 @@ export const creationFactory = async (
|
||||
order: { balanceDate: 'DESC' },
|
||||
})
|
||||
if (transaction.decay.equals(0) && transaction.creationDate) {
|
||||
confirmedCreation.contributionDate = new Date(
|
||||
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
||||
)
|
||||
transaction.creationDate = new Date(
|
||||
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
||||
)
|
||||
@ -48,6 +53,7 @@ export const creationFactory = async (
|
||||
nMonthsBefore(transaction.balanceDate, creation.moveCreationDate),
|
||||
)
|
||||
await transaction.save()
|
||||
await confirmedCreation.save()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user