Merge branch 'master' into log-response-as-trace

This commit is contained in:
Moriz Wahl 2022-06-15 23:36:20 +02:00 committed by GitHub
commit 8646471f63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 1604 additions and 397 deletions

View File

@ -0,0 +1,3 @@
4ƒk׀ךֻ1°,<EFBFBD>fעbלAלqִ¬cי<EFBFBD>#¾צ¾ר#s8-ְ1&»;נצד"¢פשל7d¥jM?bljfB¼ƒqֱ=
<EFBFBD>ײmyפ¿
vת·´V 

View File

@ -0,0 +1,32 @@
{
"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
}
}
}

View File

@ -528,7 +528,7 @@ jobs:
report_name: Coverage Backend
type: lcov
result_path: ./backend/coverage/lcov.info
min_coverage: 68
min_coverage: 70
token: ${{ github.token }}
##########################################################################

3
.gitignore vendored
View File

@ -1,6 +1,5 @@
.dbeaver
.project
*.log
*.bak
/node_modules/*
messages.pot
nbproject

18
.project Normal file
View File

@ -0,0 +1,18 @@
<?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>

View File

@ -4,8 +4,48 @@ 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.9.0](https://github.com/gradido/gradido/compare/1.8.3...1.9.0)
- 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)
- 1951 remove back link and remove gray box [`#1959`](https://github.com/gradido/gradido/pull/1959)
- 1952 change footer icons color an remove save login [`#1955`](https://github.com/gradido/gradido/pull/1955)
- fix: License should be a valid SPDX license expression [`#1954`](https://github.com/gradido/gradido/pull/1954)
- refactor: 🍰 Refactor THX Page 2. Step [`#1858`](https://github.com/gradido/gradido/pull/1858)
- fix: Add Timezone to Decay Start Block [`#1931`](https://github.com/gradido/gradido/pull/1931)
- devops: Update License in all package.json [`#1925`](https://github.com/gradido/gradido/pull/1925)
- docu: Creation Flowchart [`#1918`](https://github.com/gradido/gradido/pull/1918)
- refactor: Use Logger Categories [`#1912`](https://github.com/gradido/gradido/pull/1912)
- 1883 remove the animated coins in the profile settings [`#1946`](https://github.com/gradido/gradido/pull/1946)
- 1942 replace pictures for carousel [`#1943`](https://github.com/gradido/gradido/pull/1943)
- 1933 auth footer is not on one level [`#1941`](https://github.com/gradido/gradido/pull/1941)
- 1929 styling new template for password component [`#1935`](https://github.com/gradido/gradido/pull/1935)
- 1926 button concept for gradido template [`#1927`](https://github.com/gradido/gradido/pull/1927)
- 1916 remove select language from register form [`#1930`](https://github.com/gradido/gradido/pull/1930)
- rename files from auth folder, rule vue name = name files [`#1937`](https://github.com/gradido/gradido/pull/1937)
- Add files Bild_1_2400.jpg [`#1945`](https://github.com/gradido/gradido/pull/1945)
- Bilder für Slider [`#1940`](https://github.com/gradido/gradido/pull/1940)
- contribution analysis of elopage and concept proposal [`#1917`](https://github.com/gradido/gradido/pull/1917)
- 1676 feature federation technical concept [`#1711`](https://github.com/gradido/gradido/pull/1711)
- more details about Windows installation [`#1842`](https://github.com/gradido/gradido/pull/1842)
- Concept to Introduce Gradido ID [`#1797`](https://github.com/gradido/gradido/pull/1797)
- first draft of concept event protocol [`#1796`](https://github.com/gradido/gradido/pull/1796)
- 1682 new design for the login and registration area [`#1693`](https://github.com/gradido/gradido/pull/1693)
- fix: Database Connection Charset to utf8mb4_unicode_ci [`#1915`](https://github.com/gradido/gradido/pull/1915)
- refactor: 🍰 Create Filter Object in GQL And Rename Args [`#1860`](https://github.com/gradido/gradido/pull/1860)
- feat: 🍰 Improve Apollo Logging [`#1859`](https://github.com/gradido/gradido/pull/1859)
- Add files via upload [`#1903`](https://github.com/gradido/gradido/pull/1903)
- 🍰 Hide Pagenation On Short Transactionlist [`#1875`](https://github.com/gradido/gradido/pull/1875)
- 🍰 Ignore macOS .DS_Store Files [`#1902`](https://github.com/gradido/gradido/pull/1902)
- pre I from #1682, add images, svg for new styling [`#1900`](https://github.com/gradido/gradido/pull/1900)
- add browserstack logo image [`#1888`](https://github.com/gradido/gradido/pull/1888)
#### [1.8.3](https://github.com/gradido/gradido/compare/1.8.2...1.8.3)
> 13 May 2022
- Release 1.8.3 [`#1899`](https://github.com/gradido/gradido/pull/1899)
- Checkbox [`#1894`](https://github.com/gradido/gradido/pull/1894)
- fix: Count Deprecated Links as Well [`#1892`](https://github.com/gradido/gradido/pull/1892)

View File

@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido",
"main": "index.js",
"author": "Moriz Wahl",
"version": "1.8.3",
"version": "1.9.0",
"license": "Apache-2.0",
"private": false,
"scripts": {

View File

@ -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'],
},
},
})

View File

@ -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', {

View File

@ -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(),

View File

@ -73,7 +73,7 @@
</div>
</template>
<script>
import { updatePendingCreation } from '../graphql/updatePendingCreation'
import { adminUpdateContribution } from '../graphql/adminUpdateContribution'
import { creationMonths } from '../mixins/creationMonths'
export default {
@ -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(

View 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
)
}
`

View File

@ -0,0 +1,11 @@
import gql from 'graphql-tag'
export const adminCreateContributions = gql`
mutation ($pendingCreations: [AdminCreateContributionArgs!]!) {
adminCreateContributions(pendingCreations: $pendingCreations) {
success
successfulContribution
failedContribution
}
}
`

View File

@ -0,0 +1,7 @@
import gql from 'graphql-tag'
export const adminDeleteContribution = gql`
mutation ($id: Int!) {
adminDeleteContribution(id: $id)
}
`

View File

@ -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

View File

@ -0,0 +1,7 @@
import gql from 'graphql-tag'
export const confirmContribution = gql`
mutation ($id: Int!) {
confirmContribution(id: $id)
}
`

View File

@ -1,7 +0,0 @@
import gql from 'graphql-tag'
export const confirmPendingCreation = gql`
mutation ($id: Int!) {
confirmPendingCreation(id: $id)
}
`

View File

@ -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)
}
`

View File

@ -1,11 +0,0 @@
import gql from 'graphql-tag'
export const createPendingCreations = gql`
mutation ($pendingCreations: [CreatePendingCreationArgs!]!) {
createPendingCreations(pendingCreations: $pendingCreations) {
success
successfulCreation
failedCreation
}
}
`

View File

@ -1,7 +0,0 @@
import gql from 'graphql-tag'
export const deletePendingCreation = gql`
mutation ($id: Int!) {
deletePendingCreation(id: $id)
}
`

View File

@ -1,8 +1,8 @@
import gql from 'graphql-tag'
export const getPendingCreations = gql`
export const listUnconfirmedContributions = gql`
query {
getPendingCreations {
listUnconfirmedContributions {
id
firstName
lastName

View File

@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils'
import CreationConfirm from './CreationConfirm.vue'
import { deletePendingCreation } from '../graphql/deletePendingCreation'
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { confirmContribution } from '../graphql/confirmContribution'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue
@ -9,7 +9,7 @@ const localVue = global.localVue
const storeCommitMock = jest.fn()
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
getPendingCreations: [
listUnconfirmedContributions: [
{
id: 1,
firstName: 'Bibi',
@ -84,9 +84,9 @@ describe('CreationConfirm', () => {
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
})
it('calls the deletePendingCreation mutation', () => {
it('calls the adminDeleteContribution mutation', () => {
expect(apolloMutateMock).toBeCalledWith({
mutation: deletePendingCreation,
mutation: adminDeleteContribution,
variables: { id: 1 },
})
})
@ -141,9 +141,9 @@ describe('CreationConfirm', () => {
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
})
it('calls the confirmPendingCreation mutation', () => {
it('calls the confirmContribution mutation', () => {
expect(apolloMutateMock).toBeCalledWith({
mutation: confirmPendingCreation,
mutation: confirmContribution,
variables: { id: 2 },
})
})

View File

@ -15,9 +15,9 @@
<script>
import Overlay from '../components/Overlay.vue'
import OpenCreationsTable from '../components/Tables/OpenCreationsTable.vue'
import { getPendingCreations } from '../graphql/getPendingCreations'
import { deletePendingCreation } from '../graphql/deletePendingCreation'
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { confirmContribution } from '../graphql/confirmContribution'
export default {
name: 'CreationConfirm',
@ -36,7 +36,7 @@ export default {
removeCreation(item) {
this.$apollo
.mutate({
mutation: deletePendingCreation,
mutation: adminDeleteContribution,
variables: {
id: item.id,
},
@ -52,7 +52,7 @@ export default {
confirmCreation() {
this.$apollo
.mutate({
mutation: confirmPendingCreation,
mutation: confirmContribution,
variables: {
id: this.item.id,
},
@ -70,13 +70,13 @@ export default {
getPendingCreations() {
this.$apollo
.query({
query: getPendingCreations,
query: listUnconfirmedContributions,
fetchPolicy: 'network-only',
})
.then((result) => {
this.$store.commit('resetOpenCreations')
this.pendingCreations = result.data.getPendingCreations
this.$store.commit('setOpenCreations', result.data.getPendingCreations.length)
this.pendingCreations = result.data.listUnconfirmedContributions
this.$store.commit('setOpenCreations', result.data.listUnconfirmedContributions.length)
})
.catch((error) => {
this.toastError(error.message)

View File

@ -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()
})

View File

@ -31,7 +31,7 @@
</div>
</template>
<script>
import { getPendingCreations } from '../graphql/getPendingCreations'
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions'
export default {
name: 'overview',
@ -39,11 +39,11 @@ export default {
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)
})
},
},

View File

@ -4082,9 +4082,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"

View File

@ -1,4 +1,4 @@
CONFIG_VERSION=v6.2022-04-21
CONFIG_VERSION=v7.2022-06-15
# Server
PORT=4000
@ -28,6 +28,7 @@ COMMUNITY_NAME=Gradido Entwicklung
COMMUNITY_URL=http://localhost/
COMMUNITY_REGISTER_URL=http://localhost/register
COMMUNITY_REDEEM_URL=http://localhost/redeem/{code}
COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code}
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
# Login Server

View File

@ -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

View File

@ -1,6 +1,6 @@
{
"name": "gradido-backend",
"version": "1.8.3",
"version": "1.9.0",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend",

View File

@ -27,14 +27,19 @@ export enum RIGHTS {
GDT_BALANCE = 'GDT_BALANCE',
// Admin
SEARCH_USERS = 'SEARCH_USERS',
CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION',
UPDATE_PENDING_CREATION = 'UPDATE_PENDING_CREATION',
SEARCH_PENDING_CREATION = 'SEARCH_PENDING_CREATION',
DELETE_PENDING_CREATION = 'DELETE_PENDING_CREATION',
CONFIRM_PENDING_CREATION = 'CONFIRM_PENDING_CREATION',
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',
DELETE_USER = 'DELETE_USER',
UNDELETE_USER = 'UNDELETE_USER',
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',
}

View File

@ -10,14 +10,14 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0036-unique_previous_in_transactions',
DB_VERSION: '0039-contributions_table',
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: 'v7.2022-06-15',
CURRENT: '',
},
}
@ -54,6 +54,8 @@ const community = {
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/',
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}',
COMMUNITY_REDEEM_CONTRIBUTION_URL:
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}',
COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
}

View File

@ -3,7 +3,7 @@ import Decimal from 'decimal.js-light'
@InputType()
@ArgsType()
export default class CreatePendingCreationArgs {
export default class AdminCreateContributionArgs {
@Field(() => String)
email: string

View File

@ -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

View 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
}

View 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
})

View File

@ -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[]
}

View File

@ -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

View 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
}

View 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
}

View File

@ -2,7 +2,7 @@ import { ObjectType, Field, Int } from 'type-graphql'
import Decimal from 'decimal.js-light'
@ObjectType()
export class PendingCreation {
export class UnconfirmedContribution {
@Field(() => String)
firstName: string

View File

@ -15,25 +15,30 @@ import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
import {
deleteUser,
unDeleteUser,
createPendingCreation,
createPendingCreations,
updatePendingCreation,
deletePendingCreation,
confirmPendingCreation,
adminCreateContribution,
adminCreateContributions,
adminUpdateContribution,
adminDeleteContribution,
confirmContribution,
createContributionLink,
deleteContributionLink,
updateContributionLink,
} from '@/seeds/graphql/mutations'
import {
getPendingCreations,
listUnconfirmedContributions,
login,
searchUsers,
listTransactionLinksAdmin,
listContributionLinks,
} from '@/seeds/graphql/queries'
import { GraphQLError } from 'graphql'
import { User } from '@entity/User'
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
import Decimal from 'decimal.js-light'
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
import { Contribution } from '@entity/Contribution'
import { Transaction as DbTransaction } from '@entity/Transaction'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
// mock account activation email to avoid console spam
jest.mock('@/mailer/sendAccountActivationEmail', () => {
@ -61,7 +66,7 @@ afterAll(async () => {
let admin: User
let user: User
let creation: AdminPendingCreation | void
let creation: Contribution | void
describe('AdminResolver', () => {
describe('delete user', () => {
@ -497,9 +502,9 @@ describe('AdminResolver', () => {
}
describe('unauthenticated', () => {
describe('createPendingCreation', () => {
describe('adminCreateContribution', () => {
it('returns an error', async () => {
await expect(mutate({ mutation: createPendingCreation, variables })).resolves.toEqual(
await expect(mutate({ mutation: adminCreateContribution, variables })).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
@ -507,11 +512,11 @@ describe('AdminResolver', () => {
})
})
describe('createPendingCreations', () => {
describe('adminCreateContributions', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: createPendingCreations,
mutation: adminCreateContributions,
variables: { pendingCreations: [variables] },
}),
).resolves.toEqual(
@ -522,11 +527,11 @@ describe('AdminResolver', () => {
})
})
describe('updatePendingCreation', () => {
describe('adminUpdateContribution', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: updatePendingCreation,
mutation: adminUpdateContribution,
variables: {
id: 1,
email: 'bibi@bloxberg.de',
@ -543,11 +548,11 @@ describe('AdminResolver', () => {
})
})
describe('getPendingCreations', () => {
describe('listUnconfirmedContributions', () => {
it('returns an error', async () => {
await expect(
query({
query: getPendingCreations,
query: listUnconfirmedContributions,
}),
).resolves.toEqual(
expect.objectContaining({
@ -557,11 +562,11 @@ describe('AdminResolver', () => {
})
})
describe('deletePendingCreation', () => {
describe('adminDeleteContribution', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: deletePendingCreation,
mutation: adminDeleteContribution,
variables: {
id: 1,
},
@ -574,11 +579,11 @@ describe('AdminResolver', () => {
})
})
describe('confirmPendingCreation', () => {
describe('confirmContribution', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: confirmPendingCreation,
mutation: confirmContribution,
variables: {
id: 1,
},
@ -607,9 +612,9 @@ describe('AdminResolver', () => {
resetToken()
})
describe('createPendingCreation', () => {
describe('adminCreateContribution', () => {
it('returns an error', async () => {
await expect(mutate({ mutation: createPendingCreation, variables })).resolves.toEqual(
await expect(mutate({ mutation: adminCreateContribution, variables })).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
@ -617,11 +622,11 @@ describe('AdminResolver', () => {
})
})
describe('createPendingCreations', () => {
describe('adminCreateContributions', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: createPendingCreations,
mutation: adminCreateContributions,
variables: { pendingCreations: [variables] },
}),
).resolves.toEqual(
@ -632,11 +637,11 @@ describe('AdminResolver', () => {
})
})
describe('updatePendingCreation', () => {
describe('adminUpdateContribution', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: updatePendingCreation,
mutation: adminUpdateContribution,
variables: {
id: 1,
email: 'bibi@bloxberg.de',
@ -653,11 +658,11 @@ describe('AdminResolver', () => {
})
})
describe('getPendingCreations', () => {
describe('listUnconfirmedContributions', () => {
it('returns an error', async () => {
await expect(
query({
query: getPendingCreations,
query: listUnconfirmedContributions,
}),
).resolves.toEqual(
expect.objectContaining({
@ -667,11 +672,11 @@ describe('AdminResolver', () => {
})
})
describe('deletePendingCreation', () => {
describe('adminDeleteContribution', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: deletePendingCreation,
mutation: adminDeleteContribution,
variables: {
id: 1,
},
@ -684,11 +689,11 @@ describe('AdminResolver', () => {
})
})
describe('confirmPendingCreation', () => {
describe('confirmContribution', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: confirmPendingCreation,
mutation: confirmContribution,
variables: {
id: 1,
},
@ -716,7 +721,7 @@ describe('AdminResolver', () => {
resetToken()
})
describe('createPendingCreation', () => {
describe('adminCreateContribution', () => {
beforeAll(async () => {
const now = new Date()
creation = await creationFactory(testEnv, {
@ -729,7 +734,9 @@ describe('AdminResolver', () => {
describe('user to create for does not exist', () => {
it('throws an error', async () => {
await expect(mutate({ mutation: createPendingCreation, variables })).resolves.toEqual(
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Could not find user with email: bibi@bloxberg.de')],
}),
@ -744,9 +751,13 @@ describe('AdminResolver', () => {
})
it('throws an error', async () => {
await expect(mutate({ mutation: createPendingCreation, variables })).resolves.toEqual(
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('This user was deleted. Cannot make a creation.')],
errors: [
new GraphQLError('This user was deleted. Cannot create a contribution.'),
],
}),
)
})
@ -759,9 +770,13 @@ describe('AdminResolver', () => {
})
it('throws an error', async () => {
await expect(mutate({ mutation: createPendingCreation, variables })).resolves.toEqual(
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Creation could not be saved, Email is not activated')],
errors: [
new GraphQLError('Contribution could not be saved, Email is not activated'),
],
}),
)
})
@ -776,7 +791,7 @@ describe('AdminResolver', () => {
describe('date of creation is not a date string', () => {
it('throws an error', async () => {
await expect(
mutate({ mutation: createPendingCreation, variables }),
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
expect.objectContaining({
errors: [
@ -796,7 +811,7 @@ describe('AdminResolver', () => {
1,
).toString()
await expect(
mutate({ mutation: createPendingCreation, variables }),
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
expect.objectContaining({
errors: [
@ -816,7 +831,7 @@ describe('AdminResolver', () => {
1,
).toString()
await expect(
mutate({ mutation: createPendingCreation, variables }),
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
expect.objectContaining({
errors: [
@ -831,7 +846,7 @@ describe('AdminResolver', () => {
it('throws an error', async () => {
variables.creationDate = new Date().toString()
await expect(
mutate({ mutation: createPendingCreation, variables }),
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
expect.objectContaining({
errors: [
@ -848,11 +863,11 @@ describe('AdminResolver', () => {
it('returns an array of the open creations for the last three months', async () => {
variables.amount = new Decimal(200)
await expect(
mutate({ mutation: createPendingCreation, variables }),
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
expect.objectContaining({
data: {
createPendingCreation: [1000, 1000, 800],
adminCreateContribution: [1000, 1000, 800],
},
}),
)
@ -863,7 +878,7 @@ describe('AdminResolver', () => {
it('returns an array of the open creations for the last three months', async () => {
variables.amount = new Decimal(1000)
await expect(
mutate({ mutation: createPendingCreation, variables }),
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
expect.objectContaining({
errors: [
@ -878,7 +893,7 @@ describe('AdminResolver', () => {
})
})
describe('createPendingCreations', () => {
describe('adminCreateContributions', () => {
// at this point we have this data in DB:
// bibi@bloxberg.de: [1000, 1000, 800]
// peter@lustig.de: [1000, 600, 1000]
@ -903,16 +918,16 @@ describe('AdminResolver', () => {
it('returns success, two successful creation and three failed creations', async () => {
await expect(
mutate({
mutation: createPendingCreations,
mutation: adminCreateContributions,
variables: { pendingCreations: massCreationVariables },
}),
).resolves.toEqual(
expect.objectContaining({
data: {
createPendingCreations: {
adminCreateContributions: {
success: true,
successfulCreation: ['bibi@bloxberg.de', 'peter@lustig.de'],
failedCreation: [
successfulContribution: ['bibi@bloxberg.de', 'peter@lustig.de'],
failedContribution: [
'stephen@hawking.uk',
'garrick@ollivander.com',
'bob@baumeister.de',
@ -924,7 +939,7 @@ describe('AdminResolver', () => {
})
})
describe('updatePendingCreation', () => {
describe('adminUpdateContribution', () => {
// at this I expect to have this data in DB:
// bibi@bloxberg.de: [1000, 1000, 300]
// peter@lustig.de: [1000, 600, 500]
@ -935,7 +950,7 @@ describe('AdminResolver', () => {
it('throws an error', async () => {
await expect(
mutate({
mutation: updatePendingCreation,
mutation: adminUpdateContribution,
variables: {
id: 1,
email: 'bob@baumeister.de',
@ -956,7 +971,7 @@ describe('AdminResolver', () => {
it('throws an error', async () => {
await expect(
mutate({
mutation: updatePendingCreation,
mutation: adminUpdateContribution,
variables: {
id: 1,
email: 'stephen@hawking.uk',
@ -977,7 +992,7 @@ describe('AdminResolver', () => {
it('throws an error', async () => {
await expect(
mutate({
mutation: updatePendingCreation,
mutation: adminUpdateContribution,
variables: {
id: -1,
email: 'bibi@bloxberg.de',
@ -988,7 +1003,7 @@ describe('AdminResolver', () => {
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('No creation found to given id.')],
errors: [new GraphQLError('No contribution found to given id.')],
}),
)
})
@ -998,7 +1013,7 @@ describe('AdminResolver', () => {
it('throws an error', async () => {
await expect(
mutate({
mutation: updatePendingCreation,
mutation: adminUpdateContribution,
variables: {
id: creation ? creation.id : -1,
email: 'bibi@bloxberg.de',
@ -1011,7 +1026,7 @@ describe('AdminResolver', () => {
expect.objectContaining({
errors: [
new GraphQLError(
'user of the pending creation and send user does not correspond',
'user of the pending contribution and send user does not correspond',
),
],
}),
@ -1023,7 +1038,7 @@ describe('AdminResolver', () => {
it('throws an error', async () => {
await expect(
mutate({
mutation: updatePendingCreation,
mutation: adminUpdateContribution,
variables: {
id: creation ? creation.id : -1,
email: 'peter@lustig.de',
@ -1048,7 +1063,7 @@ describe('AdminResolver', () => {
it('returns update creation object', async () => {
await expect(
mutate({
mutation: updatePendingCreation,
mutation: adminUpdateContribution,
variables: {
id: creation ? creation.id : -1,
email: 'peter@lustig.de',
@ -1060,7 +1075,7 @@ describe('AdminResolver', () => {
).resolves.toEqual(
expect.objectContaining({
data: {
updatePendingCreation: {
adminUpdateContribution: {
date: expect.any(String),
memo: 'Danke Peter!',
amount: '300',
@ -1076,7 +1091,7 @@ describe('AdminResolver', () => {
it('returns update creation object', async () => {
await expect(
mutate({
mutation: updatePendingCreation,
mutation: adminUpdateContribution,
variables: {
id: creation ? creation.id : -1,
email: 'peter@lustig.de',
@ -1088,7 +1103,7 @@ describe('AdminResolver', () => {
).resolves.toEqual(
expect.objectContaining({
data: {
updatePendingCreation: {
adminUpdateContribution: {
date: expect.any(String),
memo: 'Das war leider zu Viel!',
amount: '200',
@ -1101,16 +1116,16 @@ describe('AdminResolver', () => {
})
})
describe('getPendingCreations', () => {
describe('listUnconfirmedContributions', () => {
it('returns four pending creations', async () => {
await expect(
query({
query: getPendingCreations,
query: listUnconfirmedContributions,
}),
).resolves.toEqual(
expect.objectContaining({
data: {
getPendingCreations: expect.arrayContaining([
listUnconfirmedContributions: expect.arrayContaining([
{
id: expect.any(Number),
firstName: 'Peter',
@ -1162,19 +1177,19 @@ describe('AdminResolver', () => {
})
})
describe('deletePendingCreation', () => {
describe('adminDeleteContribution', () => {
describe('creation id does not exist', () => {
it('throws an error', async () => {
await expect(
mutate({
mutation: deletePendingCreation,
mutation: adminDeleteContribution,
variables: {
id: -1,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Creation not found for given id.')],
errors: [new GraphQLError('Contribution not found for given id.')],
}),
)
})
@ -1184,33 +1199,33 @@ describe('AdminResolver', () => {
it('returns true', async () => {
await expect(
mutate({
mutation: deletePendingCreation,
mutation: adminDeleteContribution,
variables: {
id: creation ? creation.id : -1,
},
}),
).resolves.toEqual(
expect.objectContaining({
data: { deletePendingCreation: true },
data: { adminDeleteContribution: true },
}),
)
})
})
})
describe('confirmPendingCreation', () => {
describe('confirmContribution', () => {
describe('creation does not exits', () => {
it('throws an error', async () => {
await expect(
mutate({
mutation: confirmPendingCreation,
mutation: confirmContribution,
variables: {
id: -1,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Creation not found to given id.')],
errors: [new GraphQLError('Contribution not found to given id.')],
}),
)
})
@ -1230,14 +1245,14 @@ describe('AdminResolver', () => {
it('thows an error', async () => {
await expect(
mutate({
mutation: confirmPendingCreation,
mutation: confirmContribution,
variables: {
id: creation ? creation.id : -1,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Moderator can not confirm own pending creation')],
errors: [new GraphQLError('Moderator can not confirm own contribution')],
}),
)
})
@ -1257,14 +1272,14 @@ describe('AdminResolver', () => {
it('returns true', async () => {
await expect(
mutate({
mutation: confirmPendingCreation,
mutation: confirmContribution,
variables: {
id: creation ? creation.id : -1,
},
}),
).resolves.toEqual(
expect.objectContaining({
data: { confirmPendingCreation: true },
data: { confirmContribution: true },
}),
)
})
@ -1282,8 +1297,8 @@ describe('AdminResolver', () => {
})
describe('confirm two creations one after the other quickly', () => {
let c1: AdminPendingCreation | void
let c2: AdminPendingCreation | void
let c1: Contribution | void
let c2: Contribution | void
beforeAll(async () => {
const now = new Date()
@ -1304,25 +1319,25 @@ describe('AdminResolver', () => {
// In the futrue this should not throw anymore
it('throws an error for the second confirmation', async () => {
const r1 = mutate({
mutation: confirmPendingCreation,
mutation: confirmContribution,
variables: {
id: c1 ? c1.id : -1,
},
})
const r2 = mutate({
mutation: confirmPendingCreation,
mutation: confirmContribution,
variables: {
id: c2 ? c2.id : -1,
},
})
await expect(r1).resolves.toEqual(
expect.objectContaining({
data: { confirmPendingCreation: true },
data: { confirmContribution: true },
}),
)
await expect(r2).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Unable to confirm creation.')],
errors: [new GraphQLError('Creation was not successful.')],
}),
)
})
@ -1593,4 +1608,361 @@ describe('AdminResolver', () => {
})
})
})
describe('Contribution Links', () => {
const variables = {
amount: new Decimal(200),
name: 'Dokumenta 2022',
memo: 'Danke für deine Teilnahme an der Dokumenta 2022',
cycle: 'once',
validFrom: new Date(2022, 5, 18).toISOString(),
validTo: new Date(2022, 7, 14).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
}
describe('unauthenticated', () => {
describe('createContributionLink', () => {
it('returns an error', async () => {
await expect(mutate({ mutation: createContributionLink, variables })).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('listContributionLinks', () => {
it('returns an error', async () => {
await expect(query({ query: listContributionLinks })).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('updateContributionLink', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: updateContributionLink,
variables: {
...variables,
id: -1,
amount: new Decimal(400),
name: 'Dokumenta 2023',
memo: 'Danke für deine Teilnahme an der Dokumenta 2023',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('deleteContributionLink', () => {
it('returns an error', async () => {
await expect(
mutate({ mutation: deleteContributionLink, variables: { id: -1 } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
})
describe('authenticated', () => {
describe('without admin rights', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('createContributionLink', () => {
it('returns an error', async () => {
await expect(mutate({ mutation: createContributionLink, variables })).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('listContributionLinks', () => {
it('returns an error', async () => {
await expect(query({ query: listContributionLinks })).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('updateContributionLink', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: updateContributionLink,
variables: {
...variables,
id: -1,
amount: new Decimal(400),
name: 'Dokumenta 2023',
memo: 'Danke für deine Teilnahme an der Dokumenta 2023',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('deleteContributionLink', () => {
it('returns an error', async () => {
await expect(
mutate({ mutation: deleteContributionLink, variables: { id: -1 } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
})
describe('with admin rights', () => {
beforeAll(async () => {
user = await userFactory(testEnv, peterLustig)
await query({
query: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('createContributionLink', () => {
it('returns a contribution link object', async () => {
await expect(mutate({ mutation: createContributionLink, variables })).resolves.toEqual(
expect.objectContaining({
data: {
createContributionLink: expect.objectContaining({
id: expect.any(Number),
amount: '200',
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
link: expect.stringMatching(/^.*?\/CL-[0-9a-f]{24,24}$/),
createdAt: expect.any(String),
name: 'Dokumenta 2022',
memo: 'Danke für deine Teilnahme an der Dokumenta 2022',
validFrom: expect.any(String),
validTo: expect.any(String),
maxAmountPerMonth: '200',
cycle: 'once',
maxPerCycle: 1,
}),
},
}),
)
})
it('has a contribution link stored in db', async () => {
const cls = await DbContributionLink.find()
expect(cls).toHaveLength(1)
expect(cls[0]).toEqual(
expect.objectContaining({
id: expect.any(Number),
name: 'Dokumenta 2022',
memo: 'Danke für deine Teilnahme an der Dokumenta 2022',
validFrom: new Date('2022-06-18T00:00:00.000Z'),
validTo: new Date('2022-08-14T00:00:00.000Z'),
cycle: 'once',
maxPerCycle: 1,
totalMaxCountOfContribution: null,
maxAccountBalance: null,
minGapHours: null,
createdAt: expect.any(Date),
deletedAt: null,
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
linkEnabled: true,
// amount: '200',
// maxAmountPerMonth: '200',
}),
)
})
})
describe('listContributionLinks', () => {
describe('one link in DB', () => {
it('returns the link and count 1', async () => {
await expect(query({ query: listContributionLinks })).resolves.toEqual(
expect.objectContaining({
data: {
listContributionLinks: {
links: expect.arrayContaining([
expect.objectContaining({
amount: '200',
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
link: expect.stringMatching(/^.*?\/CL-[0-9a-f]{24,24}$/),
createdAt: expect.any(String),
name: 'Dokumenta 2022',
memo: 'Danke für deine Teilnahme an der Dokumenta 2022',
validFrom: expect.any(String),
validTo: expect.any(String),
maxAmountPerMonth: '200',
cycle: 'once',
maxPerCycle: 1,
}),
]),
count: 1,
},
},
}),
)
})
})
})
describe('updateContributionLink', () => {
describe('no valid id', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: updateContributionLink,
variables: {
...variables,
id: -1,
amount: new Decimal(400),
name: 'Dokumenta 2023',
memo: 'Danke für deine Teilnahme an der Dokumenta 2023',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Contribution Link not found to given id.')],
}),
)
})
})
describe('valid id', () => {
let linkId: number
beforeAll(async () => {
const links = await query({ query: listContributionLinks })
linkId = links.data.listContributionLinks.links[0].id
})
it('returns updated contribution link object', async () => {
await expect(
mutate({
mutation: updateContributionLink,
variables: {
...variables,
id: linkId,
amount: new Decimal(400),
name: 'Dokumenta 2023',
memo: 'Danke für deine Teilnahme an der Dokumenta 2023',
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
updateContributionLink: {
id: linkId,
amount: '400',
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
link: expect.stringMatching(/^.*?\/CL-[0-9a-f]{24,24}$/),
createdAt: expect.any(String),
name: 'Dokumenta 2023',
memo: 'Danke für deine Teilnahme an der Dokumenta 2023',
validFrom: expect.any(String),
validTo: expect.any(String),
maxAmountPerMonth: '200',
cycle: 'once',
maxPerCycle: 1,
},
},
}),
)
})
it('updated the DB record', async () => {
await expect(DbContributionLink.findOne(linkId)).resolves.toEqual(
expect.objectContaining({
id: linkId,
name: 'Dokumenta 2023',
memo: 'Danke für deine Teilnahme an der Dokumenta 2023',
// amount: '400',
}),
)
})
})
})
describe('deleteContributionLink', () => {
describe('no valid id', () => {
it('returns an error', async () => {
await expect(
mutate({ mutation: deleteContributionLink, variables: { id: -1 } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Contribution Link not found to given id.')],
}),
)
})
})
describe('valid id', () => {
let linkId: number
beforeAll(async () => {
const links = await query({ query: listContributionLinks })
linkId = links.data.listContributionLinks.links[0].id
})
it('returns a date string', async () => {
await expect(
mutate({ mutation: deleteContributionLink, variables: { id: linkId } }),
).resolves.toEqual(
expect.objectContaining({
data: {
deleteContributionLink: expect.any(String),
},
}),
)
})
it('does not list this contribution link anymore', async () => {
await expect(query({ query: listContributionLinks })).resolves.toEqual(
expect.objectContaining({
data: {
listContributionLinks: {
links: [],
count: 0,
},
},
}),
)
})
})
})
})
})
})
})

View File

@ -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,6 +44,7 @@ import { Order } from '@enum/Order'
import { communityUser } from '@/util/communityUser'
import { checkOptInCode, activationLink, printTimeDuration } from './UserResolver'
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
import CONFIG from '@/config'
// const EMAIL_OPT_IN_REGISTER = 1
@ -163,72 +169,76 @@ 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[]> {
logger.trace('adminCreateContribution...')
const user = await dbUser.findOne({ email }, { withDeleted: true })
if (!user) {
throw new Error(`Could not find user with email: ${email}`)
}
if (user.deletedAt) {
throw new Error('This user was deleted. Cannot make a creation.')
throw new Error('This user was deleted. Cannot create a contribution.')
}
if (!user.emailChecked) {
throw new Error('Creation could not be saved, Email is not activated')
throw new Error('Contribution could not be saved, Email is not activated')
}
const moderator = getUser(context)
logger.trace('moderator: ', moderator.id)
const creations = await getUserCreation(user.id)
logger.trace('creations', creations)
const creationDateObj = new Date(creationDate)
if (isCreationValid(creations, amount, creationDateObj)) {
const adminPendingCreation = AdminPendingCreation.create()
adminPendingCreation.userId = user.id
adminPendingCreation.amount = amount
adminPendingCreation.created = new Date()
adminPendingCreation.date = creationDateObj
adminPendingCreation.memo = memo
adminPendingCreation.moderator = moderator.id
if (isContributionValid(creations, amount, creationDateObj)) {
const contribution = Contribution.create()
contribution.userId = user.id
contribution.amount = amount
contribution.createdAt = new Date()
contribution.contributionDate = creationDateObj
contribution.memo = memo
contribution.moderatorId = moderator.id
await AdminPendingCreation.save(adminPendingCreation)
logger.trace('contribution to save', contribution)
await Contribution.save(contribution)
}
return getUserCreation(user.id)
}
@Authorized([RIGHTS.CREATE_PENDING_CREATION])
@Mutation(() => CreatePendingCreations)
async createPendingCreations(
@Arg('pendingCreations', () => [CreatePendingCreationArgs])
pendingCreations: CreatePendingCreationArgs[],
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTIONS])
@Mutation(() => AdminCreateContributions)
async adminCreateContributions(
@Arg('pendingCreations', () => [AdminCreateContributionArgs])
contributions: AdminCreateContributionArgs[],
@Ctx() context: Context,
): Promise<CreatePendingCreations> {
): Promise<AdminCreateContributions> {
let success = false
const successfulCreation: string[] = []
const failedCreation: string[] = []
for (const pendingCreation of pendingCreations) {
await this.createPendingCreation(pendingCreation, context)
const successfulContribution: string[] = []
const failedContribution: string[] = []
for (const contribution of contributions) {
await this.adminCreateContribution(contribution, context)
.then(() => {
successfulCreation.push(pendingCreation.email)
successfulContribution.push(contribution.email)
success = true
})
.catch(() => {
failedCreation.push(pendingCreation.email)
failedContribution.push(contribution.email)
})
}
return {
success,
successfulCreation,
failedCreation,
successfulContribution,
failedContribution,
}
}
@Authorized([RIGHTS.UPDATE_PENDING_CREATION])
@Mutation(() => UpdatePendingCreation)
async updatePendingCreation(
@Args() { id, email, amount, memo, creationDate }: UpdatePendingCreationArgs,
@Authorized([RIGHTS.ADMIN_UPDATE_CONTRIBUTION])
@Mutation(() => AdminUpdateContribution)
async adminUpdateContribution(
@Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs,
@Ctx() context: Context,
): Promise<UpdatePendingCreation> {
): Promise<AdminUpdateContribution> {
const user = await dbUser.findOne({ email }, { withDeleted: true })
if (!user) {
throw new Error(`Could not find user with email: ${email}`)
@ -239,59 +249,65 @@ export class AdminResolver {
const moderator = getUser(context)
const pendingCreationToUpdate = await AdminPendingCreation.findOne({ id })
const contributionToUpdate = await Contribution.findOne({
where: { id, confirmedAt: IsNull() },
})
if (!pendingCreationToUpdate) {
throw new Error('No creation found to given id.')
if (!contributionToUpdate) {
throw new Error('No contribution found to given id.')
}
if (pendingCreationToUpdate.userId !== user.id) {
throw new Error('user of the pending creation and send user does not correspond')
if (contributionToUpdate.userId !== user.id) {
throw new Error('user of the pending contribution and send user does not correspond')
}
const creationDateObj = new Date(creationDate)
let creations = await getUserCreation(user.id)
if (pendingCreationToUpdate.date.getMonth() === creationDateObj.getMonth()) {
creations = updateCreations(creations, pendingCreationToUpdate)
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
creations = updateCreations(creations, contributionToUpdate)
}
// all possible cases not to be true are thrown in this function
isCreationValid(creations, amount, creationDateObj)
pendingCreationToUpdate.amount = amount
pendingCreationToUpdate.memo = memo
pendingCreationToUpdate.date = new Date(creationDate)
pendingCreationToUpdate.moderator = moderator.id
isContributionValid(creations, amount, creationDateObj)
contributionToUpdate.amount = amount
contributionToUpdate.memo = memo
contributionToUpdate.contributionDate = new Date(creationDate)
contributionToUpdate.moderatorId = moderator.id
await AdminPendingCreation.save(pendingCreationToUpdate)
const result = new UpdatePendingCreation()
await Contribution.save(contributionToUpdate)
const result = new AdminUpdateContribution()
result.amount = amount
result.memo = pendingCreationToUpdate.memo
result.date = pendingCreationToUpdate.date
result.memo = contributionToUpdate.memo
result.date = contributionToUpdate.contributionDate
result.creation = await getUserCreation(user.id)
return result
}
@Authorized([RIGHTS.SEARCH_PENDING_CREATION])
@Query(() => [PendingCreation])
async getPendingCreations(): Promise<PendingCreation[]> {
const pendingCreations = await AdminPendingCreation.find()
if (pendingCreations.length === 0) {
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
@Query(() => [UnconfirmedContribution])
async listUnconfirmedContributions(): Promise<UnconfirmedContribution[]> {
const contributions = await Contribution.find({ where: { confirmedAt: IsNull() } })
if (contributions.length === 0) {
return []
}
const userIds = pendingCreations.map((p) => p.userId)
const userIds = contributions.map((p) => p.userId)
const userCreations = await getUserCreations(userIds)
const users = await dbUser.find({ where: { id: In(userIds) }, withDeleted: true })
return pendingCreations.map((pendingCreation) => {
const user = users.find((u) => u.id === pendingCreation.userId)
const creation = userCreations.find((c) => c.id === pendingCreation.userId)
return contributions.map((contribution) => {
const user = users.find((u) => u.id === contribution.userId)
const creation = userCreations.find((c) => c.id === contribution.userId)
return {
...pendingCreation,
amount: pendingCreation.amount,
id: contribution.id,
userId: contribution.userId,
date: contribution.contributionDate,
memo: contribution.memo,
amount: contribution.amount,
moderator: contribution.moderatorId,
firstName: user ? user.firstName : '',
lastName: user ? user.lastName : '',
email: user ? user.email : '',
@ -300,69 +316,93 @@ export class AdminResolver {
})
}
@Authorized([RIGHTS.DELETE_PENDING_CREATION])
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])
@Mutation(() => Boolean)
async deletePendingCreation(@Arg('id', () => Int) id: number): Promise<boolean> {
const pendingCreation = await AdminPendingCreation.findOne(id)
if (!pendingCreation) {
throw new Error('Creation not found for given id.')
async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise<boolean> {
const contribution = await Contribution.findOne(id)
if (!contribution) {
throw new Error('Contribution not found for given id.')
}
const res = await AdminPendingCreation.delete(pendingCreation)
const res = await contribution.softRemove()
return !!res
}
@Authorized([RIGHTS.CONFIRM_PENDING_CREATION])
@Authorized([RIGHTS.CONFIRM_CONTRIBUTION])
@Mutation(() => Boolean)
async confirmPendingCreation(
async confirmContribution(
@Arg('id', () => Int) id: number,
@Ctx() context: Context,
): Promise<boolean> {
const pendingCreation = await AdminPendingCreation.findOne(id)
if (!pendingCreation) {
throw new Error('Creation not found to given id.')
const contribution = await Contribution.findOne(id)
if (!contribution) {
throw new Error('Contribution not found to given id.')
}
const moderatorUser = getUser(context)
if (moderatorUser.id === pendingCreation.userId)
throw new Error('Moderator can not confirm own pending creation')
if (moderatorUser.id === contribution.userId)
throw new Error('Moderator can not confirm own contribution')
const user = await dbUser.findOneOrFail({ id: pendingCreation.userId }, { withDeleted: true })
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a creation.')
const user = await dbUser.findOneOrFail({ id: contribution.userId }, { withDeleted: true })
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a contribution.')
const creations = await getUserCreation(pendingCreation.userId, false)
if (!isCreationValid(creations, pendingCreation.amount, pendingCreation.date)) {
const creations = await getUserCreation(contribution.userId, false)
if (!isContributionValid(creations, contribution.amount, contribution.contributionDate)) {
throw new Error('Creation is not valid!!')
}
const receivedCallDate = new Date()
const transactionRepository = getCustomRepository(TransactionRepository)
const lastTransaction = await transactionRepository.findLastForUser(pendingCreation.userId)
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
try {
const lastTransaction = await queryRunner.manager
.createQueryBuilder()
.select('transaction')
.from(DbTransaction, 'transaction')
.where('transaction.userId = :id', { id: contribution.userId })
.orderBy('transaction.balanceDate', 'DESC')
.getOne()
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
let newBalance = new Decimal(0)
let decay: Decay | null = null
if (lastTransaction) {
decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, receivedCallDate)
newBalance = decay.balance
let newBalance = new Decimal(0)
let decay: Decay | null = null
if (lastTransaction) {
decay = calculateDecay(
lastTransaction.balance,
lastTransaction.balanceDate,
receivedCallDate,
)
newBalance = decay.balance
}
newBalance = newBalance.add(contribution.amount.toString())
const transaction = new DbTransaction()
transaction.typeId = TransactionTypeId.CREATION
transaction.memo = contribution.memo
transaction.userId = contribution.userId
transaction.previous = lastTransaction ? lastTransaction.id : null
transaction.amount = contribution.amount
transaction.creationDate = contribution.contributionDate
transaction.balance = newBalance
transaction.balanceDate = receivedCallDate
transaction.decay = decay ? decay.decay : new Decimal(0)
transaction.decayStart = decay ? decay.start : null
await queryRunner.manager.insert(DbTransaction, transaction)
contribution.confirmedAt = receivedCallDate
contribution.confirmedBy = moderatorUser.id
contribution.transactionId = transaction.id
await queryRunner.manager.update(Contribution, { id: contribution.id }, contribution)
await queryRunner.commitTransaction()
logger.info('creation commited successfuly.')
} catch (e) {
await queryRunner.rollbackTransaction()
logger.error(`Creation was not successful: ${e}`)
throw new Error(`Creation was not successful.`)
} finally {
await queryRunner.release()
}
newBalance = newBalance.add(pendingCreation.amount.toString())
const transaction = new DbTransaction()
transaction.typeId = TransactionTypeId.CREATION
transaction.memo = pendingCreation.memo
transaction.userId = pendingCreation.userId
transaction.previous = lastTransaction ? lastTransaction.id : null
transaction.amount = pendingCreation.amount
transaction.creationDate = pendingCreation.date
transaction.balance = newBalance
transaction.balanceDate = receivedCallDate
transaction.decay = decay ? decay.decay : new Decimal(0)
transaction.decayStart = decay ? decay.start : null
await transaction.save().catch(() => {
throw new Error('Unable to confirm creation.')
})
await AdminPendingCreation.delete(pendingCreation)
return true
}
@ -460,6 +500,99 @@ export class AdminResolver {
linkList: transactionLinks.map((tl) => new TransactionLink(tl, new User(user))),
}
}
@Authorized([RIGHTS.CREATE_CONTRIBUTION_LINK])
@Mutation(() => ContributionLink)
async createContributionLink(
@Args()
{
amount,
name,
memo,
cycle,
validFrom,
validTo,
maxAmountPerMonth,
maxPerCycle,
}: ContributionLinkArgs,
): Promise<ContributionLink> {
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()
return new ContributionLink(dbContributionLink)
}
@Authorized([RIGHTS.LIST_CONTRIBUTION_LINKS])
@Query(() => ContributionLinkList)
async listContributionLinks(
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
): Promise<ContributionLinkList> {
const [links, count] = await DbContributionLink.findAndCount({
order: { createdAt: order },
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
return {
links: links.map((link: DbContributionLink) => new ContributionLink(link)),
count,
}
}
@Authorized([RIGHTS.DELETE_CONTRIBUTION_LINK])
@Mutation(() => Date, { nullable: true })
async deleteContributionLink(@Arg('id', () => Int) id: number): Promise<Date | null> {
const contributionLink = await DbContributionLink.findOne(id)
if (!contributionLink) {
logger.error(`Contribution Link not found to given id: ${id}`)
throw new Error('Contribution Link not found to given id.')
}
await contributionLink.softRemove()
const newContributionLink = await DbContributionLink.findOne({ id }, { withDeleted: true })
return newContributionLink ? newContributionLink.deletedAt : null
}
@Authorized([RIGHTS.UPDATE_CONTRIBUTION_LINK])
@Mutation(() => ContributionLink)
async updateContributionLink(
@Args()
{
amount,
name,
memo,
cycle,
validFrom,
validTo,
maxAmountPerMonth,
maxPerCycle,
}: ContributionLinkArgs,
@Arg('id', () => Int) id: number,
): Promise<ContributionLink> {
const dbContributionLink = await DbContributionLink.findOne(id)
if (!dbContributionLink) {
logger.error(`Contribution Link not found to given id: ${id}`)
throw new Error('Contribution Link not found to given id.')
}
dbContributionLink.amount = amount
dbContributionLink.name = name
dbContributionLink.memo = memo
dbContributionLink.cycle = cycle
if (validFrom) dbContributionLink.validFrom = new Date(validFrom)
if (validTo) dbContributionLink.validTo = new Date(validTo)
dbContributionLink.maxAmountPerMonth = maxAmountPerMonth
dbContributionLink.maxPerCycle = maxPerCycle
await dbContributionLink.save()
return new ContributionLink(dbContributionLink)
}
}
interface CreationMap {
@ -468,24 +601,29 @@ interface CreationMap {
}
async function getUserCreation(id: number, includePending = true): Promise<Decimal[]> {
logger.trace('getUserCreation', id, includePending)
const creations = await getUserCreations([id], includePending)
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
}
async function getUserCreations(ids: number[], includePending = true): Promise<CreationMap[]> {
logger.trace('getUserCreations:', ids, includePending)
const months = getCreationMonths()
logger.trace('getUserCreations months', months)
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
logger.trace('getUserCreations dateFilter', dateFilter)
const unionString = includePending
? `
UNION
SELECT date AS date, amount AS amount, userId AS userId FROM admin_pending_creations
WHERE userId IN (${ids.toString()})
AND date >= ${dateFilter}`
SELECT contribution_date AS date, amount AS amount, user_id AS userId FROM contributions
WHERE user_id IN (${ids.toString()})
AND contribution_date >= ${dateFilter}
AND confirmed_at IS NULL AND deleted_at IS NULL`
: ''
const unionQuery = await queryRunner.manager.query(`
@ -515,17 +653,18 @@ async function getUserCreations(ids: number[], includePending = true): Promise<C
})
}
function updateCreations(creations: Decimal[], pendingCreation: AdminPendingCreation): Decimal[] {
const index = getCreationIndex(pendingCreation.date.getMonth())
function updateCreations(creations: Decimal[], contribution: Contribution): Decimal[] {
const index = getCreationIndex(contribution.contributionDate.getMonth())
if (index < 0) {
throw new Error('You cannot create GDD for a month older than the last three months.')
}
creations[index] = creations[index].plus(pendingCreation.amount.toString())
creations[index] = creations[index].plus(contribution.amount.toString())
return creations
}
function isCreationValid(creations: Decimal[], amount: Decimal, creationDate: Date) {
function isContributionValid(creations: Decimal[], amount: Decimal, creationDate: Date) {
logger.trace('isContributionValid', creations, amount, creationDate)
const index = getCreationIndex(creationDate.getMonth())
if (index < 0) {

View File

@ -0,0 +1,7 @@
export interface ContributionLinkInterface {
amount: number
name: string
memo: string
validFrom?: Date
validTo?: Date
}

View 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),
},
]

View File

@ -0,0 +1,27 @@
import { ApolloServerTestClient } from 'apollo-server-testing'
import { createContributionLink } from '@/seeds/graphql/mutations'
import { login } from '@/seeds/graphql/queries'
import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface'
export const contributionLinkFactory = async (
client: ApolloServerTestClient,
contributionLink: ContributionLinkInterface,
): Promise<void> => {
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,
}
await mutate({ mutation: createContributionLink, variables })
}

View File

@ -1,13 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createPendingCreation, confirmPendingCreation } from '@/seeds/graphql/mutations'
import { adminCreateContribution, confirmContribution } from '@/seeds/graphql/mutations'
import { login } from '@/seeds/graphql/queries'
import { CreationInterface } from '@/seeds/creation/CreationInterface'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { User } from '@entity/User'
import { Transaction } from '@entity/Transaction'
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
import { Contribution } from '@entity/Contribution'
// import CONFIG from '@/config/index'
export const nMonthsBefore = (date: Date, months = 1): string => {
@ -17,23 +17,23 @@ export const nMonthsBefore = (date: Date, months = 1): string => {
export const creationFactory = async (
client: ApolloServerTestClient,
creation: CreationInterface,
): Promise<AdminPendingCreation | void> => {
): Promise<Contribution | void> => {
const { mutate, query } = client
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
// TODO it would be nice to have this mutation return the id
await mutate({ mutation: createPendingCreation, variables: { ...creation } })
await mutate({ mutation: adminCreateContribution, variables: { ...creation } })
const user = await User.findOneOrFail({ where: { email: creation.email } })
const pendingCreation = await AdminPendingCreation.findOneOrFail({
const pendingCreation = await Contribution.findOneOrFail({
where: { userId: user.id, amount: creation.amount },
order: { created: 'DESC' },
order: { createdAt: 'DESC' },
})
if (creation.confirmed) {
await mutate({ mutation: confirmPendingCreation, variables: { id: pendingCreation.id } })
await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } })
if (creation.moveCreationDate) {
const transaction = await Transaction.findOneOrFail({

View File

@ -81,15 +81,20 @@ export const createTransactionLink = gql`
// from admin interface
export const createPendingCreation = gql`
export const adminCreateContribution = gql`
mutation ($email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
createPendingCreation(email: $email, amount: $amount, memo: $memo, creationDate: $creationDate)
adminCreateContribution(
email: $email
amount: $amount
memo: $memo
creationDate: $creationDate
)
}
`
export const confirmPendingCreation = gql`
export const confirmContribution = gql`
mutation ($id: Int!) {
confirmPendingCreation(id: $id)
confirmContribution(id: $id)
}
`
@ -105,19 +110,19 @@ export const unDeleteUser = gql`
}
`
export const createPendingCreations = gql`
mutation ($pendingCreations: [CreatePendingCreationArgs!]!) {
createPendingCreations(pendingCreations: $pendingCreations) {
export const adminCreateContributions = gql`
mutation ($pendingCreations: [AdminCreateContributionArgs!]!) {
adminCreateContributions(pendingCreations: $pendingCreations) {
success
successfulCreation
failedCreation
successfulContribution
failedContribution
}
}
`
export const updatePendingCreation = gql`
export const adminUpdateContribution = gql`
mutation ($id: Int!, $email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
updatePendingCreation(
adminUpdateContribution(
id: $id
email: $email
amount: $amount
@ -132,8 +137,90 @@ export const updatePendingCreation = gql`
}
`
export const deletePendingCreation = gql`
export const adminDeleteContribution = gql`
mutation ($id: Int!) {
deletePendingCreation(id: $id)
adminDeleteContribution(id: $id)
}
`
export const createContributionLink = gql`
mutation (
$amount: Decimal!
$name: String!
$memo: String!
$cycle: String!
$validFrom: String
$validTo: String
$maxAmountPerMonth: Decimal
$maxPerCycle: Int! = 1
) {
createContributionLink(
amount: $amount
name: $name
memo: $memo
cycle: $cycle
validFrom: $validFrom
validTo: $validTo
maxAmountPerMonth: $maxAmountPerMonth
maxPerCycle: $maxPerCycle
) {
id
amount
name
memo
code
link
createdAt
validFrom
validTo
maxAmountPerMonth
cycle
maxPerCycle
}
}
`
export const updateContributionLink = gql`
mutation (
$amount: Decimal!
$name: String!
$memo: String!
$cycle: String!
$validFrom: String
$validTo: String
$maxAmountPerMonth: Decimal
$maxPerCycle: Int! = 1
$id: Int!
) {
updateContributionLink(
amount: $amount
name: $name
memo: $memo
cycle: $cycle
validFrom: $validFrom
validTo: $validTo
maxAmountPerMonth: $maxAmountPerMonth
maxPerCycle: $maxPerCycle
id: $id
) {
id
amount
name
memo
code
link
createdAt
validFrom
validTo
maxAmountPerMonth
cycle
maxPerCycle
}
}
`
export const deleteContributionLink = gql`
mutation ($id: Int!) {
deleteContributionLink(id: $id)
}
`

View File

@ -173,9 +173,9 @@ export const queryTransactionLink = gql`
// from admin interface
export const getPendingCreations = gql`
export const listUnconfirmedContributions = gql`
query {
getPendingCreations {
listUnconfirmedContributions {
id
firstName
lastName
@ -217,3 +217,25 @@ export const listTransactionLinksAdmin = gql`
}
}
`
export const listContributionLinks = gql`
query ($pageSize: Int = 25, $currentPage: Int = 1, $order: Order) {
listContributionLinks(pageSize: $pageSize, currentPage: $currentPage, order: $order) {
links {
id
amount
name
memo
code
link
createdAt
validFrom
validTo
maxAmountPerMonth
cycle
maxPerCycle
}
count
}
}
`

View File

@ -9,9 +9,11 @@ import { name, internet, datatype } from 'faker'
import { users } from './users/index'
import { creations } from './creation/index'
import { transactionLinks } from './transactionLink/index'
import { contributionLinks } from './contributionLink/index'
import { userFactory } from './factory/user'
import { creationFactory } from './factory/creation'
import { transactionLinkFactory } from './factory/transactionLink'
import { contributionLinkFactory } from './factory/contributionLink'
import { entities } from '@entity/index'
import CONFIG from '@/config'
@ -77,6 +79,11 @@ const run = async () => {
await transactionLinkFactory(seedClient, transactionLinks[i])
}
// create Contribution Links
for (let i = 0; i < contributionLinks.length; i++) {
await contributionLinkFactory(seedClient, contributionLinks[i])
}
await con.close()
}

View File

@ -1,25 +0,0 @@
import { EntityRepository, Repository } from '@dbTools/typeorm'
import { UserSetting } from '@entity/UserSetting'
import { isStringBoolean } from '@/util/validate'
@EntityRepository(UserSetting)
export class UserSettingRepository extends Repository<UserSetting> {
async setOrUpdate(userId: number, value: string): Promise<UserSetting> {
let entity = await this.findOne({ userId: userId })
if (!entity) {
entity = new UserSetting()
entity.userId = userId
}
entity.value = value
return this.save(entity)
}
async readBoolean(userId: number): Promise<boolean> {
const entity = await this.findOne({ userId: userId })
if (!entity || !isStringBoolean(entity.value)) {
return true
}
return entity.value.toLowerCase() === 'true'
}
}

View File

@ -20,7 +20,6 @@ const communityDbUser: dbUser = {
isAdmin: null,
publisherId: 0,
passphrase: '',
settings: [],
hasId: function (): boolean {
throw new Error('Function not implemented.')
},

View File

@ -1,32 +1,39 @@
# database
## Project setup
```
```bash
yarn install
```
## Upgrade migrations production
```
```bash
yarn up
```
## Upgrade migrations development
```
```bash
yarn dev_up
```
## Downgrade migrations production
```
```bash
yarn down
```
## Downgrade migrations development
```
```bash
yarn dev_down
```
## Reset database
```
```bash
yarn dev_reset
```
Runs all down migrations and after this all up migrations.

View File

@ -1,5 +1,5 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'
import { UserSetting } from '../UserSetting'
import { UserSetting } from '../0002-add_settings/UserSetting'
// Moriz: I do not like the idea of having two user tables
@Entity('state_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })

View File

@ -1,5 +1,5 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'
import { User } from '../User'
import { User } from '../0034-drop_server_user_table/User'
@Entity()
export class UserSetting extends BaseEntity {

View File

@ -1,5 +1,5 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'
import { UserSetting } from '../UserSetting'
import { UserSetting } from '../0002-add_settings/UserSetting'
@Entity('state_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {

View File

@ -1,5 +1,5 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'
import { UserSetting } from '../UserSetting'
import { UserSetting } from '../0002-add_settings/UserSetting'
@Entity('state_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {

View File

@ -1,5 +1,5 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'
import { UserSetting } from '../UserSetting'
import { UserSetting } from '../0002-add_settings/UserSetting'
@Entity('state_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {

View File

@ -1,5 +1,5 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'
import { UserSetting } from '../UserSetting'
import { UserSetting } from '../0002-add_settings/UserSetting'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {

View File

@ -6,7 +6,7 @@ import {
OneToMany,
DeleteDateColumn,
} from 'typeorm'
import { UserSetting } from '../UserSetting'
import { UserSetting } from '../0002-add_settings/UserSetting'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {

View File

@ -6,7 +6,7 @@ import {
OneToMany,
DeleteDateColumn,
} from 'typeorm'
import { UserSetting } from '../UserSetting'
import { UserSetting } from '../0002-add_settings/UserSetting'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {

View File

@ -6,7 +6,7 @@ import {
OneToMany,
DeleteDateColumn,
} from 'typeorm'
import { UserSetting } from '../UserSetting'
import { UserSetting } from '../0002-add_settings/UserSetting'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {

View File

@ -0,0 +1,70 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
pubKey: Buffer
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
privKey: Buffer
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
email: string
@Column({
name: 'first_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
firstName: string
@Column({
name: 'last_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
lastName: string
@DeleteDateColumn()
deletedAt: Date | null
@Column({ type: 'bigint', default: 0, unsigned: true })
password: BigInt
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
emailHash: Buffer
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
createdAt: Date
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
emailChecked: boolean
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
language: string
@Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
isAdmin: Date | null
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
referrerId?: number | null
@Column({ name: 'publisher_id', default: 0 })
publisherId: number
@Column({
type: 'text',
name: 'passphrase',
collation: 'utf8mb4_unicode_ci',
nullable: true,
default: null,
})
passphrase: string
}

View File

@ -0,0 +1,88 @@
import Decimal from 'decimal.js-light'
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
@Entity('contribution_links')
export class ContributionLink extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ length: 100, nullable: false, collation: 'utf8mb4_unicode_ci' })
name: string
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({ name: 'valid_from', type: 'datetime', nullable: false })
validFrom: Date
@Column({ name: 'valid_to', type: 'datetime', nullable: true, default: null })
validTo: Date | null
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
amount: Decimal
@Column({ length: 12, nullable: false, collation: 'utf8mb4_unicode_ci' })
cycle: string
@Column({ name: 'max_per_cycle', unsigned: true, nullable: false, default: 1 })
maxPerCycle: number
@Column({
name: 'max_amount_per_month',
type: 'decimal',
precision: 40,
scale: 20,
nullable: true,
default: null,
transformer: DecimalTransformer,
})
maxAmountPerMonth: Decimal | null
@Column({
name: 'total_max_count_of_contribution',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
totalMaxCountOfContribution: number | null
@Column({
name: 'max_account_balance',
type: 'decimal',
precision: 40,
scale: 20,
nullable: true,
default: null,
transformer: DecimalTransformer,
})
maxAccountBalance: Decimal | null
@Column({
name: 'min_gap_hours',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
minGapHours: number | null
@Column({ name: 'created_at', type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date
@DeleteDateColumn({ name: 'deleted_at' })
deletedAt: Date | null
@Column({ length: 24, nullable: false, collation: 'utf8mb4_unicode_ci' })
code: string
@Column({ name: 'link_enabled', type: 'boolean', default: true })
linkEnabled: boolean
}

View File

@ -0,0 +1,48 @@
import Decimal from 'decimal.js-light'
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, DeleteDateColumn } from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
@Entity('contributions')
export class Contribution extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ unsigned: true, nullable: false, name: 'user_id' })
userId: number
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
createdAt: Date
@Column({ type: 'datetime', nullable: false, name: 'contribution_date' })
contributionDate: Date
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
amount: Decimal
@Column({ unsigned: true, nullable: true, name: 'moderator_id' })
moderatorId: number
@Column({ unsigned: true, nullable: true, name: 'contribution_link_id' })
contributionLinkId: number
@Column({ unsigned: true, nullable: true, name: 'confirmed_by' })
confirmedBy: number
@Column({ nullable: true, name: 'confirmed_at' })
confirmedAt: Date
@Column({ unsigned: true, nullable: true, name: 'transaction_id' })
transactionId: number
@DeleteDateColumn({ name: 'deleted_at' })
deletedAt: Date | null
}

View File

@ -0,0 +1 @@
export { Contribution } from './0039-contributions_table/Contribution'

View File

@ -0,0 +1 @@
export { ContributionLink } from './0038-add_contribution_links_table/ContributionLink'

View File

@ -1 +1 @@
export { User } from './0034-drop_server_user_table/User'
export { User } from './0037-drop_user_setting_table/User'

View File

@ -1 +0,0 @@
export { UserSetting } from './0002-add_settings/UserSetting'

View File

@ -1,19 +1,19 @@
import { ContributionLink } from './ContributionLink'
import { LoginElopageBuys } from './LoginElopageBuys'
import { LoginEmailOptIn } from './LoginEmailOptIn'
import { Migration } from './Migration'
import { Transaction } from './Transaction'
import { TransactionLink } from './TransactionLink'
import { User } from './User'
import { UserSetting } from './UserSetting'
import { AdminPendingCreation } from './AdminPendingCreation'
import { Contribution } from './Contribution'
export const entities = [
AdminPendingCreation,
Contribution,
ContributionLink,
LoginElopageBuys,
LoginEmailOptIn,
Migration,
Transaction,
TransactionLink,
User,
UserSetting,
]

View File

@ -0,0 +1,19 @@
/* MIGRATION DROP user_setting TABLE */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('DROP TABLE `user_setting`;')
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`
CREATE TABLE IF NOT EXISTS \`user_setting\` (
\`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
\`userId\` int(11) NOT NULL,
\`key\` varchar(255) NOT NULL,
\`value\` varchar(255) NOT NULL,
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`)
}

