Merge remote-tracking branch 'origin/master' into 1921-rebase_from_master
2
.github/workflows/test.yml
vendored
@ -528,7 +528,7 @@ jobs:
|
|||||||
report_name: Coverage Backend
|
report_name: Coverage Backend
|
||||||
type: lcov
|
type: lcov
|
||||||
result_path: ./backend/coverage/lcov.info
|
result_path: ./backend/coverage/lcov.info
|
||||||
min_coverage: 70
|
min_coverage: 68
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|||||||
18
CHANGELOG.md
@ -4,8 +4,26 @@ All notable changes to this project will be documented in this file. Dates are d
|
|||||||
|
|
||||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||||
|
|
||||||
|
#### [1.10.0](https://github.com/gradido/gradido/compare/1.9.0...1.10.0)
|
||||||
|
|
||||||
|
- frontend redeem contribution link [`#1988`](https://github.com/gradido/gradido/pull/1988)
|
||||||
|
- change new start picture [`#1990`](https://github.com/gradido/gradido/pull/1990)
|
||||||
|
- feat: Redeem Contribution Link [`#1987`](https://github.com/gradido/gradido/pull/1987)
|
||||||
|
- fix: Max Amount on Slider for Edit Contribution [`#1986`](https://github.com/gradido/gradido/pull/1986)
|
||||||
|
- CRUD contribution link admin interface [`#1981`](https://github.com/gradido/gradido/pull/1981)
|
||||||
|
- fix: `.env` log level for apollo and backend category [`#1967`](https://github.com/gradido/gradido/pull/1967)
|
||||||
|
- refactor: Admin Pending Creations Table to Contributions Table [`#1949`](https://github.com/gradido/gradido/pull/1949)
|
||||||
|
- devops: Update Browser List for Unit Tests as Recomended [`#1984`](https://github.com/gradido/gradido/pull/1984)
|
||||||
|
- feat: CRUD for Contribution Links in Admin Resolver [`#1979`](https://github.com/gradido/gradido/pull/1979)
|
||||||
|
- 1920 feature create contribution link table [`#1957`](https://github.com/gradido/gradido/pull/1957)
|
||||||
|
- refactor: 🍰 Delete `user_setting` Table From DB [`#1960`](https://github.com/gradido/gradido/pull/1960)
|
||||||
|
- locales link german, english navbar [`#1969`](https://github.com/gradido/gradido/pull/1969)
|
||||||
|
|
||||||
#### [1.9.0](https://github.com/gradido/gradido/compare/1.8.3...1.9.0)
|
#### [1.9.0](https://github.com/gradido/gradido/compare/1.8.3...1.9.0)
|
||||||
|
|
||||||
|
> 2 June 2022
|
||||||
|
|
||||||
|
- devops: Release Version 1.9.0 [`#1968`](https://github.com/gradido/gradido/pull/1968)
|
||||||
- refactor: 🍰 Refactor To `filters` Object And Rename Filters Properties [`#1914`](https://github.com/gradido/gradido/pull/1914)
|
- refactor: 🍰 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)
|
- 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)
|
- fixed redeem link is mobile start false [`#1958`](https://github.com/gradido/gradido/pull/1958)
|
||||||
|
|||||||
@ -22,7 +22,7 @@ module.exports = {
|
|||||||
'^.+\\.(js|jsx)?$': 'babel-jest',
|
'^.+\\.(js|jsx)?$': 'babel-jest',
|
||||||
'<rootDir>/node_modules/vee-validate/dist/rules': 'babel-jest',
|
'<rootDir>/node_modules/vee-validate/dist/rules': 'babel-jest',
|
||||||
},
|
},
|
||||||
setupFiles: ['<rootDir>/test/testSetup.js'],
|
setupFiles: ['<rootDir>/test/testSetup.js', 'jest-canvas-mock'],
|
||||||
testMatch: ['**/?(*.)+(spec|test).js?(x)'],
|
testMatch: ['**/?(*.)+(spec|test).js?(x)'],
|
||||||
// snapshotSerializers: ['jest-serializer-vue'],
|
// snapshotSerializers: ['jest-serializer-vue'],
|
||||||
transformIgnorePatterns: ['<rootDir>/node_modules/(?!vee-validate/dist/rules)'],
|
transformIgnorePatterns: ['<rootDir>/node_modules/(?!vee-validate/dist/rules)'],
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"description": "Administraion Interface for Gradido",
|
"description": "Administraion Interface for Gradido",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"author": "Moriz Wahl",
|
"author": "Moriz Wahl",
|
||||||
"version": "1.9.0",
|
"version": "1.10.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -38,7 +38,9 @@
|
|||||||
"graphql": "^15.6.1",
|
"graphql": "^15.6.1",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "26.6.3",
|
"jest": "26.6.3",
|
||||||
|
"jest-canvas-mock": "^2.3.1",
|
||||||
"portal-vue": "^2.1.7",
|
"portal-vue": "^2.1.7",
|
||||||
|
"qrcanvas-vue": "2.1.1",
|
||||||
"regenerator-runtime": "^0.13.9",
|
"regenerator-runtime": "^0.13.9",
|
||||||
"stats-webpack-plugin": "^0.7.0",
|
"stats-webpack-plugin": "^0.7.0",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
|
|||||||
BIN
admin/public/img/gdd-coin.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
49
admin/src/components/ContributionLink.spec.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import ContributionLink from './ContributionLink.vue'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsData = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Meditation',
|
||||||
|
memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l',
|
||||||
|
amount: '200',
|
||||||
|
validFrom: '2022-04-01',
|
||||||
|
validTo: '2022-08-01',
|
||||||
|
cycle: 'täglich',
|
||||||
|
maxPerCycle: '3',
|
||||||
|
maxAmountPerMonth: 0,
|
||||||
|
link: 'https://localhost/redeem/CL-1a2345678',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
count: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ContributionLink', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(ContributionLink, { localVue, mocks, propsData })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the Div Element ".contribution-link"', () => {
|
||||||
|
expect(wrapper.find('div.contribution-link').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits toggle::collapse new Contribution', async () => {
|
||||||
|
wrapper.vm.editContributionLinkData()
|
||||||
|
expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
66
admin/src/components/ContributionLink.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div class="contribution-link">
|
||||||
|
<b-card
|
||||||
|
border-variant="success"
|
||||||
|
:header="$t('contributionLink.contributionLinks')"
|
||||||
|
header-bg-variant="success"
|
||||||
|
header-text-variant="white"
|
||||||
|
header-class="text-center"
|
||||||
|
class="mt-5"
|
||||||
|
>
|
||||||
|
<b-button v-b-toggle.newContribution class="my-3 d-flex justify-content-left">
|
||||||
|
{{ $t('math.plus') }} {{ $t('contributionLink.newContributionLink') }}
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
<b-collapse v-model="visible" id="newContribution" class="mt-2">
|
||||||
|
<b-card>
|
||||||
|
<p class="h2 ml-5">{{ $t('contributionLink.contributionLinks') }}</p>
|
||||||
|
<contribution-link-form :contributionLinkData="contributionLinkData" />
|
||||||
|
</b-card>
|
||||||
|
</b-collapse>
|
||||||
|
|
||||||
|
<b-card-text>
|
||||||
|
<contribution-link-list
|
||||||
|
v-if="count > 0"
|
||||||
|
:items="items"
|
||||||
|
@editContributionLinkData="editContributionLinkData"
|
||||||
|
/>
|
||||||
|
<div v-else>{{ $t('contributionLink.noContributionLinks') }}</div>
|
||||||
|
</b-card-text>
|
||||||
|
</b-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ContributionLinkForm from './ContributionLinkForm.vue'
|
||||||
|
import ContributionLinkList from './ContributionLinkList.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ContributionLink',
|
||||||
|
components: {
|
||||||
|
ContributionLinkForm,
|
||||||
|
ContributionLinkList,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
visible: false,
|
||||||
|
contributionLinkData: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editContributionLinkData(data) {
|
||||||
|
if (!this.visible) this.$root.$emit('bv::toggle::collapse', 'newContribution')
|
||||||
|
this.contributionLinkData = data
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
102
admin/src/components/ContributionLinkForm.spec.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import ContributionLinkForm from './ContributionLinkForm.vue'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
global.alert = jest.fn()
|
||||||
|
|
||||||
|
const propsData = {
|
||||||
|
contributionLinkData: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
}
|
||||||
|
|
||||||
|
// const mockAPIcall = jest.fn()
|
||||||
|
|
||||||
|
describe('ContributionLinkForm', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(ContributionLinkForm, { localVue, mocks, propsData })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the Div Element ".contribution-link-form"', () => {
|
||||||
|
expect(wrapper.find('div.contribution-link-form').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('call onReset', () => {
|
||||||
|
it('form has the set data', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper.setData({
|
||||||
|
form: {
|
||||||
|
name: 'name',
|
||||||
|
memo: 'memo',
|
||||||
|
amount: 100,
|
||||||
|
validFrom: 'validFrom',
|
||||||
|
validTo: 'validTo',
|
||||||
|
cycle: 'ONCE',
|
||||||
|
maxPerCycle: 1,
|
||||||
|
maxAmountPerMonth: 100,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
wrapper.vm.onReset()
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.form).toEqual({
|
||||||
|
amount: null,
|
||||||
|
cycle: 'ONCE',
|
||||||
|
validTo: null,
|
||||||
|
maxAmountPerMonth: '0',
|
||||||
|
memo: null,
|
||||||
|
name: null,
|
||||||
|
maxPerCycle: 1,
|
||||||
|
validFrom: null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('call onSubmit', () => {
|
||||||
|
it('response with the contribution link url', () => {
|
||||||
|
wrapper.vm.onSubmit()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// describe('successfull submit', () => {
|
||||||
|
// beforeEach(async () => {
|
||||||
|
// mockAPIcall.mockResolvedValue({
|
||||||
|
// data: {
|
||||||
|
// createContributionLink: {
|
||||||
|
// link: 'https://localhost/redeem/CL-1a2345678',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// await wrapper.find('input.test-validFrom').setValue('2022-6-18')
|
||||||
|
// await wrapper.find('input.test-validTo').setValue('2022-7-18')
|
||||||
|
// await wrapper.find('input.test-name').setValue('test name')
|
||||||
|
// await wrapper.find('input.test-memo').setValue('test memo')
|
||||||
|
// await wrapper.find('input.test-amount').setValue('100')
|
||||||
|
// await wrapper.find('form').trigger('submit')
|
||||||
|
// })
|
||||||
|
|
||||||
|
// it('calls the API', () => {
|
||||||
|
// expect(mockAPIcall).toHaveBeenCalledWith(
|
||||||
|
// expect.objectContaining({
|
||||||
|
// variables: {
|
||||||
|
// link: 'https://localhost/redeem/CL-1a2345678',
|
||||||
|
// },
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
|
||||||
|
// it('displays the new username', () => {
|
||||||
|
// expect(wrapper.find('div.display-username').text()).toEqual('@username')
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
})
|
||||||
|
})
|
||||||
218
admin/src/components/ContributionLinkForm.vue
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
<template>
|
||||||
|
<div class="contribution-link-form">
|
||||||
|
<div v-if="updateData" class="text-light bg-info p-3">
|
||||||
|
{{ updateData }}
|
||||||
|
</div>
|
||||||
|
<b-form class="m-5" @submit.prevent="onSubmit" ref="contributionLinkForm">
|
||||||
|
<!-- Date -->
|
||||||
|
<b-row>
|
||||||
|
<b-col>
|
||||||
|
<b-form-group :label="$t('contributionLink.validFrom')">
|
||||||
|
<b-form-datepicker
|
||||||
|
v-model="form.validFrom"
|
||||||
|
size="lg"
|
||||||
|
:min="min"
|
||||||
|
class="mb-4 test-validFrom"
|
||||||
|
reset-value=""
|
||||||
|
:label-no-date-selected="$t('contributionLink.noDateSelected')"
|
||||||
|
required
|
||||||
|
></b-form-datepicker>
|
||||||
|
</b-form-group>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<b-form-group :label="$t('contributionLink.validTo')">
|
||||||
|
<b-form-datepicker
|
||||||
|
v-model="form.validTo"
|
||||||
|
size="lg"
|
||||||
|
:min="form.validFrom ? form.validFrom : min"
|
||||||
|
class="mb-4 test-validTo"
|
||||||
|
reset-value=""
|
||||||
|
:label-no-date-selected="$t('contributionLink.noDateSelected')"
|
||||||
|
required
|
||||||
|
></b-form-datepicker>
|
||||||
|
</b-form-group>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
|
||||||
|
<!-- Name -->
|
||||||
|
<b-form-group :label="$t('contributionLink.name')">
|
||||||
|
<b-form-input
|
||||||
|
v-model="form.name"
|
||||||
|
size="lg"
|
||||||
|
type="text"
|
||||||
|
placeholder="Name"
|
||||||
|
required
|
||||||
|
maxlength="100"
|
||||||
|
class="test-name"
|
||||||
|
></b-form-input>
|
||||||
|
</b-form-group>
|
||||||
|
<!-- Desc -->
|
||||||
|
<b-form-group :label="$t('contributionLink.memo')">
|
||||||
|
<b-form-textarea
|
||||||
|
v-model="form.memo"
|
||||||
|
size="lg"
|
||||||
|
:placeholder="$t('contributionLink.memo')"
|
||||||
|
required
|
||||||
|
maxlength="255"
|
||||||
|
class="test-memo"
|
||||||
|
></b-form-textarea>
|
||||||
|
</b-form-group>
|
||||||
|
<!-- Amount -->
|
||||||
|
<b-form-group :label="$t('contributionLink.amount')">
|
||||||
|
<b-form-input
|
||||||
|
v-model="form.amount"
|
||||||
|
size="lg"
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
required
|
||||||
|
class="test-amount"
|
||||||
|
></b-form-input>
|
||||||
|
</b-form-group>
|
||||||
|
<b-collapse id="collapse-2">
|
||||||
|
<b-jumbotron>
|
||||||
|
<b-row class="mb-4">
|
||||||
|
<b-col>
|
||||||
|
<!-- Cycle -->
|
||||||
|
<label for="cycle">{{ $t('contributionLink.cycle') }}</label>
|
||||||
|
<b-form-select
|
||||||
|
v-model="form.cycle"
|
||||||
|
:options="cycle"
|
||||||
|
:disabled="disabled"
|
||||||
|
class="mb-3"
|
||||||
|
size="lg"
|
||||||
|
></b-form-select>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<!-- maxPerCycle -->
|
||||||
|
<label for="maxPerCycle">{{ $t('contributionLink.maxPerCycle') }}</label>
|
||||||
|
<b-form-select
|
||||||
|
v-model="form.maxPerCycle"
|
||||||
|
:options="maxPerCycle"
|
||||||
|
:disabled="disabled"
|
||||||
|
class="mb-3"
|
||||||
|
size="lg"
|
||||||
|
></b-form-select>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
|
||||||
|
<!-- Max amount -->
|
||||||
|
<b-form-group :label="$t('contributionLink.maximumAmount')">
|
||||||
|
<b-form-input
|
||||||
|
v-model="form.maxAmountPerMonth"
|
||||||
|
size="lg"
|
||||||
|
:disabled="disabled"
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
></b-form-input>
|
||||||
|
</b-form-group>
|
||||||
|
</b-jumbotron>
|
||||||
|
</b-collapse>
|
||||||
|
<div class="mt-6">
|
||||||
|
<b-button type="submit" variant="primary">{{ $t('contributionLink.create') }}</b-button>
|
||||||
|
<b-button type="reset" variant="danger" @click.prevent="onReset">
|
||||||
|
{{ $t('contributionLink.clear') }}
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</b-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { createContributionLink } from '@/graphql/createContributionLink.js'
|
||||||
|
export default {
|
||||||
|
name: 'ContributionLinkForm',
|
||||||
|
props: {
|
||||||
|
contributionLinkData: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: {
|
||||||
|
name: null,
|
||||||
|
memo: null,
|
||||||
|
amount: null,
|
||||||
|
validFrom: null,
|
||||||
|
validTo: null,
|
||||||
|
cycle: 'ONCE',
|
||||||
|
maxPerCycle: 1,
|
||||||
|
maxAmountPerMonth: '0',
|
||||||
|
},
|
||||||
|
min: new Date(),
|
||||||
|
cycle: [
|
||||||
|
{ value: 'ONCE', text: this.$t('contributionLink.options.cycle.once') },
|
||||||
|
{ value: 'hourly', text: this.$t('contributionLink.options.cycle.hourly') },
|
||||||
|
{ value: 'daily', text: this.$t('contributionLink.options.cycle.daily') },
|
||||||
|
{ value: 'weekly', text: this.$t('contributionLink.options.cycle.weekly') },
|
||||||
|
{ value: 'monthly', text: this.$t('contributionLink.options.cycle.monthly') },
|
||||||
|
{ value: 'yearly', text: this.$t('contributionLink.options.cycle.yearly') },
|
||||||
|
],
|
||||||
|
maxPerCycle: [
|
||||||
|
{ value: '1', text: '1 x' },
|
||||||
|
{ value: '2', text: '2 x' },
|
||||||
|
{ value: '3', text: '3 x' },
|
||||||
|
{ value: '4', text: '4 x' },
|
||||||
|
{ value: '5', text: '5 x' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSubmit() {
|
||||||
|
if (this.form.validFrom === null)
|
||||||
|
return this.toastError(this.$t('contributionLink.noStartDate'))
|
||||||
|
if (this.form.validTo === null) return this.toastError(this.$t('contributionLink.noEndDate'))
|
||||||
|
// alert(JSON.stringify(this.form))
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: createContributionLink,
|
||||||
|
variables: {
|
||||||
|
validFrom: this.form.validFrom,
|
||||||
|
validTo: this.form.validTo,
|
||||||
|
name: this.form.name,
|
||||||
|
amount: this.form.amount,
|
||||||
|
memo: this.form.memo,
|
||||||
|
cycle: this.form.cycle,
|
||||||
|
maxPerCycle: this.form.maxPerCycle,
|
||||||
|
maxAmountPerMonth: this.form.maxAmountPerMonth,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.link = result.data.createContributionLink.link
|
||||||
|
this.toastSuccess(this.link)
|
||||||
|
this.onReset()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.toastError(error.message)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onReset() {
|
||||||
|
this.$refs.contributionLinkForm.reset()
|
||||||
|
this.form.validFrom = null
|
||||||
|
this.form.validTo = null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
updateData() {
|
||||||
|
return this.contributionLinkData
|
||||||
|
},
|
||||||
|
disabled() {
|
||||||
|
if (this.form.cycle === 'ONCE') return true
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
contributionLinkData() {
|
||||||
|
this.form.name = this.contributionLinkData.name
|
||||||
|
this.form.memo = this.contributionLinkData.memo
|
||||||
|
this.form.amount = this.contributionLinkData.amount
|
||||||
|
this.form.validFrom = this.contributionLinkData.validFrom
|
||||||
|
this.form.validTo = this.contributionLinkData.validTo
|
||||||
|
this.form.cycle = this.contributionLinkData.cycle
|
||||||
|
this.form.maxPerCycle = this.contributionLinkData.maxPerCycle
|
||||||
|
this.form.maxAmountPerMonth = this.contributionLinkData.maxAmountPerMonth
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
147
admin/src/components/ContributionLinkList.spec.js
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import ContributionLinkList from './ContributionLinkList.vue'
|
||||||
|
import { toastSuccessSpy, toastErrorSpy } from '../../test/testSetup'
|
||||||
|
// import { deleteContributionLink } from '../graphql/deleteContributionLink'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const mockAPIcall = jest.fn()
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
$apollo: {
|
||||||
|
mutate: mockAPIcall,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsData = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Meditation',
|
||||||
|
memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l',
|
||||||
|
amount: '200',
|
||||||
|
validFrom: '2022-04-01',
|
||||||
|
validTo: '2022-08-01',
|
||||||
|
cycle: 'täglich',
|
||||||
|
maxPerCycle: '3',
|
||||||
|
maxAmountPerMonth: 0,
|
||||||
|
link: 'https://localhost/redeem/CL-1a2345678',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ContributionLinkList', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(ContributionLinkList, { localVue, mocks, propsData })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the Div Element ".contribution-link-list"', () => {
|
||||||
|
expect(wrapper.find('div.contribution-link-list').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders table with contribution link', () => {
|
||||||
|
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
|
||||||
|
'Meditation',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('edit contribution link', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper.vm.editContributionLink()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits editContributionLinkData', async () => {
|
||||||
|
expect(wrapper.vm.$emit('editContributionLinkData')).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('delete contribution link', () => {
|
||||||
|
let spy
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
wrapper.vm.deleteContributionLink()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with success', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||||
|
spy.mockImplementation(() => Promise.resolve('some value'))
|
||||||
|
mockAPIcall.mockResolvedValue()
|
||||||
|
await wrapper.find('.test-delete-link').trigger('click')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens the modal ', () => {
|
||||||
|
expect(spy).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.skip('calls the API', () => {
|
||||||
|
// expect(mockAPIcall).toBeCalledWith(
|
||||||
|
// expect.objectContaining({
|
||||||
|
// mutation: deleteContributionLink,
|
||||||
|
// variables: {
|
||||||
|
// id: 1,
|
||||||
|
// },
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toasts a success message', () => {
|
||||||
|
expect(toastSuccessSpy).toBeCalledWith('TODO: request message deleted ')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with error', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||||
|
spy.mockImplementation(() => Promise.resolve('some value'))
|
||||||
|
mockAPIcall.mockRejectedValue({ message: 'Something went wrong :(' })
|
||||||
|
await wrapper.find('.test-delete-link').trigger('click')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toasts an error message', () => {
|
||||||
|
expect(toastErrorSpy).toBeCalledWith('Something went wrong :(')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('cancel delete', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||||
|
spy.mockImplementation(() => Promise.resolve(false))
|
||||||
|
mockAPIcall.mockResolvedValue()
|
||||||
|
await wrapper.find('.test-delete-link').trigger('click')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not call the API', () => {
|
||||||
|
expect(mockAPIcall).not.toBeCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('onClick showButton', () => {
|
||||||
|
it('modelData contains contribution link', () => {
|
||||||
|
wrapper.find('button.test-show').trigger('click')
|
||||||
|
expect(wrapper.vm.modalData).toEqual({
|
||||||
|
amount: '200',
|
||||||
|
cycle: 'täglich',
|
||||||
|
id: 1,
|
||||||
|
link: 'https://localhost/redeem/CL-1a2345678',
|
||||||
|
maxAmountPerMonth: 0,
|
||||||
|
maxPerCycle: '3',
|
||||||
|
memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l',
|
||||||
|
name: 'Meditation',
|
||||||
|
validFrom: '2022-04-01',
|
||||||
|
validTo: '2022-08-01',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
106
admin/src/components/ContributionLinkList.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<div class="contribution-link-list">
|
||||||
|
<b-table striped hover :items="items" :fields="fields">
|
||||||
|
<template #cell(delete)>
|
||||||
|
<b-button
|
||||||
|
variant="danger"
|
||||||
|
size="md"
|
||||||
|
class="mr-2 test-delete-link"
|
||||||
|
@click="deleteContributionLink"
|
||||||
|
>
|
||||||
|
<b-icon icon="trash" variant="light"></b-icon>
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
<template #cell(edit)="data">
|
||||||
|
<b-button variant="success" size="md" class="mr-2" @click="editContributionLink(data.item)">
|
||||||
|
<b-icon icon="pencil" variant="light"></b-icon>
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
<template #cell(show)="data">
|
||||||
|
<b-button
|
||||||
|
variant="info"
|
||||||
|
size="md"
|
||||||
|
class="mr-2 test-show"
|
||||||
|
@click="showContributionLink(data.item)"
|
||||||
|
>
|
||||||
|
<b-icon icon="eye" variant="light"></b-icon>
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
</b-table>
|
||||||
|
|
||||||
|
<b-modal ref="my-modal" ok-only hide-header-close>
|
||||||
|
<b-card header-tag="header" footer-tag="footer">
|
||||||
|
<template #header>
|
||||||
|
<h6 class="mb-0">{{ modalData ? modalData.name : '' }}</h6>
|
||||||
|
</template>
|
||||||
|
<b-card-text>
|
||||||
|
{{ modalData }}
|
||||||
|
<figure-qr-code :link="modalData ? modalData.link : ''" />
|
||||||
|
</b-card-text>
|
||||||
|
<template #footer>
|
||||||
|
<em>{{ modalData ? modalData.link : '' }}</em>
|
||||||
|
</template>
|
||||||
|
</b-card>
|
||||||
|
</b-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { deleteContributionLink } from '@/graphql/deleteContributionLink.js'
|
||||||
|
import FigureQrCode from './FigureQrCode.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ContributionLinkList',
|
||||||
|
components: {
|
||||||
|
FigureQrCode,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
items: { type: Array, required: true },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
fields: [
|
||||||
|
'name',
|
||||||
|
'memo',
|
||||||
|
'amount',
|
||||||
|
{ key: 'cycle', label: this.$t('contributionLink.cycle') },
|
||||||
|
{ key: 'maxPerCycle', label: this.$t('contributionLink.maxPerCycle') },
|
||||||
|
{ key: 'validFrom', label: this.$t('contributionLink.validFrom') },
|
||||||
|
{ key: 'validTo', label: this.$t('contributionLink.validTo') },
|
||||||
|
'delete',
|
||||||
|
'edit',
|
||||||
|
'show',
|
||||||
|
],
|
||||||
|
modalData: null,
|
||||||
|
modalDataLink: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
deleteContributionLink() {
|
||||||
|
this.$bvModal.msgBoxConfirm(this.$t('contributionLink.deleteNow')).then(async (value) => {
|
||||||
|
if (value)
|
||||||
|
await this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: deleteContributionLink,
|
||||||
|
variables: {
|
||||||
|
id: this.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.toastSuccess('TODO: request message deleted ')
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.toastError(err.message)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
editContributionLink(row) {
|
||||||
|
this.$emit('editContributionLinkData', row)
|
||||||
|
},
|
||||||
|
|
||||||
|
showContributionLink(row) {
|
||||||
|
this.modalData = row
|
||||||
|
this.$refs['my-modal'].show()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -1,14 +1,14 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import CreationFormular from './CreationFormular.vue'
|
import CreationFormular from './CreationFormular.vue'
|
||||||
import { createPendingCreation } from '../graphql/createPendingCreation'
|
import { adminCreateContribution } from '../graphql/adminCreateContribution'
|
||||||
import { createPendingCreations } from '../graphql/createPendingCreations'
|
import { adminCreateContributions } from '../graphql/adminCreateContributions'
|
||||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
|
||||||
const apolloMutateMock = jest.fn().mockResolvedValue({
|
const apolloMutateMock = jest.fn().mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
createPendingCreation: [0, 0, 0],
|
adminCreateContribution: [0, 0, 0],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const stateCommitMock = jest.fn()
|
const stateCommitMock = jest.fn()
|
||||||
@ -110,7 +110,7 @@ describe('CreationFormular', () => {
|
|||||||
it('sends ... to apollo', () => {
|
it('sends ... to apollo', () => {
|
||||||
expect(apolloMutateMock).toBeCalledWith(
|
expect(apolloMutateMock).toBeCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
mutation: createPendingCreation,
|
mutation: adminCreateContribution,
|
||||||
variables: {
|
variables: {
|
||||||
email: 'benjamin@bluemchen.de',
|
email: 'benjamin@bluemchen.de',
|
||||||
creationDate: getCreationDate(2),
|
creationDate: getCreationDate(2),
|
||||||
@ -334,10 +334,10 @@ describe('CreationFormular', () => {
|
|||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
apolloMutateMock.mockResolvedValue({
|
apolloMutateMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
createPendingCreations: {
|
adminCreateContributions: {
|
||||||
success: true,
|
success: true,
|
||||||
successfulCreation: ['bob@baumeister.de', 'bibi@bloxberg.de'],
|
successfulContribution: ['bob@baumeister.de', 'bibi@bloxberg.de'],
|
||||||
failedCreation: [],
|
failedContribution: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -355,7 +355,7 @@ describe('CreationFormular', () => {
|
|||||||
it('calls the API', () => {
|
it('calls the API', () => {
|
||||||
expect(apolloMutateMock).toBeCalledWith(
|
expect(apolloMutateMock).toBeCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
mutation: createPendingCreations,
|
mutation: adminCreateContributions,
|
||||||
variables: {
|
variables: {
|
||||||
pendingCreations: [
|
pendingCreations: [
|
||||||
{
|
{
|
||||||
@ -390,10 +390,10 @@ describe('CreationFormular', () => {
|
|||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
apolloMutateMock.mockResolvedValue({
|
apolloMutateMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
createPendingCreations: {
|
adminCreateContributions: {
|
||||||
success: true,
|
success: true,
|
||||||
successfulCreation: [],
|
successfulContribution: [],
|
||||||
failedCreation: ['bob@baumeister.de', 'bibi@bloxberg.de'],
|
failedContribution: ['bob@baumeister.de', 'bibi@bloxberg.de'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -85,8 +85,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { createPendingCreation } from '../graphql/createPendingCreation'
|
import { adminCreateContribution } from '../graphql/adminCreateContribution'
|
||||||
import { createPendingCreations } from '../graphql/createPendingCreations'
|
import { adminCreateContributions } from '../graphql/adminCreateContributions'
|
||||||
import { creationMonths } from '../mixins/creationMonths'
|
import { creationMonths } from '../mixins/creationMonths'
|
||||||
export default {
|
export default {
|
||||||
name: 'CreationFormular',
|
name: 'CreationFormular',
|
||||||
@ -158,25 +158,25 @@ export default {
|
|||||||
})
|
})
|
||||||
this.$apollo
|
this.$apollo
|
||||||
.mutate({
|
.mutate({
|
||||||
mutation: createPendingCreations,
|
mutation: adminCreateContributions,
|
||||||
variables: {
|
variables: {
|
||||||
pendingCreations: submitObj,
|
pendingCreations: submitObj,
|
||||||
},
|
},
|
||||||
fetchPolicy: 'no-cache',
|
fetchPolicy: 'no-cache',
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
const failedCreations = []
|
const failedContributions = []
|
||||||
this.$store.commit(
|
this.$store.commit(
|
||||||
'openCreationsPlus',
|
'openCreationsPlus',
|
||||||
result.data.createPendingCreations.successfulCreation.length,
|
result.data.adminCreateContributions.successfulContribution.length,
|
||||||
)
|
)
|
||||||
if (result.data.createPendingCreations.failedCreation.length > 0) {
|
if (result.data.adminCreateContributions.failedContribution.length > 0) {
|
||||||
result.data.createPendingCreations.failedCreation.forEach((email) => {
|
result.data.adminCreateContributions.failedContribution.forEach((email) => {
|
||||||
failedCreations.push(email)
|
failedContributions.push(email)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.$emit('remove-all-bookmark')
|
this.$emit('remove-all-bookmark')
|
||||||
this.$emit('toast-failed-creations', failedCreations)
|
this.$emit('toast-failed-creations', failedContributions)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.toastError(error.message)
|
this.toastError(error.message)
|
||||||
@ -190,11 +190,11 @@ export default {
|
|||||||
}
|
}
|
||||||
this.$apollo
|
this.$apollo
|
||||||
.mutate({
|
.mutate({
|
||||||
mutation: createPendingCreation,
|
mutation: adminCreateContribution,
|
||||||
variables: submitObj,
|
variables: submitObj,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.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.$store.commit('openCreationsPlus', 1)
|
||||||
this.toastSuccess(
|
this.toastSuccess(
|
||||||
this.$t('creation_form.toasted', {
|
this.$t('creation_form.toasted', {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ const localVue = global.localVue
|
|||||||
|
|
||||||
const apolloMutateMock = jest.fn().mockResolvedValue({
|
const apolloMutateMock = jest.fn().mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
updatePendingCreation: {
|
adminUpdateContribution: {
|
||||||
creation: [0, 0, 0],
|
creation: [0, 0, 0],
|
||||||
amount: 500,
|
amount: 500,
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
|
|||||||
@ -73,7 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { updatePendingCreation } from '../graphql/updatePendingCreation'
|
import { adminUpdateContribution } from '../graphql/adminUpdateContribution'
|
||||||
import { creationMonths } from '../mixins/creationMonths'
|
import { creationMonths } from '../mixins/creationMonths'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -103,7 +103,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
text: !this.creationUserData.memo ? '' : this.creationUserData.memo,
|
text: !this.creationUserData.memo ? '' : this.creationUserData.memo,
|
||||||
value: !this.creationUserData.amount ? 0 : this.creationUserData.amount,
|
value: !this.creationUserData.amount ? 0 : Number(this.creationUserData.amount),
|
||||||
rangeMin: 0,
|
rangeMin: 0,
|
||||||
rangeMax: 1000,
|
rangeMax: 1000,
|
||||||
selected: '',
|
selected: '',
|
||||||
@ -113,7 +113,7 @@ export default {
|
|||||||
submitCreation() {
|
submitCreation() {
|
||||||
this.$apollo
|
this.$apollo
|
||||||
.mutate({
|
.mutate({
|
||||||
mutation: updatePendingCreation,
|
mutation: adminUpdateContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: this.item.id,
|
id: this.item.id,
|
||||||
email: this.item.email,
|
email: this.item.email,
|
||||||
@ -123,11 +123,11 @@ export default {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.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', {
|
this.$emit('update-creation-data', {
|
||||||
amount: Number(result.data.updatePendingCreation.amount),
|
amount: Number(result.data.adminUpdateContribution.amount),
|
||||||
date: result.data.updatePendingCreation.date,
|
date: result.data.adminUpdateContribution.date,
|
||||||
memo: result.data.updatePendingCreation.memo,
|
memo: result.data.adminUpdateContribution.memo,
|
||||||
row: this.row,
|
row: this.row,
|
||||||
})
|
})
|
||||||
this.toastSuccess(
|
this.toastSuccess(
|
||||||
@ -155,7 +155,7 @@ export default {
|
|||||||
const month = this.$d(new Date(this.creationUserData.date), 'month')
|
const month = this.$d(new Date(this.creationUserData.date), 'month')
|
||||||
const index = this.radioOptions.findIndex((obj) => obj.item.short === month)
|
const index = this.radioOptions.findIndex((obj) => obj.item.short === month)
|
||||||
this.selected = this.radioOptions[index].item
|
this.selected = this.radioOptions[index].item
|
||||||
this.rangeMax = this.creation[index] + this.creationUserData.amount
|
this.rangeMax = Number(this.creation[index]) + Number(this.creationUserData.amount)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
30
admin/src/components/FigureQrCode.spec.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import FigureQrCode from './FigureQrCode.vue'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const propsData = {
|
||||||
|
link: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('FigureQrCode', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(FigureQrCode, { localVue, propsData })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the Div Element ".figure-qr-code"', () => {
|
||||||
|
expect(wrapper.find('div.figure-qr-code').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the QRCanvas Element ".canvas"', () => {
|
||||||
|
expect(wrapper.find('.canvas').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
56
admin/src/components/FigureQrCode.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<div class="figure-qr-code">
|
||||||
|
<div class="qrbox">
|
||||||
|
<q-r-canvas :options="options" class="canvas" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { QRCanvas } from 'qrcanvas-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FigureQrCode',
|
||||||
|
components: {
|
||||||
|
QRCanvas,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
link: { type: String, required: true },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
options: {
|
||||||
|
cellSize: 8,
|
||||||
|
correctLevel: 'H',
|
||||||
|
data: this.link,
|
||||||
|
logo: {
|
||||||
|
image: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
const image = new Image()
|
||||||
|
image.src = 'img/gdd-coin.png'
|
||||||
|
image.onload = () => {
|
||||||
|
this.options = {
|
||||||
|
...this.options,
|
||||||
|
logo: {
|
||||||
|
image,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.qrbox {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: rgb(255, 255, 255);
|
||||||
|
}
|
||||||
|
.canvas {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 300px;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: rgb(255, 255, 255);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -69,6 +69,7 @@ const propsData = {
|
|||||||
{ key: 'edit_creation', label: 'edit' },
|
{ key: 'edit_creation', label: 'edit' },
|
||||||
{ key: 'confirm', label: 'save' },
|
{ key: 'confirm', label: 'save' },
|
||||||
],
|
],
|
||||||
|
toggleDetails: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const mocks = {
|
const mocks = {
|
||||||
@ -101,7 +102,7 @@ describe('OpenCreationsTable', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('has a DIV element with the class .open-creations-table', () => {
|
it('has a DIV element with the class .open-creations-table', () => {
|
||||||
expect(wrapper.find('div.open-creations-table').exists()).toBeTruthy()
|
expect(wrapper.find('div.open-creations-table').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has a table with three rows', () => {
|
it('has a table with three rows', () => {
|
||||||
@ -109,7 +110,7 @@ describe('OpenCreationsTable', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('find first button.bi-pencil-square for open EditCreationFormular ', () => {
|
it('find first button.bi-pencil-square for open EditCreationFormular ', () => {
|
||||||
expect(wrapper.findAll('tr').at(1).find('.bi-pencil-square').exists()).toBeTruthy()
|
expect(wrapper.findAll('tr').at(1).find('.bi-pencil-square').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('show edit details', () => {
|
describe('show edit details', () => {
|
||||||
@ -122,7 +123,15 @@ describe('OpenCreationsTable', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.skip('renders the component component-edit-creation-formular', () => {
|
it.skip('renders the component component-edit-creation-formular', () => {
|
||||||
expect(wrapper.find('div.component-edit-creation-formular').exists()).toBeTruthy()
|
expect(wrapper.find('div.component-edit-creation-formular').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('call updateUserData', () => {
|
||||||
|
it('user creations has updated data', async () => {
|
||||||
|
wrapper.vm.updateUserData(propsData.items[0], [444, 555, 666])
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.items[0].creation).toEqual([444, 555, 666])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -70,12 +70,23 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
creationUserData: {
|
||||||
|
amount: null,
|
||||||
|
date: null,
|
||||||
|
memo: null,
|
||||||
|
moderator: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateCreationData(data) {
|
updateCreationData(data) {
|
||||||
this.creationUserData.amount = data.amount
|
this.creationUserData = data
|
||||||
this.creationUserData.date = data.date
|
// this.creationUserData.amount = data.amount
|
||||||
this.creationUserData.memo = data.memo
|
// this.creationUserData.date = data.date
|
||||||
this.creationUserData.moderator = data.moderator
|
// this.creationUserData.memo = data.memo
|
||||||
|
// this.creationUserData.moderator = data.moderator
|
||||||
data.row.toggleDetails()
|
data.row.toggleDetails()
|
||||||
},
|
},
|
||||||
updateUserData(rowItem, newCreation) {
|
updateUserData(rowItem, newCreation) {
|
||||||
|
|||||||
12
admin/src/graphql/adminCreateContribution.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const adminCreateContribution = gql`
|
||||||
|
mutation ($email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||||
|
adminCreateContribution(
|
||||||
|
email: $email
|
||||||
|
amount: $amount
|
||||||
|
memo: $memo
|
||||||
|
creationDate: $creationDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`
|
||||||
11
admin/src/graphql/adminCreateContributions.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const adminCreateContributions = gql`
|
||||||
|
mutation ($pendingCreations: [AdminCreateContributionArgs!]!) {
|
||||||
|
adminCreateContributions(pendingCreations: $pendingCreations) {
|
||||||
|
success
|
||||||
|
successfulContribution
|
||||||
|
failedContribution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
7
admin/src/graphql/adminDeleteContribution.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const adminDeleteContribution = gql`
|
||||||
|
mutation ($id: Int!) {
|
||||||
|
adminDeleteContribution(id: $id)
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
export const updatePendingCreation = gql`
|
export const adminUpdateContribution = gql`
|
||||||
mutation ($id: Int!, $email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
mutation ($id: Int!, $email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||||
updatePendingCreation(
|
adminUpdateContribution(
|
||||||
id: $id
|
id: $id
|
||||||
email: $email
|
email: $email
|
||||||
amount: $amount
|
amount: $amount
|
||||||
7
admin/src/graphql/confirmContribution.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const confirmContribution = gql`
|
||||||
|
mutation ($id: Int!) {
|
||||||
|
confirmContribution(id: $id)
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import gql from 'graphql-tag'
|
|
||||||
|
|
||||||
export const confirmPendingCreation = gql`
|
|
||||||
mutation ($id: Int!) {
|
|
||||||
confirmPendingCreation(id: $id)
|
|
||||||
}
|
|
||||||
`
|
|
||||||
27
admin/src/graphql/createContributionLink.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const createContributionLink = gql`
|
||||||
|
mutation (
|
||||||
|
$validFrom: String!
|
||||||
|
$validTo: String!
|
||||||
|
$name: String!
|
||||||
|
$amount: Decimal!
|
||||||
|
$memo: String!
|
||||||
|
$cycle: String!
|
||||||
|
$maxPerCycle: Int! = 1
|
||||||
|
$maxAmountPerMonth: Decimal
|
||||||
|
) {
|
||||||
|
createContributionLink(
|
||||||
|
validFrom: $validFrom
|
||||||
|
validTo: $validTo
|
||||||
|
name: $name
|
||||||
|
amount: $amount
|
||||||
|
memo: $memo
|
||||||
|
cycle: $cycle
|
||||||
|
maxPerCycle: $maxPerCycle
|
||||||
|
maxAmountPerMonth: $maxAmountPerMonth
|
||||||
|
) {
|
||||||
|
link
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import gql from 'graphql-tag'
|
|
||||||
|
|
||||||
export const createPendingCreation = gql`
|
|
||||||
mutation ($email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
|
||||||
createPendingCreation(email: $email, amount: $amount, memo: $memo, creationDate: $creationDate)
|
|
||||||
}
|
|
||||||
`
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import gql from 'graphql-tag'
|
|
||||||
|
|
||||||
export const createPendingCreations = gql`
|
|
||||||
mutation ($pendingCreations: [CreatePendingCreationArgs!]!) {
|
|
||||||
createPendingCreations(pendingCreations: $pendingCreations) {
|
|
||||||
success
|
|
||||||
successfulCreation
|
|
||||||
failedCreation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
7
admin/src/graphql/deleteContributionLink.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const deleteContributionLink = gql`
|
||||||
|
mutation ($id: Int!) {
|
||||||
|
deleteContributionLink(id: $id)
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import gql from 'graphql-tag'
|
|
||||||
|
|
||||||
export const deletePendingCreation = gql`
|
|
||||||
mutation ($id: Int!) {
|
|
||||||
deletePendingCreation(id: $id)
|
|
||||||
}
|
|
||||||
`
|
|
||||||
23
admin/src/graphql/listContributionLinks.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const listContributionLinks = gql`
|
||||||
|
query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
|
||||||
|
listContributionLinks(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
|
||||||
|
links {
|
||||||
|
id
|
||||||
|
amount
|
||||||
|
name
|
||||||
|
memo
|
||||||
|
code
|
||||||
|
link
|
||||||
|
createdAt
|
||||||
|
validFrom
|
||||||
|
validTo
|
||||||
|
maxAmountPerMonth
|
||||||
|
cycle
|
||||||
|
maxPerCycle
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
export const getPendingCreations = gql`
|
export const listUnconfirmedContributions = gql`
|
||||||
query {
|
query {
|
||||||
getPendingCreations {
|
listUnconfirmedContributions {
|
||||||
id
|
id
|
||||||
firstName
|
firstName
|
||||||
lastName
|
lastName
|
||||||
18
admin/src/graphql/showContributionLink.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const showContributionLink = gql`
|
||||||
|
query ($id: Int!) {
|
||||||
|
showContributionLink {
|
||||||
|
id
|
||||||
|
validFrom
|
||||||
|
validTo
|
||||||
|
name
|
||||||
|
memo
|
||||||
|
amount
|
||||||
|
cycle
|
||||||
|
maxPerCycle
|
||||||
|
maxAmountPerMonth
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -1,6 +1,35 @@
|
|||||||
{
|
{
|
||||||
"all_emails": "Alle Nutzer",
|
"all_emails": "Alle Nutzer",
|
||||||
"back": "zurück",
|
"back": "zurück",
|
||||||
|
"contributionLink": {
|
||||||
|
"amount": "Betrag",
|
||||||
|
"clear": "Löschen",
|
||||||
|
"contributionLinks": "Beitragslinks",
|
||||||
|
"create": "Anlegen",
|
||||||
|
"cycle": "Zyklus",
|
||||||
|
"deleteNow": "Automatische Creations wirklich löschen?",
|
||||||
|
"maximumAmount": "maximaler Betrag",
|
||||||
|
"maxPerCycle": "Wiederholungen",
|
||||||
|
"memo": "Nachricht",
|
||||||
|
"name": "Name",
|
||||||
|
"newContributionLink": "Neuer Beitragslink",
|
||||||
|
"noContributionLinks": "Es sind keine Beitragslinks angelegt.",
|
||||||
|
"noDateSelected": "Kein Datum ausgewählt",
|
||||||
|
"noEndDate": "Kein Enddatum gewählt.",
|
||||||
|
"noStartDate": "Kein Startdatum gewählt.",
|
||||||
|
"options": {
|
||||||
|
"cycle": {
|
||||||
|
"daily": "täglich",
|
||||||
|
"hourly": "stündlich",
|
||||||
|
"monthly": "monatlich",
|
||||||
|
"once": "einmalig",
|
||||||
|
"weekly": "wöchentlich",
|
||||||
|
"yearly": "jährlich"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validFrom": "Startdatum",
|
||||||
|
"validTo": "Enddatum"
|
||||||
|
},
|
||||||
"creation": "Schöpfung",
|
"creation": "Schöpfung",
|
||||||
"creationList": "Schöpfungsliste",
|
"creationList": "Schöpfungsliste",
|
||||||
"creation_form": {
|
"creation_form": {
|
||||||
@ -44,7 +73,8 @@
|
|||||||
"lastname": "Nachname",
|
"lastname": "Nachname",
|
||||||
"math": {
|
"math": {
|
||||||
"exclaim": "!",
|
"exclaim": "!",
|
||||||
"pipe": "|"
|
"pipe": "|",
|
||||||
|
"plus": "+"
|
||||||
},
|
},
|
||||||
"moderator": "Moderator",
|
"moderator": "Moderator",
|
||||||
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
|
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
|
||||||
|
|||||||
@ -1,6 +1,35 @@
|
|||||||
{
|
{
|
||||||
"all_emails": "All users",
|
"all_emails": "All users",
|
||||||
"back": "back",
|
"back": "back",
|
||||||
|
"contributionLink": {
|
||||||
|
"amount": "Amount",
|
||||||
|
"clear": "Clear",
|
||||||
|
"contributionLinks": "Contribution Links",
|
||||||
|
"create": "Create",
|
||||||
|
"cycle": "Cycle",
|
||||||
|
"deleteNow": "Do you really delete automatic creations?",
|
||||||
|
"maximumAmount": "Maximum amount",
|
||||||
|
"maxPerCycle": "Repetition",
|
||||||
|
"memo": "Memo",
|
||||||
|
"name": "Name",
|
||||||
|
"newContributionLink": "New contribution link",
|
||||||
|
"noContributionLinks": "No contribution link has been created.",
|
||||||
|
"noDateSelected": "No date selected",
|
||||||
|
"noEndDate": "No end-date",
|
||||||
|
"noStartDate": "No start-date",
|
||||||
|
"options": {
|
||||||
|
"cycle": {
|
||||||
|
"daily": "daily",
|
||||||
|
"hourly": "hourly",
|
||||||
|
"monthly": "monthly",
|
||||||
|
"once": "once",
|
||||||
|
"weekly": "weekly",
|
||||||
|
"yearly": "yearly"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validFrom": "Start-date",
|
||||||
|
"validTo": "End-Date"
|
||||||
|
},
|
||||||
"creation": "Creation",
|
"creation": "Creation",
|
||||||
"creationList": "Creation list",
|
"creationList": "Creation list",
|
||||||
"creation_form": {
|
"creation_form": {
|
||||||
@ -44,7 +73,8 @@
|
|||||||
"lastname": "Lastname",
|
"lastname": "Lastname",
|
||||||
"math": {
|
"math": {
|
||||||
"exclaim": "!",
|
"exclaim": "!",
|
||||||
"pipe": "|"
|
"pipe": "|",
|
||||||
|
"plus": "+"
|
||||||
},
|
},
|
||||||
"moderator": "Moderator",
|
"moderator": "Moderator",
|
||||||
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
|
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import CreationConfirm from './CreationConfirm.vue'
|
import CreationConfirm from './CreationConfirm.vue'
|
||||||
import { deletePendingCreation } from '../graphql/deletePendingCreation'
|
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
|
||||||
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
|
import { confirmContribution } from '../graphql/confirmContribution'
|
||||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
@ -9,7 +9,7 @@ const localVue = global.localVue
|
|||||||
const storeCommitMock = jest.fn()
|
const storeCommitMock = jest.fn()
|
||||||
const apolloQueryMock = jest.fn().mockResolvedValue({
|
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
getPendingCreations: [
|
listUnconfirmedContributions: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
firstName: 'Bibi',
|
firstName: 'Bibi',
|
||||||
@ -84,9 +84,9 @@ describe('CreationConfirm', () => {
|
|||||||
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
|
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({
|
expect(apolloMutateMock).toBeCalledWith({
|
||||||
mutation: deletePendingCreation,
|
mutation: adminDeleteContribution,
|
||||||
variables: { id: 1 },
|
variables: { id: 1 },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -141,9 +141,9 @@ describe('CreationConfirm', () => {
|
|||||||
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
|
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls the confirmPendingCreation mutation', () => {
|
it('calls the confirmContribution mutation', () => {
|
||||||
expect(apolloMutateMock).toBeCalledWith({
|
expect(apolloMutateMock).toBeCalledWith({
|
||||||
mutation: confirmPendingCreation,
|
mutation: confirmContribution,
|
||||||
variables: { id: 2 },
|
variables: { id: 2 },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -15,9 +15,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import Overlay from '../components/Overlay.vue'
|
import Overlay from '../components/Overlay.vue'
|
||||||
import OpenCreationsTable from '../components/Tables/OpenCreationsTable.vue'
|
import OpenCreationsTable from '../components/Tables/OpenCreationsTable.vue'
|
||||||
import { getPendingCreations } from '../graphql/getPendingCreations'
|
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions'
|
||||||
import { deletePendingCreation } from '../graphql/deletePendingCreation'
|
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
|
||||||
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
|
import { confirmContribution } from '../graphql/confirmContribution'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CreationConfirm',
|
name: 'CreationConfirm',
|
||||||
@ -36,7 +36,7 @@ export default {
|
|||||||
removeCreation(item) {
|
removeCreation(item) {
|
||||||
this.$apollo
|
this.$apollo
|
||||||
.mutate({
|
.mutate({
|
||||||
mutation: deletePendingCreation,
|
mutation: adminDeleteContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
},
|
},
|
||||||
@ -52,7 +52,7 @@ export default {
|
|||||||
confirmCreation() {
|
confirmCreation() {
|
||||||
this.$apollo
|
this.$apollo
|
||||||
.mutate({
|
.mutate({
|
||||||
mutation: confirmPendingCreation,
|
mutation: confirmContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: this.item.id,
|
id: this.item.id,
|
||||||
},
|
},
|
||||||
@ -70,13 +70,13 @@ export default {
|
|||||||
getPendingCreations() {
|
getPendingCreations() {
|
||||||
this.$apollo
|
this.$apollo
|
||||||
.query({
|
.query({
|
||||||
query: getPendingCreations,
|
query: listUnconfirmedContributions,
|
||||||
fetchPolicy: 'network-only',
|
fetchPolicy: 'network-only',
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.$store.commit('resetOpenCreations')
|
this.$store.commit('resetOpenCreations')
|
||||||
this.pendingCreations = result.data.getPendingCreations
|
this.pendingCreations = result.data.listUnconfirmedContributions
|
||||||
this.$store.commit('setOpenCreations', result.data.getPendingCreations.length)
|
this.$store.commit('setOpenCreations', result.data.listUnconfirmedContributions.length)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.toastError(error.message)
|
this.toastError(error.message)
|
||||||
|
|||||||
@ -5,7 +5,7 @@ const localVue = global.localVue
|
|||||||
|
|
||||||
const apolloQueryMock = jest.fn().mockResolvedValue({
|
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
getPendingCreations: [
|
listUnconfirmedContributions: [
|
||||||
{
|
{
|
||||||
pending: true,
|
pending: true,
|
||||||
},
|
},
|
||||||
@ -46,7 +46,7 @@ describe('Overview', () => {
|
|||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls getPendingCreations', () => {
|
it('calls listUnconfirmedContributions', () => {
|
||||||
expect(apolloQueryMock).toBeCalled()
|
expect(apolloQueryMock).toBeCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -28,27 +28,54 @@
|
|||||||
</b-link>
|
</b-link>
|
||||||
</b-card-text>
|
</b-card-text>
|
||||||
</b-card>
|
</b-card>
|
||||||
|
<contribution-link :items="items" :count="count" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { getPendingCreations } from '../graphql/getPendingCreations'
|
import { listContributionLinks } from '@/graphql/listContributionLinks.js'
|
||||||
|
import ContributionLink from '../components/ContributionLink.vue'
|
||||||
|
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'overview',
|
name: 'overview',
|
||||||
|
components: {
|
||||||
|
ContributionLink,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
count: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async getPendingCreations() {
|
async getPendingCreations() {
|
||||||
this.$apollo
|
this.$apollo
|
||||||
.query({
|
.query({
|
||||||
query: getPendingCreations,
|
query: listUnconfirmedContributions,
|
||||||
fetchPolicy: 'network-only',
|
fetchPolicy: 'network-only',
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.$store.commit('setOpenCreations', result.data.getPendingCreations.length)
|
this.$store.commit('setOpenCreations', result.data.listUnconfirmedContributions.length)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async getContributionLinks() {
|
||||||
|
this.$apollo
|
||||||
|
.query({
|
||||||
|
query: listContributionLinks,
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.count = result.data.listContributionLinks.count
|
||||||
|
this.items = result.data.listContributionLinks.links
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.toastError('listContributionLinks has no result, use default data')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getPendingCreations()
|
this.getPendingCreations()
|
||||||
|
this.getContributionLinks()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -932,6 +932,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.11.2", "@babel/runtime@^7.16.0":
|
||||||
|
version "7.18.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4"
|
||||||
|
integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/runtime@^7.14.0":
|
"@babel/runtime@^7.14.0":
|
||||||
version "7.17.7"
|
version "7.17.7"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.7.tgz#a5f3328dc41ff39d803f311cfe17703418cf9825"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.7.tgz#a5f3328dc41ff39d803f311cfe17703418cf9825"
|
||||||
@ -4082,9 +4089,9 @@ caniuse-api@^3.0.0:
|
|||||||
lodash.uniq "^4.5.0"
|
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:
|
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001271:
|
||||||
version "1.0.30001271"
|
version "1.0.30001354"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz#0dda0c9bcae2cf5407cd34cac304186616cc83e8"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001354.tgz"
|
||||||
integrity sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA==
|
integrity sha512-mImKeCkyGDAHNywYFA4bqnLAzTUvVkqPvhY4DV47X+Gl2c5Z8c3KNETnXp14GQt11LvxE8AwjzGxJ+rsikiOzg==
|
||||||
|
|
||||||
capture-exit@^2.0.0:
|
capture-exit@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@ -4397,7 +4404,7 @@ color-name@1.1.3:
|
|||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||||
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||||
|
|
||||||
color-name@^1.0.0, color-name@~1.1.4:
|
color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4:
|
||||||
version "1.1.4"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||||
@ -4845,6 +4852,11 @@ cssesc@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||||
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
|
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
|
||||||
|
|
||||||
|
cssfontparser@^1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3"
|
||||||
|
integrity sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==
|
||||||
|
|
||||||
cssnano-preset-default@^4.0.0, cssnano-preset-default@^4.0.8:
|
cssnano-preset-default@^4.0.0, cssnano-preset-default@^4.0.8:
|
||||||
version "4.0.8"
|
version "4.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz#920622b1fc1e95a34e8838203f1397a504f2d3ff"
|
resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz#920622b1fc1e95a34e8838203f1397a504f2d3ff"
|
||||||
@ -7821,6 +7833,14 @@ javascript-stringify@^2.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz#27c76539be14d8bd128219a2d731b09337904e79"
|
resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz#27c76539be14d8bd128219a2d731b09337904e79"
|
||||||
integrity sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==
|
integrity sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==
|
||||||
|
|
||||||
|
jest-canvas-mock@^2.3.1:
|
||||||
|
version "2.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.4.0.tgz#947b71442d7719f8e055decaecdb334809465341"
|
||||||
|
integrity sha512-mmMpZzpmLzn5vepIaHk5HoH3Ka4WykbSoLuG/EKoJd0x0ID/t+INo1l8ByfcUJuDM+RIsL4QDg/gDnBbrj2/IQ==
|
||||||
|
dependencies:
|
||||||
|
cssfontparser "^1.2.1"
|
||||||
|
moo-color "^1.0.2"
|
||||||
|
|
||||||
jest-changed-files@^24.9.0:
|
jest-changed-files@^24.9.0:
|
||||||
version "24.9.0"
|
version "24.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039"
|
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039"
|
||||||
@ -9478,6 +9498,13 @@ mkdirp@0.x, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.5"
|
minimist "^1.2.5"
|
||||||
|
|
||||||
|
moo-color@^1.0.2:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.3.tgz#d56435f8359c8284d83ac58016df7427febece74"
|
||||||
|
integrity sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==
|
||||||
|
dependencies:
|
||||||
|
color-name "^1.1.4"
|
||||||
|
|
||||||
move-concurrently@^1.0.1:
|
move-concurrently@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
||||||
@ -10959,6 +10986,27 @@ q@^1.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||||
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
||||||
|
|
||||||
|
qrcanvas-vue@2.1.1:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/qrcanvas-vue/-/qrcanvas-vue-2.1.1.tgz#27b449f99eaf46f324b300215469bfdf8ef77d88"
|
||||||
|
integrity sha512-86NMjOJ5XJGrrqrD2t+zmZxZKNuW1Is7o88UOiM8qFxDBjuTyfq9VJE9/2rN5XxThsjBuY4bRrQqL9blVwnI9w==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.16.0"
|
||||||
|
qrcanvas "^3.1.2"
|
||||||
|
|
||||||
|
qrcanvas@^3.1.2:
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/qrcanvas/-/qrcanvas-3.1.2.tgz#81a25e91b2c27e9ace91da95591cbfb100d68702"
|
||||||
|
integrity sha512-lNcAyCHN0Eno/mJ5eBc7lHV/5ejAJxII0UELthG3bNnlLR+u8hCc7CR+hXBawbYUf96kNIosXfG2cJzx92ZWKg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.11.2"
|
||||||
|
qrcode-generator "^1.4.4"
|
||||||
|
|
||||||
|
qrcode-generator@^1.4.4:
|
||||||
|
version "1.4.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.4.4.tgz#63f771224854759329a99048806a53ed278740e7"
|
||||||
|
integrity sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==
|
||||||
|
|
||||||
qs@6.7.0:
|
qs@6.7.0:
|
||||||
version "6.7.0"
|
version "6.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gradido-backend",
|
"name": "gradido-backend",
|
||||||
"version": "1.9.0",
|
"version": "1.10.0",
|
||||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": "https://github.com/gradido/gradido/backend",
|
"repository": "https://github.com/gradido/gradido/backend",
|
||||||
|
|||||||
@ -27,11 +27,12 @@ export enum RIGHTS {
|
|||||||
GDT_BALANCE = 'GDT_BALANCE',
|
GDT_BALANCE = 'GDT_BALANCE',
|
||||||
// Admin
|
// Admin
|
||||||
SEARCH_USERS = 'SEARCH_USERS',
|
SEARCH_USERS = 'SEARCH_USERS',
|
||||||
CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION',
|
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
|
||||||
UPDATE_PENDING_CREATION = 'UPDATE_PENDING_CREATION',
|
ADMIN_CREATE_CONTRIBUTIONS = 'ADMIN_CREATE_CONTRIBUTIONS',
|
||||||
SEARCH_PENDING_CREATION = 'SEARCH_PENDING_CREATION',
|
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
|
||||||
DELETE_PENDING_CREATION = 'DELETE_PENDING_CREATION',
|
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
|
||||||
CONFIRM_PENDING_CREATION = 'CONFIRM_PENDING_CREATION',
|
LIST_UNCONFIRMED_CONTRIBUTIONS = 'LIST_UNCONFIRMED_CONTRIBUTIONS',
|
||||||
|
CONFIRM_CONTRIBUTION = 'CONFIRM_CONTRIBUTION',
|
||||||
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
|
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
|
||||||
DELETE_USER = 'DELETE_USER',
|
DELETE_USER = 'DELETE_USER',
|
||||||
UNDELETE_USER = 'UNDELETE_USER',
|
UNDELETE_USER = 'UNDELETE_USER',
|
||||||
|
|||||||
@ -10,7 +10,7 @@ Decimal.set({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const constants = {
|
const constants = {
|
||||||
DB_VERSION: '0038-add_contribution_links_table',
|
DB_VERSION: '0040-add_contribution_link_id_to_user',
|
||||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||||
LOG4JS_CONFIG: 'log4js-config.json',
|
LOG4JS_CONFIG: 'log4js-config.json',
|
||||||
// default log level on production should be info
|
// default log level on production should be info
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import Decimal from 'decimal.js-light'
|
|||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
export default class CreatePendingCreationArgs {
|
export default class AdminCreateContributionArgs {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
email: string
|
email: string
|
||||||
|
|
||||||
@ -2,7 +2,7 @@ import { ArgsType, Field, Int } from 'type-graphql'
|
|||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
export default class UpdatePendingCreationArgs {
|
export default class AdminUpdateContributionArgs {
|
||||||
@Field(() => Int)
|
@Field(() => Int)
|
||||||
id: number
|
id: number
|
||||||
|
|
||||||
@ -1,19 +1,19 @@
|
|||||||
import { ObjectType, Field } from 'type-graphql'
|
import { ObjectType, Field } from 'type-graphql'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class CreatePendingCreations {
|
export class AdminCreateContributions {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.success = false
|
this.success = false
|
||||||
this.successfulCreation = []
|
this.successfulContribution = []
|
||||||
this.failedCreation = []
|
this.failedContribution = []
|
||||||
}
|
}
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
success: boolean
|
success: boolean
|
||||||
|
|
||||||
@Field(() => [String])
|
@Field(() => [String])
|
||||||
successfulCreation: string[]
|
successfulContribution: string[]
|
||||||
|
|
||||||
@Field(() => [String])
|
@Field(() => [String])
|
||||||
failedCreation: string[]
|
failedContribution: string[]
|
||||||
}
|
}
|
||||||
@ -2,7 +2,7 @@ import { ObjectType, Field } from 'type-graphql'
|
|||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class UpdatePendingCreation {
|
export class AdminUpdateContribution {
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
date: Date
|
date: Date
|
||||||
|
|
||||||
@ -2,7 +2,7 @@ import { ObjectType, Field, Int } from 'type-graphql'
|
|||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class PendingCreation {
|
export class UnconfirmedContribution {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
firstName: string
|
firstName: string
|
||||||
|
|
||||||
@ -15,17 +15,17 @@ import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
|
|||||||
import {
|
import {
|
||||||
deleteUser,
|
deleteUser,
|
||||||
unDeleteUser,
|
unDeleteUser,
|
||||||
createPendingCreation,
|
adminCreateContribution,
|
||||||
createPendingCreations,
|
adminCreateContributions,
|
||||||
updatePendingCreation,
|
adminUpdateContribution,
|
||||||
deletePendingCreation,
|
adminDeleteContribution,
|
||||||
confirmPendingCreation,
|
confirmContribution,
|
||||||
createContributionLink,
|
createContributionLink,
|
||||||
deleteContributionLink,
|
deleteContributionLink,
|
||||||
updateContributionLink,
|
updateContributionLink,
|
||||||
} from '@/seeds/graphql/mutations'
|
} from '@/seeds/graphql/mutations'
|
||||||
import {
|
import {
|
||||||
getPendingCreations,
|
listUnconfirmedContributions,
|
||||||
login,
|
login,
|
||||||
searchUsers,
|
searchUsers,
|
||||||
listTransactionLinksAdmin,
|
listTransactionLinksAdmin,
|
||||||
@ -36,7 +36,7 @@ import { User } from '@entity/User'
|
|||||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
|
import { Contribution } from '@entity/Contribution'
|
||||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ afterAll(async () => {
|
|||||||
|
|
||||||
let admin: User
|
let admin: User
|
||||||
let user: User
|
let user: User
|
||||||
let creation: AdminPendingCreation | void
|
let creation: Contribution | void
|
||||||
|
|
||||||
describe('AdminResolver', () => {
|
describe('AdminResolver', () => {
|
||||||
describe('delete user', () => {
|
describe('delete user', () => {
|
||||||
@ -502,9 +502,9 @@ describe('AdminResolver', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
describe('createPendingCreation', () => {
|
describe('adminCreateContribution', () => {
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
await expect(mutate({ mutation: createPendingCreation, variables })).resolves.toEqual(
|
await expect(mutate({ mutation: adminCreateContribution, variables })).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('401 Unauthorized')],
|
errors: [new GraphQLError('401 Unauthorized')],
|
||||||
}),
|
}),
|
||||||
@ -512,11 +512,11 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('createPendingCreations', () => {
|
describe('adminCreateContributions', () => {
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createPendingCreations,
|
mutation: adminCreateContributions,
|
||||||
variables: { pendingCreations: [variables] },
|
variables: { pendingCreations: [variables] },
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
@ -527,11 +527,11 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('updatePendingCreation', () => {
|
describe('adminUpdateContribution', () => {
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: updatePendingCreation,
|
mutation: adminUpdateContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: 1,
|
id: 1,
|
||||||
email: 'bibi@bloxberg.de',
|
email: 'bibi@bloxberg.de',
|
||||||
@ -548,11 +548,11 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getPendingCreations', () => {
|
describe('listUnconfirmedContributions', () => {
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
query({
|
query({
|
||||||
query: getPendingCreations,
|
query: listUnconfirmedContributions,
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -562,11 +562,11 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('deletePendingCreation', () => {
|
describe('adminDeleteContribution', () => {
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: deletePendingCreation,
|
mutation: adminDeleteContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: 1,
|
id: 1,
|
||||||
},
|
},
|
||||||
@ -579,11 +579,11 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('confirmPendingCreation', () => {
|
describe('confirmContribution', () => {
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: confirmPendingCreation,
|
mutation: confirmContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: 1,
|
id: 1,
|
||||||
},
|
},
|
||||||
@ -612,9 +612,9 @@ describe('AdminResolver', () => {
|
|||||||
resetToken()
|
resetToken()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('createPendingCreation', () => {
|
describe('adminCreateContribution', () => {
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
await expect(mutate({ mutation: createPendingCreation, variables })).resolves.toEqual(
|
await expect(mutate({ mutation: adminCreateContribution, variables })).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('401 Unauthorized')],
|
errors: [new GraphQLError('401 Unauthorized')],
|
||||||
}),
|
}),
|
||||||
@ -622,11 +622,11 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('createPendingCreations', () => {
|
describe('adminCreateContributions', () => {
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createPendingCreations,
|
mutation: adminCreateContributions,
|
||||||
variables: { pendingCreations: [variables] },
|
variables: { pendingCreations: [variables] },
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
@ -637,11 +637,11 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('updatePendingCreation', () => {
|
describe('adminUpdateContribution', () => {
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: updatePendingCreation,
|
mutation: adminUpdateContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: 1,
|
id: 1,
|
||||||
email: 'bibi@bloxberg.de',
|
email: 'bibi@bloxberg.de',
|
||||||
@ -658,11 +658,11 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getPendingCreations', () => {
|
describe('listUnconfirmedContributions', () => {
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
query({
|
query({
|
||||||
query: getPendingCreations,
|
query: listUnconfirmedContributions,
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -672,11 +672,11 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('deletePendingCreation', () => {
|
describe('adminDeleteContribution', () => {
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: deletePendingCreation,
|
mutation: adminDeleteContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: 1,
|
id: 1,
|
||||||
},
|
},
|
||||||
@ -689,11 +689,11 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('confirmPendingCreation', () => {
|
describe('confirmContribution', () => {
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: confirmPendingCreation,
|
mutation: confirmContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: 1,
|
id: 1,
|
||||||
},
|
},
|
||||||
@ -721,7 +721,7 @@ describe('AdminResolver', () => {
|
|||||||
resetToken()
|
resetToken()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('createPendingCreation', () => {
|
describe('adminCreateContribution', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
creation = await creationFactory(testEnv, {
|
creation = await creationFactory(testEnv, {
|
||||||
@ -734,7 +734,9 @@ describe('AdminResolver', () => {
|
|||||||
|
|
||||||
describe('user to create for does not exist', () => {
|
describe('user to create for does not exist', () => {
|
||||||
it('throws an error', async () => {
|
it('throws an error', async () => {
|
||||||
await expect(mutate({ mutation: createPendingCreation, variables })).resolves.toEqual(
|
await expect(
|
||||||
|
mutate({ mutation: adminCreateContribution, variables }),
|
||||||
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('Could not find user with email: bibi@bloxberg.de')],
|
errors: [new GraphQLError('Could not find user with email: bibi@bloxberg.de')],
|
||||||
}),
|
}),
|
||||||
@ -749,9 +751,13 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('throws an error', async () => {
|
it('throws an error', async () => {
|
||||||
await expect(mutate({ mutation: createPendingCreation, variables })).resolves.toEqual(
|
await expect(
|
||||||
|
mutate({ mutation: adminCreateContribution, variables }),
|
||||||
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('This user was deleted. Cannot make a creation.')],
|
errors: [
|
||||||
|
new GraphQLError('This user was deleted. Cannot create a contribution.'),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -764,9 +770,13 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('throws an error', async () => {
|
it('throws an error', async () => {
|
||||||
await expect(mutate({ mutation: createPendingCreation, variables })).resolves.toEqual(
|
await expect(
|
||||||
|
mutate({ mutation: adminCreateContribution, variables }),
|
||||||
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
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'),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -781,7 +791,7 @@ describe('AdminResolver', () => {
|
|||||||
describe('date of creation is not a date string', () => {
|
describe('date of creation is not a date string', () => {
|
||||||
it('throws an error', async () => {
|
it('throws an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({ mutation: createPendingCreation, variables }),
|
mutate({ mutation: adminCreateContribution, variables }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
@ -801,7 +811,7 @@ describe('AdminResolver', () => {
|
|||||||
1,
|
1,
|
||||||
).toString()
|
).toString()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({ mutation: createPendingCreation, variables }),
|
mutate({ mutation: adminCreateContribution, variables }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
@ -821,7 +831,7 @@ describe('AdminResolver', () => {
|
|||||||
1,
|
1,
|
||||||
).toString()
|
).toString()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({ mutation: createPendingCreation, variables }),
|
mutate({ mutation: adminCreateContribution, variables }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
@ -836,7 +846,7 @@ describe('AdminResolver', () => {
|
|||||||
it('throws an error', async () => {
|
it('throws an error', async () => {
|
||||||
variables.creationDate = new Date().toString()
|
variables.creationDate = new Date().toString()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({ mutation: createPendingCreation, variables }),
|
mutate({ mutation: adminCreateContribution, variables }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
@ -853,11 +863,11 @@ describe('AdminResolver', () => {
|
|||||||
it('returns an array of the open creations for the last three months', async () => {
|
it('returns an array of the open creations for the last three months', async () => {
|
||||||
variables.amount = new Decimal(200)
|
variables.amount = new Decimal(200)
|
||||||
await expect(
|
await expect(
|
||||||
mutate({ mutation: createPendingCreation, variables }),
|
mutate({ mutation: adminCreateContribution, variables }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: {
|
data: {
|
||||||
createPendingCreation: [1000, 1000, 800],
|
adminCreateContribution: [1000, 1000, 800],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -868,7 +878,7 @@ describe('AdminResolver', () => {
|
|||||||
it('returns an array of the open creations for the last three months', async () => {
|
it('returns an array of the open creations for the last three months', async () => {
|
||||||
variables.amount = new Decimal(1000)
|
variables.amount = new Decimal(1000)
|
||||||
await expect(
|
await expect(
|
||||||
mutate({ mutation: createPendingCreation, variables }),
|
mutate({ mutation: adminCreateContribution, variables }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
@ -883,7 +893,7 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('createPendingCreations', () => {
|
describe('adminCreateContributions', () => {
|
||||||
// at this point we have this data in DB:
|
// at this point we have this data in DB:
|
||||||
// bibi@bloxberg.de: [1000, 1000, 800]
|
// bibi@bloxberg.de: [1000, 1000, 800]
|
||||||
// peter@lustig.de: [1000, 600, 1000]
|
// peter@lustig.de: [1000, 600, 1000]
|
||||||
@ -908,16 +918,16 @@ describe('AdminResolver', () => {
|
|||||||
it('returns success, two successful creation and three failed creations', async () => {
|
it('returns success, two successful creation and three failed creations', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createPendingCreations,
|
mutation: adminCreateContributions,
|
||||||
variables: { pendingCreations: massCreationVariables },
|
variables: { pendingCreations: massCreationVariables },
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: {
|
data: {
|
||||||
createPendingCreations: {
|
adminCreateContributions: {
|
||||||
success: true,
|
success: true,
|
||||||
successfulCreation: ['bibi@bloxberg.de', 'peter@lustig.de'],
|
successfulContribution: ['bibi@bloxberg.de', 'peter@lustig.de'],
|
||||||
failedCreation: [
|
failedContribution: [
|
||||||
'stephen@hawking.uk',
|
'stephen@hawking.uk',
|
||||||
'garrick@ollivander.com',
|
'garrick@ollivander.com',
|
||||||
'bob@baumeister.de',
|
'bob@baumeister.de',
|
||||||
@ -929,7 +939,7 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('updatePendingCreation', () => {
|
describe('adminUpdateContribution', () => {
|
||||||
// at this I expect to have this data in DB:
|
// at this I expect to have this data in DB:
|
||||||
// bibi@bloxberg.de: [1000, 1000, 300]
|
// bibi@bloxberg.de: [1000, 1000, 300]
|
||||||
// peter@lustig.de: [1000, 600, 500]
|
// peter@lustig.de: [1000, 600, 500]
|
||||||
@ -940,7 +950,7 @@ describe('AdminResolver', () => {
|
|||||||
it('throws an error', async () => {
|
it('throws an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: updatePendingCreation,
|
mutation: adminUpdateContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: 1,
|
id: 1,
|
||||||
email: 'bob@baumeister.de',
|
email: 'bob@baumeister.de',
|
||||||
@ -961,7 +971,7 @@ describe('AdminResolver', () => {
|
|||||||
it('throws an error', async () => {
|
it('throws an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: updatePendingCreation,
|
mutation: adminUpdateContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: 1,
|
id: 1,
|
||||||
email: 'stephen@hawking.uk',
|
email: 'stephen@hawking.uk',
|
||||||
@ -982,7 +992,7 @@ describe('AdminResolver', () => {
|
|||||||
it('throws an error', async () => {
|
it('throws an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: updatePendingCreation,
|
mutation: adminUpdateContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: -1,
|
id: -1,
|
||||||
email: 'bibi@bloxberg.de',
|
email: 'bibi@bloxberg.de',
|
||||||
@ -993,7 +1003,7 @@ describe('AdminResolver', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('No creation found to given id.')],
|
errors: [new GraphQLError('No contribution found to given id.')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -1003,7 +1013,7 @@ describe('AdminResolver', () => {
|
|||||||
it('throws an error', async () => {
|
it('throws an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: updatePendingCreation,
|
mutation: adminUpdateContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: creation ? creation.id : -1,
|
id: creation ? creation.id : -1,
|
||||||
email: 'bibi@bloxberg.de',
|
email: 'bibi@bloxberg.de',
|
||||||
@ -1016,7 +1026,7 @@ describe('AdminResolver', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
new GraphQLError(
|
new GraphQLError(
|
||||||
'user of the pending creation and send user does not correspond',
|
'user of the pending contribution and send user does not correspond',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@ -1028,7 +1038,7 @@ describe('AdminResolver', () => {
|
|||||||
it('throws an error', async () => {
|
it('throws an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: updatePendingCreation,
|
mutation: adminUpdateContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: creation ? creation.id : -1,
|
id: creation ? creation.id : -1,
|
||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
@ -1053,7 +1063,7 @@ describe('AdminResolver', () => {
|
|||||||
it('returns update creation object', async () => {
|
it('returns update creation object', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: updatePendingCreation,
|
mutation: adminUpdateContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: creation ? creation.id : -1,
|
id: creation ? creation.id : -1,
|
||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
@ -1065,7 +1075,7 @@ describe('AdminResolver', () => {
|
|||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: {
|
data: {
|
||||||
updatePendingCreation: {
|
adminUpdateContribution: {
|
||||||
date: expect.any(String),
|
date: expect.any(String),
|
||||||
memo: 'Danke Peter!',
|
memo: 'Danke Peter!',
|
||||||
amount: '300',
|
amount: '300',
|
||||||
@ -1081,7 +1091,7 @@ describe('AdminResolver', () => {
|
|||||||
it('returns update creation object', async () => {
|
it('returns update creation object', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: updatePendingCreation,
|
mutation: adminUpdateContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: creation ? creation.id : -1,
|
id: creation ? creation.id : -1,
|
||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
@ -1093,7 +1103,7 @@ describe('AdminResolver', () => {
|
|||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: {
|
data: {
|
||||||
updatePendingCreation: {
|
adminUpdateContribution: {
|
||||||
date: expect.any(String),
|
date: expect.any(String),
|
||||||
memo: 'Das war leider zu Viel!',
|
memo: 'Das war leider zu Viel!',
|
||||||
amount: '200',
|
amount: '200',
|
||||||
@ -1106,16 +1116,16 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getPendingCreations', () => {
|
describe('listUnconfirmedContributions', () => {
|
||||||
it('returns four pending creations', async () => {
|
it('returns four pending creations', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
query({
|
query({
|
||||||
query: getPendingCreations,
|
query: listUnconfirmedContributions,
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: {
|
data: {
|
||||||
getPendingCreations: expect.arrayContaining([
|
listUnconfirmedContributions: expect.arrayContaining([
|
||||||
{
|
{
|
||||||
id: expect.any(Number),
|
id: expect.any(Number),
|
||||||
firstName: 'Peter',
|
firstName: 'Peter',
|
||||||
@ -1167,19 +1177,19 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('deletePendingCreation', () => {
|
describe('adminDeleteContribution', () => {
|
||||||
describe('creation id does not exist', () => {
|
describe('creation id does not exist', () => {
|
||||||
it('throws an error', async () => {
|
it('throws an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: deletePendingCreation,
|
mutation: adminDeleteContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: -1,
|
id: -1,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('Creation not found for given id.')],
|
errors: [new GraphQLError('Contribution not found for given id.')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -1189,33 +1199,33 @@ describe('AdminResolver', () => {
|
|||||||
it('returns true', async () => {
|
it('returns true', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: deletePendingCreation,
|
mutation: adminDeleteContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: creation ? creation.id : -1,
|
id: creation ? creation.id : -1,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: { deletePendingCreation: true },
|
data: { adminDeleteContribution: true },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('confirmPendingCreation', () => {
|
describe('confirmContribution', () => {
|
||||||
describe('creation does not exits', () => {
|
describe('creation does not exits', () => {
|
||||||
it('throws an error', async () => {
|
it('throws an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: confirmPendingCreation,
|
mutation: confirmContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: -1,
|
id: -1,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('Creation not found to given id.')],
|
errors: [new GraphQLError('Contribution not found to given id.')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -1235,14 +1245,14 @@ describe('AdminResolver', () => {
|
|||||||
it('thows an error', async () => {
|
it('thows an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: confirmPendingCreation,
|
mutation: confirmContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: creation ? creation.id : -1,
|
id: creation ? creation.id : -1,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('Moderator can not confirm own pending creation')],
|
errors: [new GraphQLError('Moderator can not confirm own contribution')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -1262,14 +1272,14 @@ describe('AdminResolver', () => {
|
|||||||
it('returns true', async () => {
|
it('returns true', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: confirmPendingCreation,
|
mutation: confirmContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: creation ? creation.id : -1,
|
id: creation ? creation.id : -1,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: { confirmPendingCreation: true },
|
data: { confirmContribution: true },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -1287,8 +1297,8 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('confirm two creations one after the other quickly', () => {
|
describe('confirm two creations one after the other quickly', () => {
|
||||||
let c1: AdminPendingCreation | void
|
let c1: Contribution | void
|
||||||
let c2: AdminPendingCreation | void
|
let c2: Contribution | void
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@ -1309,25 +1319,25 @@ describe('AdminResolver', () => {
|
|||||||
// In the futrue this should not throw anymore
|
// In the futrue this should not throw anymore
|
||||||
it('throws an error for the second confirmation', async () => {
|
it('throws an error for the second confirmation', async () => {
|
||||||
const r1 = mutate({
|
const r1 = mutate({
|
||||||
mutation: confirmPendingCreation,
|
mutation: confirmContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: c1 ? c1.id : -1,
|
id: c1 ? c1.id : -1,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const r2 = mutate({
|
const r2 = mutate({
|
||||||
mutation: confirmPendingCreation,
|
mutation: confirmContribution,
|
||||||
variables: {
|
variables: {
|
||||||
id: c2 ? c2.id : -1,
|
id: c2 ? c2.id : -1,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await expect(r1).resolves.toEqual(
|
await expect(r1).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: { confirmPendingCreation: true },
|
data: { confirmContribution: true },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
await expect(r2).resolves.toEqual(
|
await expect(r2).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('Unable to confirm creation.')],
|
errors: [new GraphQLError('Creation was not successful.')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -12,15 +12,15 @@ import {
|
|||||||
FindOperator,
|
FindOperator,
|
||||||
} from '@dbTools/typeorm'
|
} from '@dbTools/typeorm'
|
||||||
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
|
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
|
||||||
import { PendingCreation } from '@model/PendingCreation'
|
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||||
import { CreatePendingCreations } from '@model/CreatePendingCreations'
|
import { AdminCreateContributions } from '@model/AdminCreateContributions'
|
||||||
import { UpdatePendingCreation } from '@model/UpdatePendingCreation'
|
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
|
||||||
import { ContributionLink } from '@model/ContributionLink'
|
import { ContributionLink } from '@model/ContributionLink'
|
||||||
import { ContributionLinkList } from '@model/ContributionLinkList'
|
import { ContributionLinkList } from '@model/ContributionLinkList'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
import { UserRepository } from '@repository/User'
|
import { UserRepository } from '@repository/User'
|
||||||
import CreatePendingCreationArgs from '@arg/CreatePendingCreationArgs'
|
import AdminCreateContributionArgs from '@arg/AdminCreateContributionArgs'
|
||||||
import UpdatePendingCreationArgs from '@arg/UpdatePendingCreationArgs'
|
import AdminUpdateContributionArgs from '@arg/AdminUpdateContributionArgs'
|
||||||
import SearchUsersArgs from '@arg/SearchUsersArgs'
|
import SearchUsersArgs from '@arg/SearchUsersArgs'
|
||||||
import ContributionLinkArgs from '@arg/ContributionLinkArgs'
|
import ContributionLinkArgs from '@arg/ContributionLinkArgs'
|
||||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||||
@ -30,7 +30,7 @@ import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink'
|
|||||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
import { TransactionRepository } from '@repository/Transaction'
|
import { TransactionRepository } from '@repository/Transaction'
|
||||||
import { calculateDecay } from '@/util/decay'
|
import { calculateDecay } from '@/util/decay'
|
||||||
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
|
import { Contribution } from '@entity/Contribution'
|
||||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||||
import { User as dbUser } from '@entity/User'
|
import { User as dbUser } from '@entity/User'
|
||||||
@ -173,72 +173,76 @@ export class AdminResolver {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.CREATE_PENDING_CREATION])
|
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTION])
|
||||||
@Mutation(() => [Number])
|
@Mutation(() => [Number])
|
||||||
async createPendingCreation(
|
async adminCreateContribution(
|
||||||
@Args() { email, amount, memo, creationDate }: CreatePendingCreationArgs,
|
@Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs,
|
||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<Decimal[]> {
|
): Promise<Decimal[]> {
|
||||||
|
logger.trace('adminCreateContribution...')
|
||||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`Could not find user with email: ${email}`)
|
throw new Error(`Could not find user with email: ${email}`)
|
||||||
}
|
}
|
||||||
if (user.deletedAt) {
|
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) {
|
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)
|
const moderator = getUser(context)
|
||||||
|
logger.trace('moderator: ', moderator.id)
|
||||||
const creations = await getUserCreation(user.id)
|
const creations = await getUserCreation(user.id)
|
||||||
|
logger.trace('creations', creations)
|
||||||
const creationDateObj = new Date(creationDate)
|
const creationDateObj = new Date(creationDate)
|
||||||
if (isCreationValid(creations, amount, creationDateObj)) {
|
if (isContributionValid(creations, amount, creationDateObj)) {
|
||||||
const adminPendingCreation = AdminPendingCreation.create()
|
const contribution = Contribution.create()
|
||||||
adminPendingCreation.userId = user.id
|
contribution.userId = user.id
|
||||||
adminPendingCreation.amount = amount
|
contribution.amount = amount
|
||||||
adminPendingCreation.created = new Date()
|
contribution.createdAt = new Date()
|
||||||
adminPendingCreation.date = creationDateObj
|
contribution.contributionDate = creationDateObj
|
||||||
adminPendingCreation.memo = memo
|
contribution.memo = memo
|
||||||
adminPendingCreation.moderator = moderator.id
|
contribution.moderatorId = moderator.id
|
||||||
|
|
||||||
await AdminPendingCreation.save(adminPendingCreation)
|
logger.trace('contribution to save', contribution)
|
||||||
|
await Contribution.save(contribution)
|
||||||
}
|
}
|
||||||
return getUserCreation(user.id)
|
return getUserCreation(user.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.CREATE_PENDING_CREATION])
|
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTIONS])
|
||||||
@Mutation(() => CreatePendingCreations)
|
@Mutation(() => AdminCreateContributions)
|
||||||
async createPendingCreations(
|
async adminCreateContributions(
|
||||||
@Arg('pendingCreations', () => [CreatePendingCreationArgs])
|
@Arg('pendingCreations', () => [AdminCreateContributionArgs])
|
||||||
pendingCreations: CreatePendingCreationArgs[],
|
contributions: AdminCreateContributionArgs[],
|
||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<CreatePendingCreations> {
|
): Promise<AdminCreateContributions> {
|
||||||
let success = false
|
let success = false
|
||||||
const successfulCreation: string[] = []
|
const successfulContribution: string[] = []
|
||||||
const failedCreation: string[] = []
|
const failedContribution: string[] = []
|
||||||
for (const pendingCreation of pendingCreations) {
|
for (const contribution of contributions) {
|
||||||
await this.createPendingCreation(pendingCreation, context)
|
await this.adminCreateContribution(contribution, context)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
successfulCreation.push(pendingCreation.email)
|
successfulContribution.push(contribution.email)
|
||||||
success = true
|
success = true
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
failedCreation.push(pendingCreation.email)
|
failedContribution.push(contribution.email)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
success,
|
success,
|
||||||
successfulCreation,
|
successfulContribution,
|
||||||
failedCreation,
|
failedContribution,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.UPDATE_PENDING_CREATION])
|
@Authorized([RIGHTS.ADMIN_UPDATE_CONTRIBUTION])
|
||||||
@Mutation(() => UpdatePendingCreation)
|
@Mutation(() => AdminUpdateContribution)
|
||||||
async updatePendingCreation(
|
async adminUpdateContribution(
|
||||||
@Args() { id, email, amount, memo, creationDate }: UpdatePendingCreationArgs,
|
@Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs,
|
||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<UpdatePendingCreation> {
|
): Promise<AdminUpdateContribution> {
|
||||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`Could not find user with email: ${email}`)
|
throw new Error(`Could not find user with email: ${email}`)
|
||||||
@ -249,59 +253,65 @@ export class AdminResolver {
|
|||||||
|
|
||||||
const moderator = getUser(context)
|
const moderator = getUser(context)
|
||||||
|
|
||||||
const pendingCreationToUpdate = await AdminPendingCreation.findOne({ id })
|
const contributionToUpdate = await Contribution.findOne({
|
||||||
|
where: { id, confirmedAt: IsNull() },
|
||||||
|
})
|
||||||
|
|
||||||
if (!pendingCreationToUpdate) {
|
if (!contributionToUpdate) {
|
||||||
throw new Error('No creation found to given id.')
|
throw new Error('No contribution found to given id.')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingCreationToUpdate.userId !== user.id) {
|
if (contributionToUpdate.userId !== user.id) {
|
||||||
throw new Error('user of the pending creation and send user does not correspond')
|
throw new Error('user of the pending contribution and send user does not correspond')
|
||||||
}
|
}
|
||||||
|
|
||||||
const creationDateObj = new Date(creationDate)
|
const creationDateObj = new Date(creationDate)
|
||||||
let creations = await getUserCreation(user.id)
|
let creations = await getUserCreation(user.id)
|
||||||
if (pendingCreationToUpdate.date.getMonth() === creationDateObj.getMonth()) {
|
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
|
||||||
creations = updateCreations(creations, pendingCreationToUpdate)
|
creations = updateCreations(creations, contributionToUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// all possible cases not to be true are thrown in this function
|
// all possible cases not to be true are thrown in this function
|
||||||
isCreationValid(creations, amount, creationDateObj)
|
isContributionValid(creations, amount, creationDateObj)
|
||||||
pendingCreationToUpdate.amount = amount
|
contributionToUpdate.amount = amount
|
||||||
pendingCreationToUpdate.memo = memo
|
contributionToUpdate.memo = memo
|
||||||
pendingCreationToUpdate.date = new Date(creationDate)
|
contributionToUpdate.contributionDate = new Date(creationDate)
|
||||||
pendingCreationToUpdate.moderator = moderator.id
|
contributionToUpdate.moderatorId = moderator.id
|
||||||
|
|
||||||
await AdminPendingCreation.save(pendingCreationToUpdate)
|
await Contribution.save(contributionToUpdate)
|
||||||
const result = new UpdatePendingCreation()
|
const result = new AdminUpdateContribution()
|
||||||
result.amount = amount
|
result.amount = amount
|
||||||
result.memo = pendingCreationToUpdate.memo
|
result.memo = contributionToUpdate.memo
|
||||||
result.date = pendingCreationToUpdate.date
|
result.date = contributionToUpdate.contributionDate
|
||||||
|
|
||||||
result.creation = await getUserCreation(user.id)
|
result.creation = await getUserCreation(user.id)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.SEARCH_PENDING_CREATION])
|
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
|
||||||
@Query(() => [PendingCreation])
|
@Query(() => [UnconfirmedContribution])
|
||||||
async getPendingCreations(): Promise<PendingCreation[]> {
|
async listUnconfirmedContributions(): Promise<UnconfirmedContribution[]> {
|
||||||
const pendingCreations = await AdminPendingCreation.find()
|
const contributions = await Contribution.find({ where: { confirmedAt: IsNull() } })
|
||||||
if (pendingCreations.length === 0) {
|
if (contributions.length === 0) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const userIds = pendingCreations.map((p) => p.userId)
|
const userIds = contributions.map((p) => p.userId)
|
||||||
const userCreations = await getUserCreations(userIds)
|
const userCreations = await getUserCreations(userIds)
|
||||||
const users = await dbUser.find({ where: { id: In(userIds) }, withDeleted: true })
|
const users = await dbUser.find({ where: { id: In(userIds) }, withDeleted: true })
|
||||||
|
|
||||||
return pendingCreations.map((pendingCreation) => {
|
return contributions.map((contribution) => {
|
||||||
const user = users.find((u) => u.id === pendingCreation.userId)
|
const user = users.find((u) => u.id === contribution.userId)
|
||||||
const creation = userCreations.find((c) => c.id === pendingCreation.userId)
|
const creation = userCreations.find((c) => c.id === contribution.userId)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...pendingCreation,
|
id: contribution.id,
|
||||||
amount: pendingCreation.amount,
|
userId: contribution.userId,
|
||||||
|
date: contribution.contributionDate,
|
||||||
|
memo: contribution.memo,
|
||||||
|
amount: contribution.amount,
|
||||||
|
moderator: contribution.moderatorId,
|
||||||
firstName: user ? user.firstName : '',
|
firstName: user ? user.firstName : '',
|
||||||
lastName: user ? user.lastName : '',
|
lastName: user ? user.lastName : '',
|
||||||
email: user ? user.email : '',
|
email: user ? user.email : '',
|
||||||
@ -310,69 +320,93 @@ export class AdminResolver {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.DELETE_PENDING_CREATION])
|
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async deletePendingCreation(@Arg('id', () => Int) id: number): Promise<boolean> {
|
async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise<boolean> {
|
||||||
const pendingCreation = await AdminPendingCreation.findOne(id)
|
const contribution = await Contribution.findOne(id)
|
||||||
if (!pendingCreation) {
|
if (!contribution) {
|
||||||
throw new Error('Creation not found for given id.')
|
throw new Error('Contribution not found for given id.')
|
||||||
}
|
}
|
||||||
const res = await AdminPendingCreation.delete(pendingCreation)
|
const res = await contribution.softRemove()
|
||||||
return !!res
|
return !!res
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.CONFIRM_PENDING_CREATION])
|
@Authorized([RIGHTS.CONFIRM_CONTRIBUTION])
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async confirmPendingCreation(
|
async confirmContribution(
|
||||||
@Arg('id', () => Int) id: number,
|
@Arg('id', () => Int) id: number,
|
||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const pendingCreation = await AdminPendingCreation.findOne(id)
|
const contribution = await Contribution.findOne(id)
|
||||||
if (!pendingCreation) {
|
if (!contribution) {
|
||||||
throw new Error('Creation not found to given id.')
|
throw new Error('Contribution not found to given id.')
|
||||||
}
|
}
|
||||||
const moderatorUser = getUser(context)
|
const moderatorUser = getUser(context)
|
||||||
if (moderatorUser.id === pendingCreation.userId)
|
if (moderatorUser.id === contribution.userId)
|
||||||
throw new Error('Moderator can not confirm own pending creation')
|
throw new Error('Moderator can not confirm own contribution')
|
||||||
|
|
||||||
const user = await dbUser.findOneOrFail({ id: pendingCreation.userId }, { withDeleted: true })
|
const user = await dbUser.findOneOrFail({ id: contribution.userId }, { withDeleted: true })
|
||||||
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a creation.')
|
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a contribution.')
|
||||||
|
|
||||||
const creations = await getUserCreation(pendingCreation.userId, false)
|
const creations = await getUserCreation(contribution.userId, false)
|
||||||
if (!isCreationValid(creations, pendingCreation.amount, pendingCreation.date)) {
|
if (!isContributionValid(creations, contribution.amount, contribution.contributionDate)) {
|
||||||
throw new Error('Creation is not valid!!')
|
throw new Error('Creation is not valid!!')
|
||||||
}
|
}
|
||||||
|
|
||||||
const receivedCallDate = new Date()
|
const receivedCallDate = new Date()
|
||||||
|
|
||||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
const lastTransaction = await transactionRepository.findLastForUser(pendingCreation.userId)
|
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 newBalance = new Decimal(0)
|
||||||
let decay: Decay | null = null
|
let decay: Decay | null = null
|
||||||
if (lastTransaction) {
|
if (lastTransaction) {
|
||||||
decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, receivedCallDate)
|
decay = calculateDecay(
|
||||||
newBalance = decay.balance
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -614,25 +648,30 @@ interface CreationMap {
|
|||||||
creations: Decimal[]
|
creations: Decimal[]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserCreation(id: number, includePending = true): Promise<Decimal[]> {
|
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
||||||
|
logger.trace('getUserCreation', id, includePending)
|
||||||
const creations = await getUserCreations([id], includePending)
|
const creations = await getUserCreations([id], includePending)
|
||||||
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserCreations(ids: number[], includePending = true): Promise<CreationMap[]> {
|
async function getUserCreations(ids: number[], includePending = true): Promise<CreationMap[]> {
|
||||||
|
logger.trace('getUserCreations:', ids, includePending)
|
||||||
const months = getCreationMonths()
|
const months = getCreationMonths()
|
||||||
|
logger.trace('getUserCreations months', months)
|
||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
|
|
||||||
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
||||||
|
logger.trace('getUserCreations dateFilter', dateFilter)
|
||||||
|
|
||||||
const unionString = includePending
|
const unionString = includePending
|
||||||
? `
|
? `
|
||||||
UNION
|
UNION
|
||||||
SELECT date AS date, amount AS amount, userId AS userId FROM admin_pending_creations
|
SELECT contribution_date AS date, amount AS amount, user_id AS userId FROM contributions
|
||||||
WHERE userId IN (${ids.toString()})
|
WHERE user_id IN (${ids.toString()})
|
||||||
AND date >= ${dateFilter}`
|
AND contribution_date >= ${dateFilter}
|
||||||
|
AND confirmed_at IS NULL AND deleted_at IS NULL`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
const unionQuery = await queryRunner.manager.query(`
|
const unionQuery = await queryRunner.manager.query(`
|
||||||
@ -662,17 +701,22 @@ async function getUserCreations(ids: number[], includePending = true): Promise<C
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCreations(creations: Decimal[], pendingCreation: AdminPendingCreation): Decimal[] {
|
function updateCreations(creations: Decimal[], contribution: Contribution): Decimal[] {
|
||||||
const index = getCreationIndex(pendingCreation.date.getMonth())
|
const index = getCreationIndex(contribution.contributionDate.getMonth())
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
throw new Error('You cannot create GDD for a month older than the last three months.')
|
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
|
return creations
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCreationValid(creations: Decimal[], amount: Decimal, creationDate: Date) {
|
export const isContributionValid = (
|
||||||
|
creations: Decimal[],
|
||||||
|
amount: Decimal,
|
||||||
|
creationDate: Date,
|
||||||
|
): boolean => {
|
||||||
|
logger.trace('isContributionValid', creations, amount, creationDate)
|
||||||
const index = getCreationIndex(creationDate.getMonth())
|
const index = getCreationIndex(creationDate.getMonth())
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
|
|||||||
@ -1,7 +1,21 @@
|
|||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
import { Context, getUser } from '@/server/context'
|
import { Context, getUser } from '@/server/context'
|
||||||
import { Resolver, Args, Arg, Authorized, Ctx, Mutation, Query, Int } from 'type-graphql'
|
import { getConnection } from '@dbTools/typeorm'
|
||||||
|
import {
|
||||||
|
Resolver,
|
||||||
|
Args,
|
||||||
|
Arg,
|
||||||
|
Authorized,
|
||||||
|
Ctx,
|
||||||
|
Mutation,
|
||||||
|
Query,
|
||||||
|
Int,
|
||||||
|
createUnionType,
|
||||||
|
} from 'type-graphql'
|
||||||
import { TransactionLink } from '@model/TransactionLink'
|
import { TransactionLink } from '@model/TransactionLink'
|
||||||
|
import { ContributionLink } from '@model/ContributionLink'
|
||||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
|
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||||
import { User as dbUser } from '@entity/User'
|
import { User as dbUser } from '@entity/User'
|
||||||
import TransactionLinkArgs from '@arg/TransactionLinkArgs'
|
import TransactionLinkArgs from '@arg/TransactionLinkArgs'
|
||||||
import Paginated from '@arg/Paginated'
|
import Paginated from '@arg/Paginated'
|
||||||
@ -12,6 +26,17 @@ import { User } from '@model/User'
|
|||||||
import { calculateDecay } from '@/util/decay'
|
import { calculateDecay } from '@/util/decay'
|
||||||
import { executeTransaction } from './TransactionResolver'
|
import { executeTransaction } from './TransactionResolver'
|
||||||
import { Order } from '@enum/Order'
|
import { Order } from '@enum/Order'
|
||||||
|
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||||
|
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||||
|
import { getUserCreation, isContributionValid } from './AdminResolver'
|
||||||
|
import { Decay } from '@model/Decay'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||||
|
|
||||||
|
const QueryLinkResult = createUnionType({
|
||||||
|
name: 'QueryLinkResult', // the name of the GraphQL union
|
||||||
|
types: () => [TransactionLink, ContributionLink] as const, // function that returns tuple of object types classes
|
||||||
|
})
|
||||||
|
|
||||||
// TODO: do not export, test it inside the resolver
|
// TODO: do not export, test it inside the resolver
|
||||||
export const transactionLinkCode = (date: Date): string => {
|
export const transactionLinkCode = (date: Date): string => {
|
||||||
@ -95,15 +120,23 @@ export class TransactionLinkResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.QUERY_TRANSACTION_LINK])
|
@Authorized([RIGHTS.QUERY_TRANSACTION_LINK])
|
||||||
@Query(() => TransactionLink)
|
@Query(() => QueryLinkResult)
|
||||||
async queryTransactionLink(@Arg('code') code: string): Promise<TransactionLink> {
|
async queryTransactionLink(@Arg('code') code: string): Promise<typeof QueryLinkResult> {
|
||||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
|
if (code.match(/^CL-/)) {
|
||||||
const user = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
const contributionLink = await DbContributionLink.findOneOrFail(
|
||||||
let redeemedBy: User | null = null
|
{ code: code.replace('CL-', '') },
|
||||||
if (transactionLink && transactionLink.redeemedBy) {
|
{ withDeleted: true },
|
||||||
redeemedBy = new User(await dbUser.findOneOrFail({ id: transactionLink.redeemedBy }))
|
)
|
||||||
|
return new ContributionLink(contributionLink)
|
||||||
|
} else {
|
||||||
|
const transactionLink = await dbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
|
||||||
|
const user = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
||||||
|
let redeemedBy: User | null = null
|
||||||
|
if (transactionLink && transactionLink.redeemedBy) {
|
||||||
|
redeemedBy = new User(await dbUser.findOneOrFail({ id: transactionLink.redeemedBy }))
|
||||||
|
}
|
||||||
|
return new TransactionLink(transactionLink, new User(user), redeemedBy)
|
||||||
}
|
}
|
||||||
return new TransactionLink(transactionLink, new User(user), redeemedBy)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS])
|
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS])
|
||||||
@ -137,31 +170,143 @@ export class TransactionLinkResolver {
|
|||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
|
||||||
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
if (user.id === linkedUser.id) {
|
if (code.match(/^CL-/)) {
|
||||||
throw new Error('Cannot redeem own transaction link.')
|
logger.info('redeem contribution link...')
|
||||||
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
|
await queryRunner.connect()
|
||||||
|
await queryRunner.startTransaction('SERIALIZABLE')
|
||||||
|
try {
|
||||||
|
const contributionLink = await queryRunner.manager
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select('contributionLink')
|
||||||
|
.from(DbContributionLink, 'contributionLink')
|
||||||
|
.where('contributionLink.code = :code', { code: code.replace('CL-', '') })
|
||||||
|
.getOne()
|
||||||
|
if (!contributionLink) {
|
||||||
|
logger.error('no contribution link found to given code:', code)
|
||||||
|
throw new Error('No contribution link found')
|
||||||
|
}
|
||||||
|
logger.info('...contribution link found with id', contributionLink.id)
|
||||||
|
if (new Date(contributionLink.validFrom).getTime() > now.getTime()) {
|
||||||
|
logger.error(
|
||||||
|
'contribution link is not valid yet. Valid from: ',
|
||||||
|
contributionLink.validFrom,
|
||||||
|
)
|
||||||
|
throw new Error('Contribution link not valid yet')
|
||||||
|
}
|
||||||
|
if (contributionLink.validTo) {
|
||||||
|
if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) {
|
||||||
|
logger.error('contribution link is depricated. Valid to: ', contributionLink.validTo)
|
||||||
|
throw new Error('Contribution link is depricated')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (contributionLink.cycle !== 'ONCE') {
|
||||||
|
logger.error('contribution link has unknown cycle', contributionLink.cycle)
|
||||||
|
throw new Error('Contribution link has unknown cycle')
|
||||||
|
}
|
||||||
|
// Test ONCE rule
|
||||||
|
const alreadyRedeemed = await queryRunner.manager
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select('contribution')
|
||||||
|
.from(DbContribution, 'contribution')
|
||||||
|
.where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', {
|
||||||
|
linkId: contributionLink.id,
|
||||||
|
id: user.id,
|
||||||
|
})
|
||||||
|
.getOne()
|
||||||
|
if (alreadyRedeemed) {
|
||||||
|
logger.error('contribution link with rule ONCE already redeemed by user with id', user.id)
|
||||||
|
throw new Error('Contribution link already redeemed')
|
||||||
|
}
|
||||||
|
|
||||||
|
const creations = await getUserCreation(user.id, false)
|
||||||
|
logger.info('open creations', creations)
|
||||||
|
if (!isContributionValid(creations, contributionLink.amount, now)) {
|
||||||
|
logger.error(
|
||||||
|
'Amount of Contribution link exceeds available amount for this month',
|
||||||
|
contributionLink.amount,
|
||||||
|
)
|
||||||
|
throw new Error('Amount of Contribution link exceeds available amount')
|
||||||
|
}
|
||||||
|
const contribution = new DbContribution()
|
||||||
|
contribution.userId = user.id
|
||||||
|
contribution.createdAt = now
|
||||||
|
contribution.contributionDate = now
|
||||||
|
contribution.memo = contributionLink.memo
|
||||||
|
contribution.amount = contributionLink.amount
|
||||||
|
contribution.contributionLinkId = contributionLink.id
|
||||||
|
await queryRunner.manager.insert(DbContribution, contribution)
|
||||||
|
|
||||||
|
const lastTransaction = await queryRunner.manager
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select('transaction')
|
||||||
|
.from(DbTransaction, 'transaction')
|
||||||
|
.where('transaction.userId = :id', { id: user.id })
|
||||||
|
.orderBy('transaction.balanceDate', 'DESC')
|
||||||
|
.getOne()
|
||||||
|
let newBalance = new Decimal(0)
|
||||||
|
|
||||||
|
let decay: Decay | null = null
|
||||||
|
if (lastTransaction) {
|
||||||
|
decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now)
|
||||||
|
newBalance = decay.balance
|
||||||
|
}
|
||||||
|
newBalance = newBalance.add(contributionLink.amount.toString())
|
||||||
|
|
||||||
|
const transaction = new DbTransaction()
|
||||||
|
transaction.typeId = TransactionTypeId.CREATION
|
||||||
|
transaction.memo = contribution.memo
|
||||||
|
transaction.userId = contribution.userId
|
||||||
|
transaction.previous = lastTransaction ? lastTransaction.id : null
|
||||||
|
transaction.amount = contribution.amount
|
||||||
|
transaction.creationDate = contribution.contributionDate
|
||||||
|
transaction.balance = newBalance
|
||||||
|
transaction.balanceDate = now
|
||||||
|
transaction.decay = decay ? decay.decay : new Decimal(0)
|
||||||
|
transaction.decayStart = decay ? decay.start : null
|
||||||
|
await queryRunner.manager.insert(DbTransaction, transaction)
|
||||||
|
|
||||||
|
contribution.confirmedAt = now
|
||||||
|
contribution.transactionId = transaction.id
|
||||||
|
await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction()
|
||||||
|
logger.info('creation from contribution link commited successfuly.')
|
||||||
|
} catch (e) {
|
||||||
|
await queryRunner.rollbackTransaction()
|
||||||
|
logger.error(`Creation from contribution link was not successful: ${e}`)
|
||||||
|
throw new Error(`Creation from contribution link was not successful. ${e}`)
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
||||||
|
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
||||||
|
|
||||||
|
if (user.id === linkedUser.id) {
|
||||||
|
throw new Error('Cannot redeem own transaction link.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transactionLink.validUntil.getTime() < now.getTime()) {
|
||||||
|
throw new Error('Transaction Link is not valid anymore.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transactionLink.redeemedBy) {
|
||||||
|
throw new Error('Transaction Link already redeemed.')
|
||||||
|
}
|
||||||
|
|
||||||
|
await executeTransaction(
|
||||||
|
transactionLink.amount,
|
||||||
|
transactionLink.memo,
|
||||||
|
linkedUser,
|
||||||
|
user,
|
||||||
|
transactionLink,
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transactionLink.validUntil.getTime() < now.getTime()) {
|
|
||||||
throw new Error('Transaction Link is not valid anymore.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transactionLink.redeemedBy) {
|
|
||||||
throw new Error('Transaction Link already redeemed.')
|
|
||||||
}
|
|
||||||
|
|
||||||
await executeTransaction(
|
|
||||||
transactionLink.amount,
|
|
||||||
transactionLink.memo,
|
|
||||||
linkedUser,
|
|
||||||
user,
|
|
||||||
transactionLink,
|
|
||||||
)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,10 @@ import CONFIG from '@/config'
|
|||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
||||||
import { printTimeDuration, activationLink } from './UserResolver'
|
import { printTimeDuration, activationLink } from './UserResolver'
|
||||||
|
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
||||||
|
// import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
|
||||||
|
import { ContributionLink } from '@model/ContributionLink'
|
||||||
|
// import { TransactionLink } from '@entity/TransactionLink'
|
||||||
|
|
||||||
import { logger } from '@test/testSetup'
|
import { logger } from '@test/testSetup'
|
||||||
|
|
||||||
@ -69,6 +73,7 @@ describe('UserResolver', () => {
|
|||||||
|
|
||||||
let result: any
|
let result: any
|
||||||
let emailOptIn: string
|
let emailOptIn: string
|
||||||
|
let user: User[]
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
@ -86,7 +91,6 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('valid input data', () => {
|
describe('valid input data', () => {
|
||||||
let user: User[]
|
|
||||||
let loginEmailOptIn: LoginEmailOptIn[]
|
let loginEmailOptIn: LoginEmailOptIn[]
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
user = await User.find()
|
user = await User.find()
|
||||||
@ -114,6 +118,7 @@ describe('UserResolver', () => {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
publisherId: 1234,
|
publisherId: 1234,
|
||||||
referrerId: null,
|
referrerId: null,
|
||||||
|
contributionLinkId: null,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@ -195,6 +200,72 @@ describe('UserResolver', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('redeem codes', () => {
|
||||||
|
describe('contribution link', () => {
|
||||||
|
let link: ContributionLink
|
||||||
|
beforeAll(async () => {
|
||||||
|
// activate account of admin Peter Lustig
|
||||||
|
await mutate({
|
||||||
|
mutation: setPassword,
|
||||||
|
variables: { code: emailOptIn, password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
// make Peter Lustig Admin
|
||||||
|
const peter = await User.findOneOrFail({ id: user[0].id })
|
||||||
|
peter.isAdmin = new Date()
|
||||||
|
await peter.save()
|
||||||
|
// factory logs in as Peter Lustig
|
||||||
|
link = await contributionLinkFactory(testEnv, {
|
||||||
|
name: 'Dokumenta 2022',
|
||||||
|
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022',
|
||||||
|
amount: 200,
|
||||||
|
validFrom: new Date(2022, 5, 18),
|
||||||
|
validTo: new Date(2022, 8, 25),
|
||||||
|
})
|
||||||
|
resetToken()
|
||||||
|
await mutate({
|
||||||
|
mutation: createUser,
|
||||||
|
variables: { ...variables, email: 'ein@besucher.de', redeemCode: 'CL-' + link.code },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets the contribution link id', async () => {
|
||||||
|
await expect(User.findOne({ email: 'ein@besucher.de' })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
contributionLinkId: link.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/* A transaction link requires GDD on account
|
||||||
|
describe('transaction link', () => {
|
||||||
|
let code: string
|
||||||
|
beforeAll(async () => {
|
||||||
|
// factory logs in as Peter Lustig
|
||||||
|
await transactionLinkFactory(testEnv, {
|
||||||
|
email: 'peter@lustig.de',
|
||||||
|
amount: 19.99,
|
||||||
|
memo: `Kein Trick, keine Zauberrei,
|
||||||
|
bei Gradidio sei dabei!`,
|
||||||
|
})
|
||||||
|
const transactionLink = await TransactionLink.findOneOrFail()
|
||||||
|
resetToken()
|
||||||
|
await mutate({
|
||||||
|
mutation: createUser,
|
||||||
|
variables: { ...variables, email: 'neuer@user.de', redeemCode: transactionLink.code },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets the referrer id to Peter Lustigs id', async () => {
|
||||||
|
await expect(User.findOne({ email: 'neuer@user.de' })).resolves.toEqual(expect.objectContaining({
|
||||||
|
referrerId: user[0].id,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
*/
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('setPassword', () => {
|
describe('setPassword', () => {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import CONFIG from '@/config'
|
|||||||
import { User } from '@model/User'
|
import { User } from '@model/User'
|
||||||
import { User as DbUser } from '@entity/User'
|
import { User as DbUser } from '@entity/User'
|
||||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
|
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
||||||
import { encode } from '@/auth/JWT'
|
import { encode } from '@/auth/JWT'
|
||||||
import CreateUserArgs from '@arg/CreateUserArgs'
|
import CreateUserArgs from '@arg/CreateUserArgs'
|
||||||
import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
|
import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
|
||||||
@ -349,10 +350,20 @@ export class UserResolver {
|
|||||||
dbUser.passphrase = passphrase.join(' ')
|
dbUser.passphrase = passphrase.join(' ')
|
||||||
logger.debug('new dbUser=' + dbUser)
|
logger.debug('new dbUser=' + dbUser)
|
||||||
if (redeemCode) {
|
if (redeemCode) {
|
||||||
const transactionLink = await dbTransactionLink.findOne({ code: redeemCode })
|
if (redeemCode.match(/^CL-/)) {
|
||||||
logger.info('redeemCode found transactionLink=' + transactionLink)
|
const contributionLink = await dbContributionLink.findOne({
|
||||||
if (transactionLink) {
|
code: redeemCode.replace('CL-', ''),
|
||||||
dbUser.referrerId = transactionLink.userId
|
})
|
||||||
|
logger.info('redeemCode found contributionLink=' + contributionLink)
|
||||||
|
if (contributionLink) {
|
||||||
|
dbUser.contributionLinkId = contributionLink.id
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const transactionLink = await dbTransactionLink.findOne({ code: redeemCode })
|
||||||
|
logger.info('redeemCode found transactionLink=' + transactionLink)
|
||||||
|
if (transactionLink) {
|
||||||
|
dbUser.referrerId = transactionLink.userId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO this field has no null allowed unlike the loginServer table
|
// TODO this field has no null allowed unlike the loginServer table
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||||
import { createContributionLink } from '@/seeds/graphql/mutations'
|
import { createContributionLink } from '@/seeds/graphql/mutations'
|
||||||
import { login } from '@/seeds/graphql/queries'
|
import { login } from '@/seeds/graphql/queries'
|
||||||
|
import { ContributionLink } from '@model/ContributionLink'
|
||||||
import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface'
|
import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface'
|
||||||
|
|
||||||
export const contributionLinkFactory = async (
|
export const contributionLinkFactory = async (
|
||||||
client: ApolloServerTestClient,
|
client: ApolloServerTestClient,
|
||||||
contributionLink: ContributionLinkInterface,
|
contributionLink: ContributionLinkInterface,
|
||||||
): Promise<void> => {
|
): Promise<ContributionLink> => {
|
||||||
const { mutate, query } = client
|
const { mutate, query } = client
|
||||||
|
|
||||||
// login as admin
|
// login as admin
|
||||||
@ -23,5 +24,6 @@ export const contributionLinkFactory = async (
|
|||||||
validTo: contributionLink.validTo ? contributionLink.validTo.toISOString() : undefined,
|
validTo: contributionLink.validTo ? contributionLink.validTo.toISOString() : undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
await mutate({ mutation: createContributionLink, variables })
|
const result = await mutate({ mutation: createContributionLink, variables })
|
||||||
|
return result.data.createContributionLink
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* 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 { login } from '@/seeds/graphql/queries'
|
||||||
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
||||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
import { Transaction } from '@entity/Transaction'
|
import { Transaction } from '@entity/Transaction'
|
||||||
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
|
import { Contribution } from '@entity/Contribution'
|
||||||
// import CONFIG from '@/config/index'
|
// import CONFIG from '@/config/index'
|
||||||
|
|
||||||
export const nMonthsBefore = (date: Date, months = 1): string => {
|
export const nMonthsBefore = (date: Date, months = 1): string => {
|
||||||
@ -17,23 +17,23 @@ export const nMonthsBefore = (date: Date, months = 1): string => {
|
|||||||
export const creationFactory = async (
|
export const creationFactory = async (
|
||||||
client: ApolloServerTestClient,
|
client: ApolloServerTestClient,
|
||||||
creation: CreationInterface,
|
creation: CreationInterface,
|
||||||
): Promise<AdminPendingCreation | void> => {
|
): Promise<Contribution | void> => {
|
||||||
const { mutate, query } = client
|
const { mutate, query } = client
|
||||||
|
|
||||||
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||||
|
|
||||||
// TODO it would be nice to have this mutation return the id
|
// 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 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 },
|
where: { userId: user.id, amount: creation.amount },
|
||||||
order: { created: 'DESC' },
|
order: { createdAt: 'DESC' },
|
||||||
})
|
})
|
||||||
|
|
||||||
if (creation.confirmed) {
|
if (creation.confirmed) {
|
||||||
await mutate({ mutation: confirmPendingCreation, variables: { id: pendingCreation.id } })
|
await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } })
|
||||||
|
|
||||||
if (creation.moveCreationDate) {
|
if (creation.moveCreationDate) {
|
||||||
const transaction = await Transaction.findOneOrFail({
|
const transaction = await Transaction.findOneOrFail({
|
||||||
|
|||||||
@ -81,15 +81,20 @@ export const createTransactionLink = gql`
|
|||||||
|
|
||||||
// from admin interface
|
// from admin interface
|
||||||
|
|
||||||
export const createPendingCreation = gql`
|
export const adminCreateContribution = gql`
|
||||||
mutation ($email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
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!) {
|
mutation ($id: Int!) {
|
||||||
confirmPendingCreation(id: $id)
|
confirmContribution(id: $id)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -105,19 +110,19 @@ export const unDeleteUser = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const createPendingCreations = gql`
|
export const adminCreateContributions = gql`
|
||||||
mutation ($pendingCreations: [CreatePendingCreationArgs!]!) {
|
mutation ($pendingCreations: [AdminCreateContributionArgs!]!) {
|
||||||
createPendingCreations(pendingCreations: $pendingCreations) {
|
adminCreateContributions(pendingCreations: $pendingCreations) {
|
||||||
success
|
success
|
||||||
successfulCreation
|
successfulContribution
|
||||||
failedCreation
|
failedContribution
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const updatePendingCreation = gql`
|
export const adminUpdateContribution = gql`
|
||||||
mutation ($id: Int!, $email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
mutation ($id: Int!, $email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||||
updatePendingCreation(
|
adminUpdateContribution(
|
||||||
id: $id
|
id: $id
|
||||||
email: $email
|
email: $email
|
||||||
amount: $amount
|
amount: $amount
|
||||||
@ -132,9 +137,9 @@ export const updatePendingCreation = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const deletePendingCreation = gql`
|
export const adminDeleteContribution = gql`
|
||||||
mutation ($id: Int!) {
|
mutation ($id: Int!) {
|
||||||
deletePendingCreation(id: $id)
|
adminDeleteContribution(id: $id)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@ -173,9 +173,9 @@ export const queryTransactionLink = gql`
|
|||||||
|
|
||||||
// from admin interface
|
// from admin interface
|
||||||
|
|
||||||
export const getPendingCreations = gql`
|
export const listUnconfirmedContributions = gql`
|
||||||
query {
|
query {
|
||||||
getPendingCreations {
|
listUnconfirmedContributions {
|
||||||
id
|
id
|
||||||
firstName
|
firstName
|
||||||
lastName
|
lastName
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import { readFileSync } from 'fs'
|
|||||||
|
|
||||||
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
|
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
|
||||||
|
|
||||||
options.categories.default.level = CONFIG.LOG_LEVEL
|
options.categories.backend.level = CONFIG.LOG_LEVEL
|
||||||
|
options.categories.apollo.level = CONFIG.LOG_LEVEL
|
||||||
|
|
||||||
log4js.configure(options)
|
log4js.configure(options)
|
||||||
|
|
||||||
|
|||||||
@ -37,9 +37,11 @@ ${mutation || query}variables: ${JSON.stringify(filterVariables(variables), null
|
|||||||
return {
|
return {
|
||||||
willSendResponse(requestContext: any) {
|
willSendResponse(requestContext: any) {
|
||||||
if (requestContext.context.user) logger.info(`User ID: ${requestContext.context.user.id}`)
|
if (requestContext.context.user) logger.info(`User ID: ${requestContext.context.user.id}`)
|
||||||
if (requestContext.response.data)
|
if (requestContext.response.data) {
|
||||||
logger.info(`Response-Data:
|
logger.info('Response Success!')
|
||||||
|
logger.trace(`Response-Data:
|
||||||
${JSON.stringify(requestContext.response.data, null, 2)}`)
|
${JSON.stringify(requestContext.response.data, null, 2)}`)
|
||||||
|
}
|
||||||
if (requestContext.response.errors)
|
if (requestContext.response.errors)
|
||||||
logger.error(`Response-Errors:
|
logger.error(`Response-Errors:
|
||||||
${JSON.stringify(requestContext.response.errors, null, 2)}`)
|
${JSON.stringify(requestContext.response.errors, null, 2)}`)
|
||||||
|
|||||||
48
database/entity/0039-contributions_table/Contribution.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, DeleteDateColumn } from 'typeorm'
|
||||||
|
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||||
|
|
||||||
|
@Entity('contributions')
|
||||||
|
export class Contribution extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({ unsigned: true, nullable: false, name: 'user_id' })
|
||||||
|
userId: number
|
||||||
|
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@Column({ type: 'datetime', nullable: false, name: 'contribution_date' })
|
||||||
|
contributionDate: Date
|
||||||
|
|
||||||
|
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||||
|
memo: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'decimal',
|
||||||
|
precision: 40,
|
||||||
|
scale: 20,
|
||||||
|
nullable: false,
|
||||||
|
transformer: DecimalTransformer,
|
||||||
|
})
|
||||||
|
amount: Decimal
|
||||||
|
|
||||||
|
@Column({ unsigned: true, nullable: true, name: 'moderator_id' })
|
||||||
|
moderatorId: number
|
||||||
|
|
||||||
|
@Column({ unsigned: true, nullable: true, name: 'contribution_link_id' })
|
||||||
|
contributionLinkId: number
|
||||||
|
|
||||||
|
@Column({ unsigned: true, nullable: true, name: 'confirmed_by' })
|
||||||
|
confirmedBy: number
|
||||||
|
|
||||||
|
@Column({ nullable: true, name: 'confirmed_at' })
|
||||||
|
confirmedAt: Date
|
||||||
|
|
||||||
|
@Column({ unsigned: true, nullable: true, name: 'transaction_id' })
|
||||||
|
transactionId: number
|
||||||
|
|
||||||
|
@DeleteDateColumn({ name: 'deleted_at' })
|
||||||
|
deletedAt: Date | null
|
||||||
|
}
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm'
|
||||||
|
|
||||||
|
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||||
|
export class User extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
|
||||||
|
pubKey: Buffer
|
||||||
|
|
||||||
|
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
|
||||||
|
privKey: Buffer
|
||||||
|
|
||||||
|
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||||
|
email: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'first_name',
|
||||||
|
length: 255,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
firstName: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'last_name',
|
||||||
|
length: 255,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
lastName: string
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deletedAt: Date | null
|
||||||
|
|
||||||
|
@Column({ type: 'bigint', default: 0, unsigned: true })
|
||||||
|
password: BigInt
|
||||||
|
|
||||||
|
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
|
||||||
|
emailHash: Buffer
|
||||||
|
|
||||||
|
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
|
||||||
|
emailChecked: boolean
|
||||||
|
|
||||||
|
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
|
||||||
|
language: string
|
||||||
|
|
||||||
|
@Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
|
||||||
|
isAdmin: Date | null
|
||||||
|
|
||||||
|
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||||
|
referrerId?: number | null
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'contribution_link_id',
|
||||||
|
type: 'int',
|
||||||
|
unsigned: true,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
})
|
||||||
|
contributionLinkId?: number | null
|
||||||
|
|
||||||
|
@Column({ name: 'publisher_id', default: 0 })
|
||||||
|
publisherId: number
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'text',
|
||||||
|
name: 'passphrase',
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
})
|
||||||
|
passphrase: string
|
||||||
|
}
|
||||||
1
database/entity/Contribution.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { Contribution } from './0039-contributions_table/Contribution'
|
||||||
@ -1 +1 @@
|
|||||||
export { User } from './0037-drop_user_setting_table/User'
|
export { User } from './0040-add_contribution_link_id_to_user/User'
|
||||||
|
|||||||
@ -5,10 +5,10 @@ import { Migration } from './Migration'
|
|||||||
import { Transaction } from './Transaction'
|
import { Transaction } from './Transaction'
|
||||||
import { TransactionLink } from './TransactionLink'
|
import { TransactionLink } from './TransactionLink'
|
||||||
import { User } from './User'
|
import { User } from './User'
|
||||||
import { AdminPendingCreation } from './AdminPendingCreation'
|
import { Contribution } from './Contribution'
|
||||||
|
|
||||||
export const entities = [
|
export const entities = [
|
||||||
AdminPendingCreation,
|
Contribution,
|
||||||
ContributionLink,
|
ContributionLink,
|
||||||
LoginElopageBuys,
|
LoginElopageBuys,
|
||||||
LoginEmailOptIn,
|
LoginEmailOptIn,
|
||||||
|
|||||||
59
database/migrations/0039-contributions_table.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/* MIGRATION to rename ADMIN_PENDING_CREATION table and add columns
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
await queryFn('RENAME TABLE `admin_pending_creations` TO `contributions`;')
|
||||||
|
|
||||||
|
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `userId` `user_id` int(10);')
|
||||||
|
|
||||||
|
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `created` `created_at` datetime;')
|
||||||
|
|
||||||
|
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `date` `contribution_date` datetime;')
|
||||||
|
|
||||||
|
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `moderator` `moderator_id` int(10);')
|
||||||
|
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE `contributions` ADD COLUMN `contribution_link_id` int(10) unsigned DEFAULT NULL AFTER `moderator_id`;',
|
||||||
|
)
|
||||||
|
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE `contributions` ADD COLUMN `confirmed_by` int(10) unsigned DEFAULT NULL AFTER `contribution_link_id`;',
|
||||||
|
)
|
||||||
|
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE `contributions` ADD COLUMN `confirmed_at` datetime DEFAULT NULL AFTER `confirmed_by`;',
|
||||||
|
)
|
||||||
|
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE `contributions` ADD COLUMN `transaction_id` int(10) unsigned DEFAULT NULL AFTER `confirmed_at`;',
|
||||||
|
)
|
||||||
|
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE `contributions` ADD COLUMN `deleted_at` datetime DEFAULT NULL AFTER `confirmed_at`;',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
await queryFn('ALTER TABLE `contributions` DROP COLUMN IF EXISTS `deleted_at`;')
|
||||||
|
|
||||||
|
await queryFn('ALTER TABLE `contributions` DROP COLUMN IF EXISTS `transaction_id`;')
|
||||||
|
|
||||||
|
await queryFn('ALTER TABLE `contributions` DROP COLUMN IF EXISTS `confirmed_at`;')
|
||||||
|
|
||||||
|
await queryFn('ALTER TABLE `contributions` DROP COLUMN IF EXISTS `confirmed_by`;')
|
||||||
|
|
||||||
|
await queryFn('ALTER TABLE `contributions` DROP COLUMN IF EXISTS `contribution_link_id`;')
|
||||||
|
|
||||||
|
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `moderator_id` `moderator` int(10);')
|
||||||
|
|
||||||
|
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `created_at` `created` datetime;')
|
||||||
|
|
||||||
|
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `contribution_date` `date` datetime;')
|
||||||
|
|
||||||
|
await queryFn('ALTER TABLE `contributions` CHANGE COLUMN `user_id` `userId` int(10);')
|
||||||
|
|
||||||
|
await queryFn('RENAME TABLE `contributions` TO `admin_pending_creations`;')
|
||||||
|
}
|
||||||
14
database/migrations/0040-add_contribution_link_id_to_user.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/* MIGRATION TO ADD contribution_link_id FIELD TO users */
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE `users` ADD COLUMN `contribution_link_id` int UNSIGNED DEFAULT NULL AFTER `referrer_id`;',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
await queryFn('ALTER TABLE `users` DROP COLUMN `contribution_link_id`;')
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gradido-database",
|
"name": "gradido-database",
|
||||||
"version": "1.9.0",
|
"version": "1.10.0",
|
||||||
"description": "Gradido Database Tool to execute database migrations",
|
"description": "Gradido Database Tool to execute database migrations",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": "https://github.com/gradido/gradido/database",
|
"repository": "https://github.com/gradido/gradido/database",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bootstrap-vue-gradido-wallet",
|
"name": "bootstrap-vue-gradido-wallet",
|
||||||
"version": "1.9.0",
|
"version": "1.10.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node run/server.js",
|
"start": "node run/server.js",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 673 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 63 KiB |
@ -3,7 +3,7 @@
|
|||||||
<b-navbar toggleable="lg" class="pr-4">
|
<b-navbar toggleable="lg" class="pr-4">
|
||||||
<b-navbar-brand>
|
<b-navbar-brand>
|
||||||
<b-img
|
<b-img
|
||||||
class="imgLogo position-absolute ml--3 mt--3 p-2 zindex1000"
|
class="imgLogo position-absolute ml--3 mt-lg--2 mt-3 p-2 zindex1000"
|
||||||
:src="logo"
|
:src="logo"
|
||||||
width="200"
|
width="200"
|
||||||
alt="..."
|
alt="..."
|
||||||
@ -18,7 +18,7 @@
|
|||||||
<b-img class="sheet-img position-absolute d-block d-lg-none zindex1000" :src="sheet"></b-img>
|
<b-img class="sheet-img position-absolute d-block d-lg-none zindex1000" :src="sheet"></b-img>
|
||||||
<b-navbar-toggle target="nav-collapse" class="zindex1000"></b-navbar-toggle>
|
<b-navbar-toggle target="nav-collapse" class="zindex1000"></b-navbar-toggle>
|
||||||
|
|
||||||
<b-collapse id="nav-collapse" is-nav class="mt-5 mt-lg-0">
|
<b-collapse id="nav-collapse" is-nav class="ml-5">
|
||||||
<b-navbar-nav class="ml-auto" right>
|
<b-navbar-nav class="ml-auto" right>
|
||||||
<b-nav-item :href="`https://gradido.net/${$i18n.locale}`" target="_blank">
|
<b-nav-item :href="`https://gradido.net/${$i18n.locale}`" target="_blank">
|
||||||
{{ $t('auth.navbar.aboutGradido') }}
|
{{ $t('auth.navbar.aboutGradido') }}
|
||||||
@ -49,10 +49,18 @@ export default {
|
|||||||
color: #383838 !important;
|
color: #383838 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar-toggler {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.authNavbar > .router-link-exact-active {
|
.authNavbar > .router-link-exact-active {
|
||||||
color: #0e79bc !important;
|
color: #0e79bc !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.navbar-toggler > span.navbar-toggler-icon {
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(4, 112, 6, 1)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
|
||||||
.auth-header {
|
.auth-header {
|
||||||
font-family: 'Open Sans', sans-serif !important;
|
font-family: 'Open Sans', sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|||||||
133
frontend/src/components/LanguageSwitch2.spec.js
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import LanguageSwitch from './LanguageSwitch2'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const updateUserInfosMutationMock = jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
updateUserInfos: {
|
||||||
|
validValues: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('LanguageSwitch', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
email: 'he@ho.he',
|
||||||
|
language: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$store: {
|
||||||
|
state,
|
||||||
|
commit: jest.fn(),
|
||||||
|
},
|
||||||
|
$i18n: {
|
||||||
|
locale: 'en',
|
||||||
|
},
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
$apollo: {
|
||||||
|
mutate: updateUserInfosMutationMock,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(LanguageSwitch, { localVue, mocks })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the component', () => {
|
||||||
|
expect(wrapper.find('div.language-switch').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with locales en and de', () => {
|
||||||
|
describe('empty store', () => {
|
||||||
|
describe('navigator language is "en-US"', () => {
|
||||||
|
const languageGetter = jest.spyOn(navigator, 'language', 'get')
|
||||||
|
it('shows English as default navigator langauge', async () => {
|
||||||
|
languageGetter.mockReturnValue('en-US')
|
||||||
|
wrapper.vm.setCurrentLanguage()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('navigator language is "de-DE"', () => {
|
||||||
|
const languageGetter = jest.spyOn(navigator, 'language', 'get')
|
||||||
|
it('shows Deutsch as language ', async () => {
|
||||||
|
languageGetter.mockReturnValue('de-DE')
|
||||||
|
wrapper.vm.setCurrentLanguage()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('navigator language is "es-ES" (not supported)', () => {
|
||||||
|
const languageGetter = jest.spyOn(navigator, 'language', 'get')
|
||||||
|
it('shows English as language ', async () => {
|
||||||
|
languageGetter.mockReturnValue('es-ES')
|
||||||
|
wrapper.vm.setCurrentLanguage()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('no navigator langauge', () => {
|
||||||
|
const languageGetter = jest.spyOn(navigator, 'language', 'get')
|
||||||
|
it('shows English as language ', async () => {
|
||||||
|
languageGetter.mockReturnValue(null)
|
||||||
|
wrapper.vm.setCurrentLanguage()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('language "de" in store', () => {
|
||||||
|
it('shows Deutsch as language', async () => {
|
||||||
|
wrapper.vm.$store.state.language = 'de'
|
||||||
|
wrapper.vm.setCurrentLanguage()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('language menu', () => {
|
||||||
|
it('has English and German as languages to choose', () => {
|
||||||
|
expect(wrapper.findAll('span.locales')).toHaveLength(2)
|
||||||
|
})
|
||||||
|
it('has English as first language to choose', () => {
|
||||||
|
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
|
||||||
|
})
|
||||||
|
it('has German as second language to choose', () => {
|
||||||
|
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('calls the API', () => {
|
||||||
|
it("with locale 'de'", () => {
|
||||||
|
wrapper.findAll('span.locales').at(1).trigger('click')
|
||||||
|
expect(updateUserInfosMutationMock).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
variables: {
|
||||||
|
locale: 'de',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// it("with locale 'en'", () => {
|
||||||
|
// wrapper.findAll('span.locales').at(0).trigger('click')
|
||||||
|
// expect(updateUserInfosMutationMock).toBeCalledWith(
|
||||||
|
// expect.objectContaining({
|
||||||
|
// variables: {
|
||||||
|
// locale: 'en',
|
||||||
|
// },
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -4,10 +4,10 @@
|
|||||||
v-for="(lang, index) in locales"
|
v-for="(lang, index) in locales"
|
||||||
@click.prevent="saveLocale(lang.code)"
|
@click.prevent="saveLocale(lang.code)"
|
||||||
:key="lang.code"
|
:key="lang.code"
|
||||||
class="pointer pr-3"
|
class="pointer pr-2"
|
||||||
:class="$store.state.language === lang.code ? 'c-blau' : 'c-grey'"
|
:class="$store.state.language === lang.code ? 'c-blau' : 'c-grey'"
|
||||||
>
|
>
|
||||||
{{ lang.name }}
|
<span class="locales">{{ lang.name }}</span>
|
||||||
<span class="ml-3">{{ locales.length - 1 > index ? $t('math.pipe') : '' }}</span>
|
<span class="ml-3">{{ locales.length - 1 > index ? $t('math.pipe') : '' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="redeem-information">
|
<div class="redeem-information">
|
||||||
<b-jumbotron bg-variant="muted" text-variant="dark" border-variant="info">
|
<b-jumbotron bg-variant="muted" text-variant="dark" border-variant="info">
|
||||||
<h1>
|
<h1 v-if="isContributionLink">
|
||||||
{{ firstName }}
|
{{ CONFIG.COMMUNITY_NAME }}
|
||||||
|
{{ $t('contribution-link.thanksYouWith') }} {{ amount | GDD }}
|
||||||
|
</h1>
|
||||||
|
<h1 v-else>
|
||||||
|
{{ user.firstName }}
|
||||||
{{ $t('transaction-link.send_you') }} {{ amount | GDD }}
|
{{ $t('transaction-link.send_you') }} {{ amount | GDD }}
|
||||||
</h1>
|
</h1>
|
||||||
<b>{{ memo }}</b>
|
<b>{{ memo }}</b>
|
||||||
@ -10,12 +14,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RedeemInformation',
|
name: 'RedeemInformation',
|
||||||
props: {
|
props: {
|
||||||
firstName: { type: String, required: true },
|
user: { type: Object, required: false },
|
||||||
amount: { type: String, required: true },
|
amount: { type: String, required: true },
|
||||||
memo: { type: String, required: true, default: '' },
|
memo: { type: String, required: true, default: '' },
|
||||||
|
isContributionLink: { type: Boolean, default: false },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
CONFIG,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="redeem-logged-out">
|
<div class="redeem-logged-out">
|
||||||
<redeem-information :firstName="user.firstName" :amount="amount" :memo="memo" />
|
<redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
|
||||||
|
|
||||||
<b-jumbotron>
|
<b-jumbotron>
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
@ -32,9 +32,8 @@ export default {
|
|||||||
RedeemInformation,
|
RedeemInformation,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
user: { type: Object, required: true },
|
linkData: { type: Object, required: true },
|
||||||
amount: { type: String, required: true },
|
isContributionLink: { type: Boolean, default: false },
|
||||||
memo: { type: String, required: true, default: '' },
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
login() {
|
login() {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="redeem-self-creator">
|
<div class="redeem-self-creator">
|
||||||
<redeem-information :firstName="user.firstName" :amount="amount" :memo="memo" />
|
<redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
|
||||||
|
|
||||||
<b-jumbotron>
|
<b-jumbotron>
|
||||||
<div class="mb-3 text-center">
|
<div class="mb-3 text-center">
|
||||||
@ -23,9 +23,8 @@ export default {
|
|||||||
RedeemInformation,
|
RedeemInformation,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
user: { type: Object, required: true },
|
linkData: { type: Object, required: true },
|
||||||
amount: { type: String, required: true },
|
isContributionLink: { type: Boolean, default: false },
|
||||||
memo: { type: String, required: true, default: '' },
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="redeem-valid">
|
<div class="redeem-valid">
|
||||||
<redeem-information :firstName="user.firstName" :amount="amount" :memo="memo" />
|
<redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
|
||||||
<b-jumbotron>
|
<b-jumbotron>
|
||||||
<div class="mb-3 text-center">
|
<div class="mb-3 text-center">
|
||||||
<b-button variant="primary" @click="$emit('redeem-link', amount)" size="lg">
|
<b-button variant="primary" @click="$emit('redeem-link', linkData.amount)" size="lg">
|
||||||
{{ $t('gdd_per_link.redeem') }}
|
{{ $t('gdd_per_link.redeem') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
@ -19,9 +19,8 @@ export default {
|
|||||||
RedeemInformation,
|
RedeemInformation,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
user: { type: Object, required: false },
|
linkData: { type: Object, required: true },
|
||||||
amount: { type: String, required: false },
|
isContributionLink: { type: Boolean, default: false },
|
||||||
memo: { type: String, required: false, default: '' },
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -114,17 +114,33 @@ export const queryOptIn = gql`
|
|||||||
export const queryTransactionLink = gql`
|
export const queryTransactionLink = gql`
|
||||||
query($code: String!) {
|
query($code: String!) {
|
||||||
queryTransactionLink(code: $code) {
|
queryTransactionLink(code: $code) {
|
||||||
id
|
... on TransactionLink {
|
||||||
amount
|
id
|
||||||
memo
|
amount
|
||||||
createdAt
|
memo
|
||||||
validUntil
|
createdAt
|
||||||
redeemedAt
|
validUntil
|
||||||
deletedAt
|
redeemedAt
|
||||||
user {
|
deletedAt
|
||||||
firstName
|
user {
|
||||||
publisherId
|
firstName
|
||||||
email
|
publisherId
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on ContributionLink {
|
||||||
|
id
|
||||||
|
validTo
|
||||||
|
validFrom
|
||||||
|
amount
|
||||||
|
name
|
||||||
|
memo
|
||||||
|
cycle
|
||||||
|
createdAt
|
||||||
|
code
|
||||||
|
link
|
||||||
|
deletedAt
|
||||||
|
maxAmountPerMonth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,8 +31,8 @@
|
|||||||
<auth-navbar-small />
|
<auth-navbar-small />
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
<b-row class="mt-5 pl-2 pl-md-0 pl-lg-0">
|
<b-row class="mt-0 mt-md-5 pl-2 pl-md-0 pl-lg-0">
|
||||||
<b-col cols="9">
|
<b-col lg="9" md="9" sm="12">
|
||||||
<div class="h1 mb--2">{{ $t('welcome') }}</div>
|
<div class="h1 mb--2">{{ $t('welcome') }}</div>
|
||||||
<div class="h1 mb-0">{{ $t('WelcomeBy', { name: communityName }) }}</div>
|
<div class="h1 mb-0">{{ $t('WelcomeBy', { name: communityName }) }}</div>
|
||||||
<div class="mb-0">{{ $t('1000thanks') }}</div>
|
<div class="mb-0">{{ $t('1000thanks') }}</div>
|
||||||
|
|||||||
@ -26,6 +26,9 @@
|
|||||||
"other-communities": "Weitere Gemeinschaften",
|
"other-communities": "Weitere Gemeinschaften",
|
||||||
"switch-to-this-community": "zu dieser Gemeinschaft wechseln"
|
"switch-to-this-community": "zu dieser Gemeinschaft wechseln"
|
||||||
},
|
},
|
||||||
|
"contribution-link": {
|
||||||
|
"thanksYouWith": "dankt dir mit"
|
||||||
|
},
|
||||||
"decay": {
|
"decay": {
|
||||||
"before_startblock_transaction": "Diese Transaktion beinhaltet keine Vergänglichkeit.",
|
"before_startblock_transaction": "Diese Transaktion beinhaltet keine Vergänglichkeit.",
|
||||||
"calculation_decay": "Berechnung der Vergänglichkeit",
|
"calculation_decay": "Berechnung der Vergänglichkeit",
|
||||||
|
|||||||
@ -26,6 +26,9 @@
|
|||||||
"other-communities": "Other communities",
|
"other-communities": "Other communities",
|
||||||
"switch-to-this-community": "Switch to this community"
|
"switch-to-this-community": "Switch to this community"
|
||||||
},
|
},
|
||||||
|
"contribution-link": {
|
||||||
|
"thanksYouWith": "thanks you with"
|
||||||
|
},
|
||||||
"decay": {
|
"decay": {
|
||||||
"before_startblock_transaction": "This transaction does not include decay.",
|
"before_startblock_transaction": "This transaction does not include decay.",
|
||||||
"calculation_decay": "Calculation of Decay",
|
"calculation_decay": "Calculation of Decay",
|
||||||
|
|||||||
@ -88,7 +88,11 @@ export default {
|
|||||||
? this.$t('message.checkEmail')
|
? this.$t('message.checkEmail')
|
||||||
: this.$t('message.reset')
|
: this.$t('message.reset')
|
||||||
this.messageButtonText = this.$t('login')
|
this.messageButtonText = this.$t('login')
|
||||||
this.messageButtonLinktTo = '/login'
|
if (this.$route.params.code) {
|
||||||
|
this.messageButtonLinktTo = `/login/${this.$route.params.code}`
|
||||||
|
} else {
|
||||||
|
this.messageButtonLinktTo = '/login'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
let errorMessage
|
let errorMessage
|
||||||
|
|||||||
@ -24,6 +24,7 @@ const transactionLinkValidExpireDate = () => {
|
|||||||
apolloQueryMock.mockResolvedValue({
|
apolloQueryMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
queryTransactionLink: {
|
queryTransactionLink: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
id: 92,
|
id: 92,
|
||||||
amount: '22',
|
amount: '22',
|
||||||
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
||||||
@ -82,6 +83,7 @@ describe('TransactionLink', () => {
|
|||||||
variables: {
|
variables: {
|
||||||
code: 'some-code',
|
code: 'some-code',
|
||||||
},
|
},
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -90,6 +92,7 @@ describe('TransactionLink', () => {
|
|||||||
apolloQueryMock.mockResolvedValue({
|
apolloQueryMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
queryTransactionLink: {
|
queryTransactionLink: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
id: 92,
|
id: 92,
|
||||||
amount: '22',
|
amount: '22',
|
||||||
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
||||||
@ -120,6 +123,7 @@ describe('TransactionLink', () => {
|
|||||||
apolloQueryMock.mockResolvedValue({
|
apolloQueryMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
queryTransactionLink: {
|
queryTransactionLink: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
id: 92,
|
id: 92,
|
||||||
amount: '22',
|
amount: '22',
|
||||||
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
||||||
@ -150,6 +154,7 @@ describe('TransactionLink', () => {
|
|||||||
apolloQueryMock.mockResolvedValue({
|
apolloQueryMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
queryTransactionLink: {
|
queryTransactionLink: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
id: 92,
|
id: 92,
|
||||||
amount: '22',
|
amount: '22',
|
||||||
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
||||||
@ -177,9 +182,11 @@ describe('TransactionLink', () => {
|
|||||||
|
|
||||||
describe('no token in store', () => {
|
describe('no token in store', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mocks.$store.state.token = null
|
||||||
apolloQueryMock.mockResolvedValue({
|
apolloQueryMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
queryTransactionLink: {
|
queryTransactionLink: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
id: 92,
|
id: 92,
|
||||||
amount: '22',
|
amount: '22',
|
||||||
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
||||||
@ -213,6 +220,7 @@ describe('TransactionLink', () => {
|
|||||||
apolloQueryMock.mockResolvedValue({
|
apolloQueryMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
queryTransactionLink: {
|
queryTransactionLink: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
id: 92,
|
id: 92,
|
||||||
amount: '22',
|
amount: '22',
|
||||||
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
||||||
@ -248,6 +256,7 @@ describe('TransactionLink', () => {
|
|||||||
apolloQueryMock.mockResolvedValue({
|
apolloQueryMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
queryTransactionLink: {
|
queryTransactionLink: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
id: 92,
|
id: 92,
|
||||||
amount: '22',
|
amount: '22',
|
||||||
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
||||||
@ -298,7 +307,7 @@ describe('TransactionLink', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('toasts a success message', () => {
|
it('toasts a success message', () => {
|
||||||
expect(mocks.$t).toBeCalledWith('gdd_per_link.redeemed', { n: '22' })
|
expect(mocks.$t).toBeCalledWith('gdd_per_link.redeem')
|
||||||
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.redeemed; ')
|
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.redeemed; ')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="show-transaction-link-informations">
|
<div class="show-transaction-link-informations">
|
||||||
<div class="text-center"><b-img :src="img" fluid alt="logo"></b-img></div>
|
|
||||||
<b-container class="mt-4">
|
<b-container class="mt-4">
|
||||||
<transaction-link-item :type="itemType">
|
<transaction-link-item :type="itemType">
|
||||||
<template #LOGGED_OUT>
|
<template #LOGGED_OUT>
|
||||||
<redeem-logged-out v-bind="linkData" />
|
<redeem-logged-out :linkData="linkData" :isContributionLink="isContributionLink" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #SELF_CREATOR>
|
<template #SELF_CREATOR>
|
||||||
<redeem-self-creator v-bind="linkData" />
|
<redeem-self-creator :linkData="linkData" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #VALID>
|
<template #VALID>
|
||||||
<redeem-valid v-bind="linkData" @redeem-link="redeemLink" />
|
<redeem-valid
|
||||||
|
:linkData="linkData"
|
||||||
|
:isContributionLink="isContributionLink"
|
||||||
|
@redeem-link="redeemLink"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #TEXT>
|
<template #TEXT>
|
||||||
@ -44,6 +47,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
img: '/img/brand/green.png',
|
img: '/img/brand/green.png',
|
||||||
linkData: {
|
linkData: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
amount: '123.45',
|
amount: '123.45',
|
||||||
memo: 'memo',
|
memo: 'memo',
|
||||||
user: {
|
user: {
|
||||||
@ -57,6 +61,7 @@ export default {
|
|||||||
setTransactionLinkInformation() {
|
setTransactionLinkInformation() {
|
||||||
this.$apollo
|
this.$apollo
|
||||||
.query({
|
.query({
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
query: queryTransactionLink,
|
query: queryTransactionLink,
|
||||||
variables: {
|
variables: {
|
||||||
code: this.$route.params.code,
|
code: this.$route.params.code,
|
||||||
@ -64,37 +69,45 @@ export default {
|
|||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.linkData = result.data.queryTransactionLink
|
this.linkData = result.data.queryTransactionLink
|
||||||
|
if (this.linkData.__typename === 'ContributionLink' && this.$store.state.token) {
|
||||||
|
this.mutationLink(this.linkData.amount)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
this.toastError(err.message)
|
this.toastError(err.message)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
mutationLink(amount) {
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: redeemTransactionLink,
|
||||||
|
variables: {
|
||||||
|
code: this.$route.params.code,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.toastSuccess(
|
||||||
|
this.$t('gdd_per_link.redeemed', {
|
||||||
|
n: amount,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
this.$router.push('/overview')
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.toastError(err.message)
|
||||||
|
this.$router.push('/overview')
|
||||||
|
})
|
||||||
|
},
|
||||||
redeemLink(amount) {
|
redeemLink(amount) {
|
||||||
this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.redeem-text')).then(async (value) => {
|
this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.redeem-text')).then((value) => {
|
||||||
if (value)
|
if (value) this.mutationLink(amount)
|
||||||
await this.$apollo
|
|
||||||
.mutate({
|
|
||||||
mutation: redeemTransactionLink,
|
|
||||||
variables: {
|
|
||||||
code: this.$route.params.code,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.toastSuccess(
|
|
||||||
this.$t('gdd_per_link.redeemed', {
|
|
||||||
n: amount,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
this.$router.push('/overview')
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
this.toastError(err.message)
|
|
||||||
this.$router.push('/overview')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isContributionLink() {
|
||||||
|
return this.$route.params.code.search(/^CL-/) === 0
|
||||||
|
},
|
||||||
itemType() {
|
itemType() {
|
||||||
// link wurde gelöscht: am, von
|
// link wurde gelöscht: am, von
|
||||||
if (this.linkData.deletedAt) {
|
if (this.linkData.deletedAt) {
|
||||||
@ -124,16 +137,12 @@ export default {
|
|||||||
|
|
||||||
if (this.$store.state.token) {
|
if (this.$store.state.token) {
|
||||||
// logged in, nicht berechtigt einzulösen, eigener link
|
// logged in, nicht berechtigt einzulösen, eigener link
|
||||||
if (this.$store.state.email === this.linkData.user.email) {
|
if (this.linkData.user && this.$store.state.email === this.linkData.user.email) {
|
||||||
return `SELF_CREATOR`
|
return `SELF_CREATOR`
|
||||||
}
|
}
|
||||||
|
|
||||||
// logged in und berechtigt einzulösen
|
// logged in und berechtigt einzulösen
|
||||||
if (
|
if (!this.linkData.redeemedAt && !this.linkData.deletedAt) {
|
||||||
this.$store.state.email !== this.linkData.user.email &&
|
|
||||||
!this.linkData.redeemedAt &&
|
|
||||||
!this.linkData.deletedAt
|
|
||||||
) {
|
|
||||||
return `VALID`
|
return `VALID`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,11 @@ const authLink = new ApolloLink((operation, forward) => {
|
|||||||
|
|
||||||
const apolloClient = new ApolloClient({
|
const apolloClient = new ApolloClient({
|
||||||
link: authLink.concat(httpLink),
|
link: authLink.concat(httpLink),
|
||||||
cache: new InMemoryCache(),
|
cache: new InMemoryCache({
|
||||||
|
possibleTypes: {
|
||||||
|
QueryLinkResult: ['TransactionLink', 'ContributionLink'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const apolloProvider = new VueApollo({
|
export const apolloProvider = new VueApollo({
|
||||||
|
|||||||
@ -4615,20 +4615,10 @@ caniuse-api@^3.0.0:
|
|||||||
lodash.memoize "^4.1.2"
|
lodash.memoize "^4.1.2"
|
||||||
lodash.uniq "^4.5.0"
|
lodash.uniq "^4.5.0"
|
||||||
|
|
||||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109:
|
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.30001251"
|
version "1.0.30001354"
|
||||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001354.tgz"
|
||||||
integrity sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==
|
integrity sha512-mImKeCkyGDAHNywYFA4bqnLAzTUvVkqPvhY4DV47X+Gl2c5Z8c3KNETnXp14GQt11LvxE8AwjzGxJ+rsikiOzg==
|
||||||
|
|
||||||
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==
|
|
||||||
|
|
||||||
capture-exit@^2.0.0:
|
capture-exit@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gradido",
|
"name": "gradido",
|
||||||
"version": "1.9.0",
|
"version": "1.10.0",
|
||||||
"description": "Gradido",
|
"description": "Gradido",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": "git@github.com:gradido/gradido.git",
|
"repository": "git@github.com:gradido/gradido.git",
|
||||||
|
|||||||