View File

@ -0,0 +1,35 @@
/* MIGRATION TO ADD CONTRIBUTION_LINKS
*
* This migration adds the table `contribution_links` in order to store all sorts of contribution_links data
*/
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`
CREATE TABLE IF NOT EXISTS \`contribution_links\` (
\`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
\`name\` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
\`memo\` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
\`valid_from\` datetime NOT NULL,
\`valid_to\` datetime NULL,
\`amount\` bigint(20) NOT NULL,
\`cycle\` varchar(12) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'ONCE',
\`max_per_cycle\` int(10) unsigned NOT NULL DEFAULT '1',
\`max_amount_per_month\` bigint(20) NULL DEFAULT NULL,
\`total_max_count_of_contribution\` int(10) unsigned NULL DEFAULT NULL,
\`max_account_balance\` bigint(20) NULL DEFAULT NULL,
\`min_gap_hours\` int(10) unsigned NULL DEFAULT NULL,
\`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
\`deleted_at\` datetime NULL DEFAULT NULL,
\`code\` varchar(24) COLLATE utf8mb4_unicode_ci NOT NULL,
\`link_enabled\` tinyint(4) NOT NULL DEFAULT '1',
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// write downgrade logic as parameter of queryFn
await queryFn(`DROP TABLE IF EXISTS \`contribution_links\`;`)
}

View File

@ -0,0 +1,59 @@
/* MIGRATION to rename ADMIN_PENDING_CREATION table and add columns
*/
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('RENAME TABLE `admin_pending_creations` TO `contributions`;')
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `userId` `user_id` int(10);')
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `created` `created_at` datetime;')
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `date` `contribution_date` datetime;')
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `moderator` `moderator_id` int(10);')
await queryFn(
'ALTER TABLE `contributions` ADD COLUMN `contribution_link_id` int(10) unsigned DEFAULT NULL AFTER `moderator_id`;',
)
await queryFn(
'ALTER TABLE `contributions` ADD COLUMN `confirmed_by` int(10) unsigned DEFAULT NULL AFTER `contribution_link_id`;',
)
await queryFn(
'ALTER TABLE `contributions` ADD COLUMN `confirmed_at` datetime DEFAULT NULL AFTER `confirmed_by`;',
)
await queryFn(
'ALTER TABLE `contributions` ADD COLUMN `transaction_id` int(10) unsigned DEFAULT NULL AFTER `confirmed_at`;',
)
await queryFn(
'ALTER TABLE `contributions` ADD COLUMN `deleted_at` datetime DEFAULT NULL AFTER `confirmed_at`;',
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('ALTER TABLE `contributions` DROP COLUMN IF EXISTS `deleted_at`;')
await queryFn('ALTER TABLE `contributions` DROP COLUMN IF EXISTS `transaction_id`;')
await queryFn('ALTER TABLE `contributions` DROP COLUMN IF EXISTS `confirmed_at`;')
await queryFn('ALTER TABLE `contributions` DROP COLUMN IF EXISTS `confirmed_by`;')
await queryFn('ALTER TABLE `contributions` DROP COLUMN IF EXISTS `contribution_link_id`;')
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `moderator_id` `moderator` int(10);')
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `created_at` `created` datetime;')
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `contribution_date` `date` datetime;')
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `user_id` `userId` int(10);')
await queryFn('RENAME TABLE `contributions` TO `admin_pending_creations`;')
}

View File

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

View File

@ -22,10 +22,11 @@ COMMUNITY_NAME="Gradido Development Stage1"
COMMUNITY_URL=https://stage1.gradido.net/
COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register
COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code}
COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
# backend
BACKEND_CONFIG_VERSION=v6.2022-04-21
BACKEND_CONFIG_VERSION=v7.2022-06-15
JWT_EXPIRES_IN=30m
GDT_API_URL=https://gdt.gradido.net

View File

@ -1,6 +1,6 @@
{
"name": "bootstrap-vue-gradido-wallet",
"version": "1.8.3",
"version": "1.9.0",
"private": true,
"scripts": {
"start": "node run/server.js",

View File

@ -20,7 +20,7 @@
<b-collapse id="nav-collapse" is-nav class="mt-5 mt-lg-0">
<b-navbar-nav class="ml-auto" right>
<b-nav-item href="https://gradido.net/de/" target="_blank">
<b-nav-item :href="`https://gradido.net/${$i18n.locale}`" target="_blank">
{{ $t('auth.navbar.aboutGradido') }}
</b-nav-item>
<b-nav-item to="/register" class="authNavbar ml-lg-5">{{ $t('signup') }}</b-nav-item>

View File

@ -4615,20 +4615,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109:
version "1.0.30001251"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz"
integrity sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==
caniuse-lite@^1.0.30001181, caniuse-lite@^1.0.30001280:
version "1.0.30001285"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001285.tgz#fe1e52229187e11d6670590790d669b9e03315b7"
integrity sha512-KAOkuUtcQ901MtmvxfKD+ODHH9YVDYnBt+TGYSz2KIfnq22CiArbUxXPN9067gNbgMlnNYRSwho8OPXZPALB9Q==
caniuse-lite@^1.0.30001286:
version "1.0.30001303"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001303.tgz#9b168e4f43ccfc372b86f4bc5a551d9b909c95c9"
integrity sha512-/Mqc1oESndUNszJP0kx0UaQU9kEv9nNtJ7Kn8AdA0mNnH8eR1cj0kG+NbNuC1Wq/b21eA8prhKRA3bbkjONegQ==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001181, caniuse-lite@^1.0.30001280, caniuse-lite@^1.0.30001286:
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"

View File

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